OyeChats
FeaturesSolutionsIntegrationsPricingDocsBlogContact us

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

  1. Create the endpoint on your side

    It must accept POST with a JSON body on a publicly resolvable host, and return a 2xx status quickly. Private, internal and loopback addresses are rejected at registration. Use an https:// URL: deliveries carry lead names, emails and phone numbers, and a plain http:// endpoint sends those in clear text.

  2. Register it

    Go to Workspace → Integrations → Webhook endpoints, pick the chatbot, paste the URL, and select which events it should receive.

  3. 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.

  4. 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.

Endpoints are per chatbot. Each endpoint belongs to one chatbot, so a workspace with several chatbots can send each one's events to a different destination. Webhooks are included from Standard upwards, and only owners and admins can create or re-point them.

The payload envelope

Every delivery has the same four top-level fields. Only data varies by event.

JSON
{
  "event": "tier_transition",
  "bot_id": 42,
  "timestamp": "2026-08-17T14:23:05.412000+00:00",
  "data": { }
}
FieldTypeMeaning
eventstringWhich event fired. See Webhook events.
bot_idintegerThe chatbot the event belongs to.
timestampstringISO 8601, UTC, when the delivery was built.
dataobjectThe 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.

Hash the raw bytes. Compute the HMAC over the body exactly as received, before any JSON parse or re-serialise. Re-serialising changes whitespace and key order, and the signature will not match.
Python. FastAPI
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, fast
Node.js. Express
import 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_id in data.

Retries and delivery log

A delivery succeeds on any 2xx. Anything else (a 4xx, a 5xx, a timeout, a connection error) is retried.

BehaviourValue
AttemptsUp to 5, including the first
Backoff between attempts30 seconds, 2 minutes, 10 minutes, 1 hour
Request timeout10 seconds
After the last failed attemptThe 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.

Return 2xx fast, work afterwards. Do the slow part of your handler after responding. Enqueue a job, then reply. A handler that takes longer than 10 seconds is a failed delivery even if it eventually succeeds.

Something here wrong or missing? Tell us and name this page. We will fix it.