Webhooks
Zitena reports events such as requests, calls and outbound calls to your URL with signed POST requests. Endpoints are managed on Settings → Webhooks (plan: Webhooks; writing needs admin or owner).
Adding an endpoint
Section titled “Adding an endpoint”- New endpoint: URL (
https://only; addresses leading to an internal network or private IP are rejected), description and the events to listen to. - After saving, the secret (
whsec_…) is shown once. Keep it on your server; change it with Rotate secret. - Use Send test to send the
testevent right away and see the result.
Each endpoint shows the status of its last delivery and the number of failed deliveries in the last 24 hours; the Deliveries list shows each delivery’s status, attempts, response code and body, and you can retry a failed delivery by hand.
Request
Section titled “Request”POST https://your-endpoint.example.com/zitenaContent-Type: application/jsonUser-Agent: Zitena-Webhook/1X-Zitena-Event: request.createdX-Zitena-Delivery: 0199a1b2-… (delivery id; same on retries)X-Zitena-Signature: t=1790000000,v1=5f2c…
{"id": "0199a1b2-…", "event": "request.created", "created_at": "2026-09-24T10:15:00+00:00", "data": {…}}Events
Section titled “Events”| Event | When | data |
|---|---|---|
request.created | A request was opened during a call | Request: id, number, call_id, kind, status, order_number, order_status, customer_name, email, phone, details, history, created_at, updated_at |
request.status_changed | A request’s status changed | Request + previous_status |
talep.acildi | A request was opened (lean, no contact data) | id, talep_no, tur, durum, siparis_no, call_id, created_at |
order.applied | A request was applied on the site automatically (address, cancellation) | request_id, request_number, kind, order_number, action, site, call_id, applied_at |
call.completed | A call ended (completed, busy, no answer, failed…) | id, agent_id, channel, status, outcome, phone, callee_name, is_test, summary, created_at, answered_at, ended_at, duration_s, requests |
cagri.etiketlendi | A call was tagged or its tags were corrected by hand | id, agent_id, channel, is_test, created_at, duration_s, summary, cozum, kaynak (otomatik/elle), konu_duzeltme, konular, tagged_at |
arama.planlandi | An outbound call request was scheduled | Outbound call request (below) |
arama.reddedildi | A request was rejected by a pre-check | Outbound call request (sebep) |
arama.tamamlandi | A request finished: tamamlandi, basarisiz or iptal | Outbound call request |
arama.sonuc | The call’s outcome code was recorded | Outbound call request (sonuc_kodu, sonuc) |
kupon.olusturuldu | The assistant created a coupon during a call | id, kod, tip, deger, gecerlilik, arama_id, call_id, siparis_no, created_at (no email) |
test | Send test in the console | message, endpoint_id |
Outbound call request payload: id, amac, tur, iys_kontrolu, kaynak, durum, sebep, planlanan, deneme, siparis_no, sepet_id, musteri (ad, eposta masked, telefon_maskeli), tetikleyen (eposta masked), call_id, sonuc_kodu, sonuc, parent_id, created_at, updated_at.
Example payloads
Section titled “Example payloads”{ "id": "0199a1b2-7c3d-7e41-9f00-2b1c3d4e5f60", "event": "request.created", "created_at": "2026-09-24T10:15:02+00:00", "data": { "id": "0199a1b2-6a10-7b22-8c33-1d2e3f405162", "number": "TLP-000123", "call_id": "0199a1b0-1111-7222-8333-944455566677", "kind": "adres_degisikligi", "status": "acik", "order_number": "98765", "order_status": "processing", "customer_name": "Ayşe Yılmaz", "phone": "+90 532 *** ** 67", "details": {"yeni_adres": "…"}, "history": [], "created_at": "2026-09-24T10:15:01+00:00", "updated_at": "2026-09-24T10:15:01+00:00" }}{ "id": "0199a1c0-…", "event": "call.completed", "created_at": "2026-09-24T10:18:40+00:00", "data": { "id": "0199a1b0-1111-7222-8333-944455566677", "agent_id": "0199a0aa-…", "channel": "phone_in", "status": "completed", "outcome": "user_hangup", "phone": "+90 532 *** ** 67", "callee_name": null, "is_test": false, "summary": null, "created_at": "2026-09-24T10:14:02+00:00", "answered_at": "2026-09-24T10:14:03+00:00", "ended_at": "2026-09-24T10:18:39+00:00", "duration_s": 276, "requests": ["TLP-000123"] }}The summary may not be ready when the call ends; once tagging finishes, the cagri.etiketlendi event brings the summary, resolution and topics.
{ "id": "0199a2d0-…", "event": "arama.sonuc", "created_at": "2026-09-25T07:06:12+00:00", "data": { "id": "0199a2c0-…", "amac": "sikayet", "tur": "hizmet", "iys_kontrolu": "gerekmez", "kaynak": "api", "durum": "tamamlandi", "sebep": null, "planlanan": "2026-09-25T07:00:00+00:00", "deneme": 1, "siparis_no": "560991", "sepet_id": null, "musteri": {"ad": "Ayşe Yılmaz", "eposta": "a***@example.com", "telefon_maskeli": "+90 532 *** ** 67"}, "tetikleyen": {"ad": "Can Demir", "eposta": "c***@example.com"}, "call_id": "0199a2c5-…", "sonuc_kodu": "memnun", "sonuc": {"kaynak": "asistan", "memnuniyet": 5, "not": "…"}, "parent_id": null, "created_at": "2026-09-24T12:00:00+00:00", "updated_at": "2026-09-25T07:06:12+00:00" }}{ "id": "0199a1c9-…", "event": "cagri.etiketlendi", "created_at": "2026-09-24T10:19:05+00:00", "data": { "id": "0199a1b0-1111-7222-8333-944455566677", "agent_id": "0199a0aa-…", "channel": "phone_in", "is_test": false, "created_at": "2026-09-24T10:14:02+00:00", "duration_s": 276, "summary": "Müşteri teslimat adresini değiştirmek istedi. Talep açıldı.", "cozum": "talep_acildi", "kaynak": "otomatik", "konu_duzeltme": 0, "konular": [{"id": "0199…", "ad": "Adres değişikliği", "grup": "Sipariş", "kaynak": "otomatik"}], "tagged_at": "2026-09-24T10:19:04+00:00" }}Verifying the signature
Section titled “Verifying the signature”The X-Zitena-Signature header has the form t=<unix seconds>,v1=<signature>. The signature is the hex value of HMAC-SHA256(secret, "<t>.<raw body>"). To verify:
- Split the header on
,; taketand one or morev1values (after a secret rotation there can be severalv1; any match is valid). - Check that
tis at most 300 seconds from now (replay protection). - Compute the expected signature over the raw body (don’t parse and re-serialise the JSON) and compare in constant time.
import crypto from 'node:crypto';import express from 'express';
const SECRET = process.env.RZ_WEBHOOK_SECRET; // whsec_xxxconst app = express();
function verify(header, rawBody) { if (!header) return false; let t = null; const sigs = []; for (const part of header.split(',')) { const [k, v] = part.trim().split('=', 2); if (k === 't' && /^\d+$/.test(v)) t = Number(v); else if (k === 'v1' && v) sigs.push(v); } if (t === null || !sigs.length || Math.abs(Date.now() / 1000 - t) > 300) return false; const expected = crypto.createHmac('sha256', SECRET).update(`${t}.`).update(rawBody).digest('hex'); return sigs.some((s) => s.length === expected.length && crypto.timingSafeEqual(Buffer.from(s), Buffer.from(expected)));}
app.post('/zitena', express.raw({ type: 'application/json' }), (req, res) => { if (!verify(req.get('X-Zitena-Signature'), req.body)) return res.sendStatus(401); const evt = JSON.parse(req.body.toString('utf8')); // The same delivery can arrive more than once: store evt.id (X-Zitena-Delivery) and skip repeats. res.sendStatus(204); // answer fast, queue the work});<?php$secret = getenv('RZ_WEBHOOK_SECRET'); // whsec_xxx$raw = file_get_contents('php://input');$header = $_SERVER['HTTP_X_ZITENA_SIGNATURE'] ?? '';
function rz_verify(string $secret, string $header, string $raw): bool { $t = null; $sigs = []; foreach (explode(',', $header) as $part) { [$k, $v] = array_pad(explode('=', trim($part), 2), 2, ''); if ($k === 't' && ctype_digit($v)) { $t = (int) $v; } elseif ($k === 'v1' && $v !== '') { $sigs[] = $v; } } if ($t === null || !$sigs || abs(time() - $t) > 300) { return false; } $expected = hash_hmac('sha256', $t . '.' . $raw, $secret); foreach ($sigs as $s) { if (hash_equals($expected, $s)) { return true; } } return false;}
if (!rz_verify($secret, $header, $raw)) { http_response_code(401); exit; }$event = json_decode($raw, true);http_response_code(204);import hashlib, hmac, time
def verify(secret: str, header: str | None, body: bytes, tolerance: int = 300) -> bool: if not header: return False t, sigs = None, [] for part in header.split(","): k, _, v = part.strip().partition("=") if k == "t" and v.isdigit(): t = int(v) elif k == "v1" and v: sigs.append(v) if t is None or not sigs or abs(time.time() - t) > tolerance: return False expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest() return any(hmac.compare_digest(expected, s) for s in sigs)Delivery and retries
Section titled “Delivery and retries”- A 2xx response counts as delivered. Timeout 10 seconds; redirects are not followed (3xx fails).
- A failed delivery is retried after 1 min, 5 min, 30 min, 2 h and 12 h (6 attempts in total), then marked failed. You can retry by hand from the console.
- Pending deliveries older than 24 hours are not sent (expired): a late event notification is meaningless and carries personal data.
- The address is resolved again on every send; requests to private/local IPs are never made and count as permanent errors (not retried).
- At-least-once delivery: the same delivery can arrive more than once. Store
X-Zitena-Delivery(the body’sid) and skip repeats. Order is not guaranteed; use time fields such asupdated_at. - Logs and delivery errors never contain the body, the secret or the response content.
© 2026 Erusoft