On this page
Webhook Events#
Receive real-time notifications when chat events occur — messages, sessions, and visitor identity updates — delivered to your HTTPS endpoints via HTTP POST with HMAC-SHA256 signatures.
Not to be confused with the Conversations API callback. Webhook events are push notifications for activity on your chatbot widgets. The callback URL on API keys is for receiving async AI responses from the Conversations API.
Setting Up Webhooks#
Create webhook endpoints in Console → Settings → Developer → Webhook Endpoints. Each endpoint requires:
- An HTTPS URL that accepts POST requests
- One or more event types to subscribe to
- An optional widget scope to filter events from a specific widget
A signing secret is automatically generated when you create an endpoint. Store it securely — you will need it to verify webhook signatures.
Event Types#
| Event | Trigger |
|---|---|
message.completed | AI response sent successfully |
message.failed | Message processing error |
session.created | New or resumed chat session |
session.ended | Session resolved or expired |
session.rated | Visitor rated a session |
visitor.identified | Visitor identity fields updated |
knowledge.upsert.completed | Integration API upsert batch finished |
knowledge.reconcile.completed | Integration API feed reconcile finished |
Payload Format#
Every webhook delivery is an HTTP POST with a JSON body:
{
"eventId": "01HX7V...",
"event": "message.completed",
"widgetId": "6650a1b2c3d4e5f6a7b8c9d0",
"sessionId": "sess_abc123",
"timestamp": "2025-01-15T10:30:00.000Z",
"data": {
"messageId": "msg_xyz789",
"content": "Here are some products under $50...",
"products": [
{ "id": "prod_1", "name": "Wireless Earbuds", "score": 0.95 }
],
"sessionEnded": false,
"usage": { "inputTokens": 1234, "outputTokens": 567 }
}
}Event Data#
Each event type includes different fields in the data object:
message.completed#
{
"messageId": "msg_abc123",
"content": "Here are some products...",
"products": [{ "id": "prod_1", "name": "Wireless Earbuds", "score": 0.95 }],
"sessionEnded": false,
"credits": {
"chat": 8,
"retrieval": 4,
"orchestration": 2,
"embedding": 1,
"rerank": 0,
"plugin": 0,
"crawler": 0
},
"usage": { "inputTokens": 1234, "outputTokens": 567 }
}message.failed#
{
"error": {
"code": "CREDITS_EXHAUSTED",
"message": "Account credits have been exhausted"
}
}session.created#
{
"status": "active",
"createdAt": "2025-01-15T10:30:00.000Z",
"visitorId": "vis_abc123",
"resumed": false
}session.ended#
{
"status": "resolved",
"resolvedAt": "2025-01-15T11:00:00.000Z",
"resolution": "manual"
}session.rated#
{
"rating": "positive"
}visitor.identified#
{
"visitorId": "vis_abc123",
"fields": ["name", "email"],
"context": {
"name": "John",
"email": "john@example.com"
}
}knowledge.upsert.completed#
{
"knowledgeBaseId": "6650a1b2c3d4e5f6a7b8c9d0",
"knowledgeBaseType": "product",
"operation": "upsert",
"matchBy": "sku",
"created": 12,
"updated": 38,
"unchanged": 48,
"failed": 2,
"requestId": "req_01HX7V..."
}knowledge.reconcile.completed#
{
"knowledgeBaseId": "6650a1b2c3d4e5f6a7b8c9d0",
"knowledgeBaseType": "product",
"operation": "reconcile",
"matchBy": "sku",
"deleted": 5,
"unchanged": 995,
"requestId": "req_01HX8A..."
}Verifying Signatures#
Every delivery includes three headers for signature verification:
| Header | Description |
|---|---|
X-Gydr-Signature | HMAC-SHA256 signature of {timestamp}.{body} |
X-Gydr-Timestamp | Unix epoch seconds when the delivery was signed |
X-Gydr-Event | Event type (e.g., message.completed) |
To verify a webhook, compute the HMAC-SHA256 of the timestamp and raw request body bytes using your endpoint's signing secret, then compare it to the signature header. Always verify the raw bytes — re-serialising a parsed JSON object risks key-order or whitespace differences that break the comparison:
import crypto from "crypto"
// Mount this route with express.raw() so req.body is the unmodified Buffer:
// app.post("/webhook", express.raw({ type: "application/json" }), handler)
function verifyWebhook(req, secret) {
const signature = req.headers["x-gydr-signature"]
const timestamp = req.headers["x-gydr-timestamp"]
// req.body is a Buffer when using express.raw()
const rawBody = Buffer.isBuffer(req.body)
? req.body.toString("utf8")
: req.body
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}Tip: Always use a timing-safe comparison function to prevent timing attacks. In Node.js, use crypto.timingSafeEqual(). In Python, use hmac.compare_digest().
Delivery Behavior#
- Deliveries timeout after 10 seconds — your endpoint must respond within this window
- Failed deliveries are attempted a total of 3 times at 0 s, 1 s, and 5 s intervals
- A fresh HMAC signature is generated for each retry attempt
- Your endpoint must return a 2xx status code to acknowledge receipt
- After all retries fail, the event is sent to a dead letter queue for monitoring
Security Requirements#
- Webhook URLs must use HTTPS — HTTP is not accepted
- Private IP addresses and localhost are blocked for SSRF protection
- Redirects are blocked — your URL must respond directly
- Rotate your signing secret periodically via the Console
Widget Scoping#
By default, a webhook endpoint receives events from all widgets in your account. To limit an endpoint to events from a specific widget, select the widget when creating or editing the endpoint. You can create multiple endpoints with different widget scopes to route events to different systems.
Best Practices#
- Respond quickly — return a 200 status immediately and process the event asynchronously
- Handle duplicates — use the
eventIdfield for idempotency - Verify signatures — always validate the HMAC signature before processing
- Check timestamps — reject events with timestamps more than 5 minutes old to prevent replay attacks
- Log failures — monitor your endpoint for errors to catch issues early