All developer pages

Developers

Webhooks

Endpoints you register receive the events you subscribe to, signed, retried and replayable. 33 event types are available.

Delivery

Register an endpoint in the console's Developer page or with the API, and subscribe it to event types or to all of them. Each event is delivered once per endpoint as a JSON POST with CC-Event-Id, CC-Event-Type, CC-Delivery-Id and CC-Signature headers.

Answer with a 2xx status. Anything else is retried with exponential back-off, and after 8 attempts the delivery goes to the dead letter, from where you can replay it. A replay is a new delivery with the same event id, so use the event id to ignore duplicates.

  • Endpoint URLs must use https and resolve to public addresses; redirects are not followed.
  • Each endpoint has its own secret, shown once when it is created.
  • Rotating the secret signs with both the new and the old secret for 24 hours, so you can switch without missing a delivery.

Verifying the signature

CC-Signature is t=<unix seconds>,v1=<hex>, with a second v1 during a rotation. The v1 value is the HMAC-SHA-256, under the endpoint's secret, of the timestamp, a full stop and the raw request body. Check it against the raw bytes before parsing the JSON, reject timestamps more than 5 minutes away, and remember each delivery you accepted by its timestamp and body (not by the signature that matched) until its timestamp is too old, so a replayed request is refused even with one of its two signatures removed. In Node.js:

// Verify a webhook delivery (Node.js 18 or later, no dependencies).
import { createHash, createHmac, timingSafeEqual } from 'node:crypto';

const TOLERANCE_SECONDS = 300;
// Deliveries already accepted (a digest of the timestamp and the body), each kept until its
// timestamp is too old to be accepted anyway. With several processes, keep them in your database.
const seen = new Map();

/**
 * header:  the CC-Signature request header
 * rawBody: the request body exactly as received (a Buffer or a string)
 * secrets: your endpoint secret, or [new, old] while a rotation is under way
 * Returns true only for a genuine delivery that has not been seen before.
 */
export function verifyWebhook(header, rawBody, secrets, nowMs = Date.now()) {
  let t = null;
  const v1 = [];
  for (const part of String(header ?? '').split(',')) {
    const [k, v = ''] = part.split('=').map((s) => s.trim());
    if (k === 't' && /^[0-9]{1,12}$/.test(v)) t = Number(v);
    else if (k === 'v1' && /^[0-9a-f]{64}$/.test(v)) v1.push(v);
  }
  if (t === null || v1.length === 0) return false;
  const now = Math.floor(nowMs / 1000);
  if (Math.abs(now - t) > TOLERANCE_SECONDS) return false;
  const body = Buffer.isBuffer(rawBody) ? rawBody : Buffer.from(rawBody);
  const genuine = [secrets].flat().some((secret) => {
    const want = createHmac('sha256', secret).update(`${t}.`).update(body).digest();
    return v1.some((got) => timingSafeEqual(want, Buffer.from(got, 'hex')));
  });
  if (!genuine) return false;
  // A replay is the same timestamp and body, whichever of its signatures it still carries:
  // during a rotation a delivery has two, and resending it with only one is still a replay.
  const key = createHash('sha256').update(`${t}.`).update(body).digest('hex');
  for (const [k, until] of seen) if (until < now) seen.delete(k);
  if (seen.has(key)) return false;
  seen.set(key, t + TOLERANCE_SECONDS);
  return true;
}

// With node:http:
//   const chunks = [];
//   req.on('data', (c) => chunks.push(c)).on('end', () => {
//     const raw = Buffer.concat(chunks);
//     if (!verifyWebhook(req.headers['cc-signature'], raw, process.env.CC_WEBHOOK_SECRET)) return res.writeHead(401).end();
//     res.writeHead(204).end();
//     handle(JSON.parse(raw)); // a retry is a new delivery: use the CC-Event-Id header to ignore an event you already handled
//   });

Event catalogue

The event types you can subscribe to and the fields in each event's data. Internal events are never sent.

Event catalogue
Event typeMeaningData fields
conversation.updatedA conversation changed status (opened, resolved, closed) or was reopened by the customer.conversationId, status, reason (optional), disposition (optional)
conversation.transferredA conversation moved to another queue or agent.conversationId, queueId (optional), agentId (optional)
message.createdA message was received or sent on any channel. Internal notes (and replies synced from Slack or Teams) never leave: only inbound and outbound messages are sent.conversationId, interactionId, messageId, channel, direction, kind
message.statusAn outbound message moved forward: sent, delivered, read or failed.messageId, conversationId, status, reason (optional)
routing.enqueuedWork entered a queue.workItemId, conversationId, interactionId, channel, queueId
routing.assignedAn agent took a piece of work.workItemId, conversationId, agentId
contact.mergedTwo contacts were found to be one customer and merged (automatically on verified identifiers, or by a person).contactId, mergedContactId, mergeId, kind
contact.unmergedA contact merge was undone: what it moved is back on the merged contact.contactId, mergedContactId, mergeId
contact.opted_inA contact gave consent on a channel for a purpose.contactId, channel, purpose
contact.opted_outA contact withdrew consent (STOP, an opt-out disposition, an unsubscribe).contactId (optional), channel, purpose (optional)
email.bouncedAn e-mail address bounced and is suppressed.address, kind
sla.warningA ticket reached its SLA policy's warning share of a target.conversationId, caseNumber, kind, dueAt
sla.breachedA first-response, next-response or resolution target passed unmet.conversationId, kind, dueAt
ticket.createdA conversation became a ticket with a reference number.conversationId, caseNumber, priority
ticket.updatedA ticket's case fields, status, priority, tags or assignee changed.conversationId, caseNumber, changes
ticket.mergedA ticket was merged into another.conversationId, into, caseNumber
ticket.splitThreads were split off a ticket into a new one.conversationId, from, caseNumber, interactionIds
survey.sentA CSAT or NPS survey went out after a conversation was resolved.surveyId, conversationId, channel, kind
survey.answeredA customer answered a survey.surveyId, conversationId, kind, score, via, followUpConversationId (optional)
dialler.attemptAn outbound call attempt changed state.campaignId, attemptId, leadId, status, outcome (optional)
dialler.dispositionAn agent recorded a call outcome.campaignId, attemptId, leadId, agentId, code
campaign.changedA campaign was created, edited, started, paused or finished.campaignId, status (optional)
agent.stateAn agent's presence changed.agentId, state, reason (optional)
alert.firedA threshold alert rule fired (queue wait, abandon rate, service level or adherence crossed its threshold).alertId, ruleId, rule, metric, value, threshold, unit, queueId (optional), text
alert.resolvedA firing alert rule went back inside its threshold.alertId, ruleId, rule, metric, value, threshold, unit, queueId (optional), text
export.readyAn analytics export finished; the data is at downloadUrl until it expires.exportId, dataset, format, rows, hasMore, nextCursor, downloadUrl (optional), downloadExpiresAt (optional)
outcome.recordedA deposit or conversion was recorded and attributed.outcomeId, kind, amountMinor, currency, occurredAt, agentId (optional), campaignId (optional), channel (optional), templateName (optional)
secureform.submittedA customer submitted a secure form sent in a conversation. Ids only: what they sent is never in an event.requestId, conversationId, formId
change.requestedSomeone asked for a change to a flow, routing, a rule or a price book that needs the approval of a second person.changeId, objectType, objectId, action
change.decidedA change request was applied, rejected, cancelled, found stale or failed.changeId, objectType, objectId, status
emergency.changedEmergency mode was switched on or off, tenant-wide or for one queue.emergencyId, active, scope, queueId (optional), divertQueueId (optional), pausedCampaigns, resumedCampaigns (optional)
monitor.flaggedAn AI conversation monitor flagged a conversation for a person to look at.resultId, monitorId, monitor, conversationId, severity
wallet.low_balanceThe prepaid balance fell below its threshold.availableMinor, thresholdMinor, currency