Webhooks
Get a signed HTTP request when a conversation starts, escalates, gets a reply or is resolved.
Webhooks push events from Resolido to your system as they happen – post escalations to Slack, sync conversations to a CRM, or start your own automation. They follow the Standard Webhooks specification, so existing libraries can verify them.
Create a webhook
In Settings → Developers → Webhooks, click Add webhook, enter a public https URL and choose the events. Copy the signing secret (whsec_…) – it is shown only once. Owners and admins can manage webhooks; an organization can have up to 10.
Over the API (scope webhooks:manage):
curl -X POST https://api.resolido.com/v1/webhooks \
-H "Authorization: Bearer $RESOLIDO_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/hooks/resolido", "events": ["conversation.escalated"]}'Send test (or POST /webhooks/{id}/test) delivers a webhook.test event right away.
Events
| Event | When |
|---|---|
conversation.created | A customer started a new conversation – the first customer message in the chat widget, WhatsApp or email. |
conversation.escalated | The conversation now needs a person: the AI handed it off, could not answer, or a teammate moved it to the human queue. |
conversation.assigned | The conversation was assigned to a team member (conversation.assignedTo) or unassigned (null). |
conversation.resolved | Resolved by a person, the AI or auto-close. |
message.received | The customer sent a message or an email. |
message.sent | The AI or a team member sent a message or an email (message.author: ai or operator). |
Subscribing to no specific events means all of them. Test conversations from the playground or resolido test never trigger webhooks, and neither do spam or imported old emails.
Payload
Every request is a POST with a JSON body:
{
"type": "message.received",
"timestamp": "2026-10-22T20:25:01.963Z",
"data": {
"conversation": {
"id": "chat_nx79vy9y5r9vxk0xhnwggay2kh8gsbfg",
"channel": "chat",
"status": "open",
"agentId": "k17egncq5nqytjq9b5ect8fzph8graf1",
"assignedTo": null,
"tags": [],
"contact": { "id": "ms7c5bhrb7t8zzqrm1cqpfx8bx8gs1fh", "name": "Jana", "email": "[email protected]" }
},
"message": {
"id": "t17e4pb72q2anc884rycs4ch9h8gsxef",
"author": "customer",
"text": "Where is my order 1042?",
"attachments": 0,
"createdAt": 1792700701963
}
}
}conversation.channelischat,whatsapp,instagramoremail; email conversations also havesubject.conversation.escalatedaddsescalation: { reason, task }– why the AI handed off and what the person should do.conversation.assignedaddspreviousAssignee.- Message text is capped at 10,000 characters. Use the conversation
idwith the API or MCP to read more.
Verifying signatures
Each request carries three headers:
| Header | Value |
|---|---|
webhook-id | Unique event id (evt_…) – the same for retries; use it to ignore duplicates. |
webhook-timestamp | Unix time in seconds. Reject requests older than 5 minutes. |
webhook-signature | v1,<base64 HMAC-SHA256> of {webhook-id}.{webhook-timestamp}.{raw body}, keyed with the base64-decoded part of your secret after whsec_. |
With the standardwebhooks library:
import { Webhook } from "standardwebhooks";
const webhook = new Webhook(process.env.RESOLIDO_WEBHOOK_SECRET!);
export async function POST(request: Request) {
const body = await request.text(); // the raw body, before JSON.parse
const event = webhook.verify(body, Object.fromEntries(request.headers)) as { type: string; data: unknown };
// …handle event.type
return new Response(null, { status: 204 });
}Without a library (Node.js):
import { createHmac, timingSafeEqual } from "node:crypto";
function verify(secret: string, headers: Headers, body: string) {
const id = headers.get("webhook-id")!;
const timestamp = headers.get("webhook-timestamp")!;
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > 300) return false;
const key = Buffer.from(secret.replace(/^whsec_/, ""), "base64");
const expected = createHmac("sha256", key).update(`${id}.${timestamp}.${body}`).digest("base64");
return headers
.get("webhook-signature")!
.split(" ")
.some((value) => {
const signature = value.split(",")[1] ?? "";
return signature.length === expected.length && timingSafeEqual(Buffer.from(signature), Buffer.from(expected));
});
}Delivery and retries
- Respond with any
2xxstatus within 15 seconds. Redirects are not followed. - Failed deliveries are retried after 5 seconds, 5 minutes, 30 minutes, 2, 5, 10 and 10 hours – 8 attempts over about 27 hours.
- Events can arrive out of order and, rarely, more than once. Deduplicate by
webhook-id. - If an endpoint keeps failing for 3 days, Resolido turns it off and shows a warning; switch it back on once it is fixed.
- The delivery log (Deliveries, or
GET /webhooks/{id}/deliveries) keeps 30 days of requests with status codes and payloads. Failed deliveries can be sent again with Retry. - Rotate secret replaces the secret immediately – update your receiver at the same time.
Endpoints must be public https URLs on port 443. IP addresses, localhost and internal hosts are refused. For local development use a tunnel such as ngrok or cloudflared.