Skip to content
RosettaChat

Outbound webhooks

Create an outbound webhook in your workspace to receive events at your own HTTPS endpoint. Each webhook subscribes to one or more events, is scoped to a channel or the whole workspace, and is signed with a secret shown once at creation.

A message.created delivery looks like this (fields under conversation and message.direction are present for support channels):

{
"event": "message.created",
"channel": { "id": "chn_…", "name": "Website", "kind": "support" },
"message": {
"id": "msg_…",
"content_type": "text",
"created_at": "2026-07-13T12:00:00+00:00",
"direction": "visitor"
},
"conversation": {
"id": "cnv_…",
"external_reference": "order-1234",
"external_user_id": "user-42"
}
}

direction is visitor, agent, or system.

The conversation.created and conversation.closed deliveries carry the fuller conversation shape, including any signed metadata you attached:

{
"event": "conversation.created",
"conversation": {
"id": "cnv_…",
"status": "open",
"external_reference": "order-1234",
"external_user_id": "user-42",
"metadata": { "plan": "enterprise", "account_value": 4200 }
}
}

By default the payload carries no message text — only metadata. If the webhook opts into content, the payload adds:

"message": {
"content": "How can I help?",
"content_language": "en"
}

The delivery is created when the message is created. If the translation into the webhook’s preferred content_language has already completed, content is that translation; otherwise it is the original text and its source language. A later translation completion does not trigger a replacement delivery, so receivers must not assume the message text is always in content_language.

Every delivery carries:

X-Rosetta-Signature: t=<unix>,v1=<hmac_sha256("<t>.<body>", secret)>

Recompute the HMAC over "<t>.<rawBody>" with your webhook secret and compare in constant time. Reject deliveries whose timestamp is too old (a 5-minute window is a good default) to guard against replays.

// Node.js (Express raw body)
import crypto from 'node:crypto';
function verify(rawBody, header, secret) {
const parts = Object.fromEntries(
header.split(',').map((kv) => kv.split('=')),
);
const t = parts.t;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto
.createHmac('sha256', secret)
.update(`${t}.${rawBody}`)
.digest('hex');
return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1));
}

Every delivery is recorded — status, response code, attempts — so you can debug integrations. Failed deliveries are retried with backoff, and you can manually retry a delivery from the webhook’s delivery log in workspace settings.