Webhooks
OyeChats POSTs a signed JSON payload to your endpoint when something happens. This page covers setup, signature verification and retry behaviour.
Setting one up
Create the endpoint on your side
It must accept
POSTwith a JSON body on a publicly resolvable host, and return a 2xx status quickly. Private, internal and loopback addresses are rejected at registration. Use anhttps://URL: deliveries carry lead names, emails and phone numbers, and a plainhttp://endpoint sends those in clear text.Register it
Go to Workspace → Integrations → Webhook endpoints, pick the chatbot, paste the URL, and select which events it should receive.
Store the signing secret
A secret is generated when you create the endpoint and shown once in full. Copy it then. Afterwards it is masked.
Send a test
Use the Test button and check the delivery log. Then verify the signature in your handler before you rely on the data.
The payload envelope
Every delivery has the same four top-level fields. Only data varies by event.
{
"event": "tier_transition",
"bot_id": 42,
"timestamp": "2026-08-17T14:23:05.412000+00:00",
"data": { }
}| Field | Type | Meaning |
|---|---|---|
event | string | Which event fired. See Webhook events. |
bot_id | integer | The chatbot the event belongs to. |
timestamp | string | ISO 8601, UTC, when the delivery was built. |
data | object | The event-specific body. |
Verifying the signature
Every request carries an X-OyeChats-Signature header: the string sha256= followed by the hex HMAC-SHA256 of the raw request body, keyed with your endpoint's secret.
import hashlib
import hmac
from fastapi import FastAPI, Header, HTTPException, Request
app = FastAPI()
SECRET = "your_endpoint_secret"
@app.post("/oyechats/webhook")
async def receive(request: Request, x_oyechats_signature: str = Header(default="")):
raw = await request.body()
expected = "sha256=" + hmac.new(
SECRET.encode(), raw, hashlib.sha256
).hexdigest()
if not hmac.compare_digest(expected, x_oyechats_signature):
raise HTTPException(status_code=401, detail="bad signature")
payload = await request.json()
handle(payload) # your logic
return {"ok": True} # 2xx, fastimport crypto from 'node:crypto';
import express from 'express';
const app = express();
const SECRET = process.env.OYECHATS_WEBHOOK_SECRET;
// express.raw keeps the body as bytes so the HMAC matches.
app.post(
'/oyechats/webhook',
express.raw({ type: 'application/json' }),
(req, res) => {
const expected =
'sha256=' +
crypto.createHmac('sha256', SECRET).update(req.body).digest('hex');
const received = req.get('X-OyeChats-Signature') ?? '';
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.status(401).send('bad signature');
}
const payload = JSON.parse(req.body.toString('utf8'));
handle(payload);
res.json({ ok: true });
},
);- Always use a constant-time comparison. A plain
===leaks timing information. - Never skip verification because the payload "looks right". The URL is not a secret.
- Treat deliveries as at-least-once and make your handler idempotent. Key on the
session_idindata.
Retries and delivery log
A delivery succeeds on any 2xx. Anything else (a 4xx, a 5xx, a timeout, a connection error) is retried.
| Behaviour | Value |
|---|---|
| Attempts | Up to 5, including the first |
| Backoff between attempts | 30 seconds, 2 minutes, 10 minutes, 1 hour |
| Request timeout | 10 seconds |
| After the last failed attempt | The delivery is marked permanently failed and stops |
Every attempt is logged with its status code and response body, viewable per endpoint in the dashboard. Failed deliveries can be replayed manually once your endpoint is healthy again.
Something here wrong or missing? Tell us and name this page. We will fix it.