Webhooks Real-time event delivery

Webhooks

ConvoAI pushes real-time event notifications to your server via HTTP POST requests whenever something happens — a new message, a contact update, a handoff request. Register a webhook endpoint once and receive every event automatically.

How It Works

When an event occurs in ConvoAI, the platform sends an HTTP POST request to your registered endpoint URL with a JSON body describing the event. Your server must respond with a 2xx status within 5 seconds to acknowledge receipt.

ConvoAI Platform
HTTP POST + HMAC Signature
Your Server
2xx Response

Registering a Webhook

Register a webhook endpoint for a specific agent. Each agent can have multiple webhook registrations, each listening to different event subsets.

POST /v1/agents/{agent_id}/webhooks/ Register webhook

Request Body

ParameterTypeRequiredDescription
urlstringrequiredHTTPS endpoint URL to receive events. Must be publicly reachable.
eventsarrayrequiredList of event types to subscribe to. Use ["*"] to receive all events.
secretstringoptionalA secret string used to sign payloads (HMAC-SHA256). Auto-generated if omitted.
descriptionstringoptionalHuman-readable label for this webhook registration.
activebooleanoptionalWhether to immediately activate delivery. Default: true.
Register a webhook — curl
curl -X POST https://app.convoai.cloud/api/v1/agents/ag_01J8X…/webhooks/ \ -H "Authorization: Bearer ck_live_…" \ -H "Content-Type: application/json" \ -d '{ "url": "https://your-server.com/webhooks/convoai", "events": ["message.received", "conversation.created", "handoff.requested"], "description": "Production webhook", "secret": "your-webhook-secret-min-32-chars" }'
Response 201 Created
{ "id": "wh_01J9K2P…", "agent_id": "ag_01J8X…", "url": "https://your-server.com/webhooks/convoai", "events": ["message.received", "conversation.created", "handoff.requested"], "secret": "your-webhook-secret-min-32-chars", "description": "Production webhook", "active": true, "created_at": "2026-05-11T14:30:00Z" }

Webhook Management Endpoints

GET/v1/agents/{agent_id}/webhooks/List webhooks
GET/v1/agents/{agent_id}/webhooks/{webhook_id}/Get webhook
PATCH/v1/agents/{agent_id}/webhooks/{webhook_id}/Update webhook
DELETE/v1/agents/{agent_id}/webhooks/{webhook_id}/Delete webhook
POST/v1/agents/{agent_id}/webhooks/{webhook_id}/test/Send test event

Event Types

Every event payload shares a common envelope. The data object varies by event type.

Common event envelope
{ "id": "evt_01J9K…", // Unique event ID — safe to deduplicate on "type": "message.received", // Event type string "created_at": "2026-05-11T14:35:22Z", "agent_id": "ag_01J8X…", "livemode": true, // false when sent from /test/ endpoint "data": { /* event-specific payload */ } }
message.received Inbound message from a customer
Fired immediately on receipt
"data": { "message_id": "msg_01J9…", "conversation_id": "conv_01J9…", "contact_id": "con_01J9…", "direction": "inbound", "type": "text", // text | image | audio | video | document | sticker | location | reaction "text": "Hi, I need help with my order", "media_url": null, "timestamp": "2026-05-11T14:35:22Z", "whatsapp_message_id": "wamid.HBgL…" }
message.sent Outbound message dispatched
Fired on successful API dispatch
"data": { "message_id": "msg_01J9…", "conversation_id": "conv_01J9…", "direction": "outbound", "type": "text", "text": "Of course! Could you share your order number?", "source": "ai_agent", // ai_agent | human_agent | api | template_campaign "timestamp": "2026-05-11T14:35:24Z" }
message.delivered message.read WhatsApp delivery status update
"data": { "message_id": "msg_01J9…", "conversation_id": "conv_01J9…", "status": "delivered", // sent | delivered | read | failed "timestamp": "2026-05-11T14:35:26Z" }
conversation.created New conversation opened
"data": { "conversation_id": "conv_01J9…", "contact_id": "con_01J9…", "contact_phone": "+15551234567", "contact_name": "Maria Garcia", "channel": "whatsapp", "status": "open", "created_at": "2026-05-11T14:35:22Z" }
conversation.closed Conversation marked resolved
"data": { "conversation_id": "conv_01J9…", "closed_by": "ai_agent", // ai_agent | human_agent | api | auto_timeout "duration_seconds": 184, "message_count": 7, "resolution": "resolved", // resolved | unresolved | abandoned "closed_at": "2026-05-11T14:38:26Z" }
contact.created contact.updated CRM contact changes
"data": { "contact_id": "con_01J9…", "phone": "+15551234567", "name": "Maria Garcia", "email": "maria@example.com", "stage": "qualified", // new | contacted | qualified | customer | churned "tags": ["vip", "spanish"], "custom_fields": { "plan": "enterprise" }, "changed_fields": ["stage", "tags"] // only present on contact.updated }
handoff.requested AI agent escalating to human
Action required — assign a human agent
"data": { "conversation_id": "conv_01J9…", "contact_id": "con_01J9…", "reason": "customer_requested", // customer_requested | ai_confidence_low | rule_triggered "summary": "Customer asking about a refund for order #4821. Frustrated tone.", "priority": "high", // low | normal | high | urgent "requested_at": "2026-05-11T14:36:10Z" }
agent.limit_reached Plan message limit reached
"data": { "agent_id": "ag_01J8X…", "limit_type": "monthly_messages", "current_usage": 1000, "limit": 1000, "reset_at": "2026-06-01T00:00:00Z" }

All Event Types

EventDescriptionFrequency
message.receivedInbound message from customerEvery inbound message
message.sentOutbound message dispatchedEvery outbound message
message.deliveredWhatsApp delivery confirmedPer message status update
message.readCustomer opened the messagePer read receipt
message.failedDelivery failure from WhatsAppOn failure
conversation.createdNew conversation startedFirst contact message
conversation.closedConversation resolvedOn closure
contact.createdNew CRM contact createdFirst contact seen
contact.updatedContact fields changedOn CRM update
handoff.requestedAI escalating to humanOn handoff trigger
agent.limit_reachedPlan usage limit hitOnce per billing period
agent.updatedAgent configuration changedOn agent settings save

Signature Verification

Every webhook delivery includes an X-ConvoAI-Signature header. This is a HMAC-SHA256 hex digest of the raw request body, keyed with your webhook secret.

Always verify the signature before processing a webhook payload. Reject requests where the signature is missing or invalid.
X-ConvoAI-Signature: sha256=7b3f1a8c2e9d4f0b6a5c8e1f2d4a7b9c3e6f0a2b5d8e1c4f7a0b3d6e9f2c5a
Verify signature — Python
import hmac import hashlib from django.http import HttpResponse, HttpResponseForbidden WEBHOOK_SECRET = "your-webhook-secret" def convoai_webhook(request): # 1. Get the signature from the header sig_header = request.headers.get("X-ConvoAI-Signature", "") if not sig_header.startswith("sha256="): return HttpResponseForbidden() received_sig = sig_header[7:] # strip "sha256=" prefix # 2. Compute expected signature from raw body expected_sig = hmac.new( WEBHOOK_SECRET.encode("utf-8"), request.body, # use the RAW bytes, not decoded string hashlib.sha256 ).hexdigest() # 3. Compare — use compare_digest to prevent timing attacks if not hmac.compare_digest(expected_sig, received_sig): return HttpResponseForbidden() # 4. Safe to parse and process import json payload = json.loads(request.body) handle_event(payload) return HttpResponse(status=200)
Verify signature — Node.js (Express)
const crypto = require('crypto'); const express = require('express'); const app = express(); const WEBHOOK_SECRET = process.env.CONVOAI_WEBHOOK_SECRET; // Use express.raw() to preserve the raw body for signature checking app.post('/webhooks/convoai', express.raw({ type: 'application/json' }), (req, res) => { const sigHeader = req.headers['x-convoai-signature'] || ''; if (!sigHeader.startsWith('sha256=')) { return res.status(403).send('Missing signature'); } const receivedSig = sigHeader.slice(7); const expectedSig = crypto .createHmac('sha256', WEBHOOK_SECRET) .update(req.body) // req.body is a Buffer here .digest('hex'); const valid = crypto.timingSafeEqual( Buffer.from(expectedSig, 'hex'), Buffer.from(receivedSig, 'hex') ); if (!valid) return res.status(403).send('Invalid signature'); const payload = JSON.parse(req.body.toString()); handleEvent(payload); res.sendStatus(200); });

Retry Policy

If your endpoint returns a non-2xx response or times out (5s), ConvoAI retries delivery with exponential backoff. Each retry carries the same id in the envelope so you can deduplicate.

AttemptDelay after previous failureCumulative time
1 (original)0s
230 seconds~30s
35 minutes~5m
430 minutes~35m
5 (final)2 hours~2h 35m

After 5 failed attempts the event is marked failed and delivery stops. You can view delivery logs and manually replay events from the ConvoAI dashboard under Settings → Webhooks.

Deduplication tip: Always process webhook events idempotently. Store the event id and skip processing if you've seen it before. This protects against duplicate deliveries during retries or any transient failures on our side.

Request Headers

HeaderDescription
Content-TypeAlways application/json; charset=utf-8
X-ConvoAI-SignatureHMAC-SHA256 signature: sha256={hex_digest}
X-ConvoAI-EventThe event type string, e.g. message.received
X-ConvoAI-DeliveryUnique delivery attempt ID — different from event ID
User-AgentConvoAI-Webhook/1.0

Testing Webhooks

Use the test endpoint to send a synthetic event to your registered URL without needing a real conversation.

POST /v1/agents/{agent_id}/webhooks/{webhook_id}/test/ Send test event
Send test event — curl
curl -X POST https://app.convoai.cloud/api/v1/agents/ag_01J8X…/webhooks/wh_01J9K…/test/ \ -H "Authorization: Bearer ck_live_…" \ -H "Content-Type: application/json" \ -d '{"event_type": "message.received"}'

Test events have "livemode": false in the envelope. IDs use a test_ prefix. For local development, use a tunnel tool like ngrok or Tunnelmole to expose your local server.