On this page
Getting Started with Webhooks#
Webhooks let Gydr push event notifications to your server the moment something happens on your chatbot. Instead of polling the API, your endpoint receives an HTTP POST for each event — visitor messages, session starts and ends, satisfaction ratings, visitor identity updates, and knowledge sync completions.
This guide walks you through building a working receiver from scratch: exposing an endpoint, registering it in the Console, verifying the signature, and returning a fast response.
What webhooks give you#
Every subscribed event arrives as a signed HTTP POST within seconds of the activity. Common uses:
- Messages — log AI responses or pipe transcripts into your CRM
- Sessions — open support tickets when a session starts, close them when it ends, record satisfaction ratings
- Visitors — sync collected identity fields (name, email, custom context) to your user database
- Knowledge syncs — confirm that an Integration API upsert or reconcile completed and check for failures
Step 1: Expose an HTTPS endpoint#
Your endpoint must accept POST requests over HTTPS. Gydr rejects HTTP URLs and will not follow redirects — your URL must respond directly.
Signature verification requires the raw, unmodified request body bytes. If your framework parses JSON before your handler runs, you will not have access to the original bytes and the HMAC will not match. Use express.raw() (not express.json()) on this route:
import express from "express"
import crypto from "crypto"
const app = express()
// Mount with express.raw() — this gives req.body as a Buffer.
// Do NOT use express.json() here; re-serialising the parsed object
// will break the HMAC comparison.
app.post(
"/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
// Acknowledge receipt immediately — process async.
res.sendStatus(200)
const secret = process.env.GYDR_WEBHOOK_SECRET
if (!verifySignature(req, secret)) {
console.warn("Rejected webhook: invalid signature")
return
}
const payload = JSON.parse(req.body.toString("utf8"))
handleEvent(payload.event, payload).catch(console.error)
}
)
app.listen(3000)Tip: Deploy your receiver behind a stable public URL before registering it in the Console. Tools like ngrok or localtunnel can expose a local server for testing.
Step 2: Register your endpoint in the Console#
Go to Console → Settings → Developer → Webhook Endpoints and click Add Endpoint. Provide:
- Your HTTPS URL
- The event types you want to receive — you can select one or more
- Optionally, a widget to scope events to a single chatbot rather than your whole account
After saving, the Console displays your endpoint's signing secret. Copy it now — it is shown only once. Store it in an environment variable such as GYDR_WEBHOOK_SECRET. If you lose it, you can rotate the secret from the endpoint settings at any time.
Step 3: Verify the signature#
Every delivery includes three headers:
| Header | Description |
|---|---|
X-Gydr-Signature | HMAC-SHA256 hex digest of {timestamp}.{rawBody} signed with your endpoint's secret |
X-Gydr-Timestamp | Unix epoch seconds when the delivery was signed |
X-Gydr-Event | Event type, e.g. message.completed |
To verify, compute the HMAC-SHA256 of the signed payload string and compare it to the signature header using a timing-safe comparison. Also reject any delivery whose timestamp is more than five minutes old — this prevents replay attacks.
import crypto from "crypto"
// req.body must be a Buffer (use express.raw() — see Step 1)
function verifySignature(req, secret) {
const signature = req.headers["x-gydr-signature"]
const timestamp = req.headers["x-gydr-timestamp"]
// Reject stale deliveries (> 5 minutes old)
const age = Math.floor(Date.now() / 1000) - Number(timestamp)
if (age > 300) return false
const rawBody = Buffer.isBuffer(req.body)
? req.body.toString("utf8")
: req.body
const expected = crypto
.createHmac("sha256", secret)
.update(`${timestamp}.${rawBody}`)
.digest("hex")
// Use timingSafeEqual to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
)
}Always drop deliveries that fail signature verification without processing them. Never skip verification in production — it is the only guarantee that the request came from Gydr.
Step 4: Respond 2xx quickly#
Your endpoint must return a 2xx status code within 10 seconds. Any non-2xx response, or no response within the timeout, counts as a failure.
Failed deliveries are retried a total of 3 times — the initial attempt plus two retries — at approximately 0 s, 1 s, and 5 s intervals. A fresh HMAC signature is computed for each attempt, so the timestamp in the headers changes per retry.
Send the 200 immediately and process the event in the background. If your handler performs database writes, external API calls, or other slow work, do not block the HTTP response on it:
app.post(
"/webhook",
express.raw({ type: "application/json" }),
(req, res) => {
// Respond first — then work.
res.sendStatus(200)
if (!verifySignature(req, process.env.GYDR_WEBHOOK_SECRET)) return
const payload = JSON.parse(req.body.toString("utf8"))
handleEvent(payload.event, payload).catch(console.error)
}
)
async function handleEvent(event, payload) {
switch (event) {
case "message.completed":
await logMessageToDatabase(payload)
break
case "session.ended":
await closeTicket(payload.sessionId)
break
case "visitor.identified":
await syncVisitorTocrm(payload.data)
break
// handle other events ...
}
}Use the eventId field on each payload for idempotency — store it and skip processing if you have already handled that ID. Retries carry the same eventId, so a duplicate-check prevents double-processing on transient failures.
Next steps#
- Events & Payloads — see the full event catalog and the exact shape of each event's
dataobject in the Events & Payloads reference - Error codes — if your endpoint returns error responses, see /docs/api/errors for the full list of Gydr error codes and what they mean