Webhooks
A webhook is a call Xatern One makes to your server when something changes. Use it to keep your own systems in step without asking again and again.
Make endpoints in Settings, Developers, or with the webhook endpoints. A company can have up to 10.
Events
| Type | When it is sent |
|---|---|
order.created | A new order is placed. |
order.paid | payment_status changes to paid. |
order.shipped | status changes to shipped. |
order.completed | status changes to completed. |
order.cancelled | status changes to cancelled. |
order.updated | Any other change to status or payment_status. |
product.updated | A catalog product or size is saved. |
ping | You press Send test, or call the test endpoint. |
Body
{
"id": "evt_0123456789abcdef01234567",
"type": "order.paid",
"created": "2026-11-02T08:20:00Z",
"company": "5d7b8a0e-2c43-4c2f-9d41-7a1e0c3f6b92",
"data": {
"order": { "number": "NT-482913", "status": "confirmed", "payment_status": "paid", "...": "full order object" }
}
}For product.updated, data.product is {id, name, status, skus}. For ping, data is {}. The order object is described in Orders.
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | XaternOne-Webhooks/1 |
Xatern-Event | The event type, for example order.paid. |
Xatern-Delivery | The delivery id. Same on every retry of one delivery. |
Xatern-Signature | t=<unix seconds>,v1=<hex> |
Answer fast
Reply with any 2xx status within 10 seconds. Do the slow work after you reply. Xatern One does not follow redirects. A redirect counts as a failure.
The same event can arrive more than once. Use the event id to ignore repeats.
Check the signature
Each endpoint has a signing secret that starts with whsec_. The signature is an HMAC-SHA256, in hex, of the text t.body: the timestamp, a full stop, then the raw request body. The key is the signing secret. Always use the raw body, not a re-encoded copy. Reject a call if the signature does not match, or if t is more than 5 minutes old.
Node
import crypto from "node:crypto";
export function verify(rawBody, header, secret) {
const parts = Object.fromEntries(header.split(",").map(p => p.split("=")));
const t = parts.t, v1 = parts.v1;
if (!t || !v1) return false;
if (Math.abs(Date.now() / 1000 - Number(t)) > 300) return false;
const expected = crypto.createHmac("sha256", secret)
.update(t + "." + rawBody).digest("hex");
const a = Buffer.from(expected), b = Buffer.from(v1);
return a.length === b.length && crypto.timingSafeEqual(a, b);
}PHP
<?php
function verify(string $rawBody, string $header, string $secret): bool {
$parts = [];
foreach (explode(',', $header) as $p) {
[$k, $v] = array_pad(explode('=', $p, 2), 2, '');
$parts[$k] = $v;
}
if (empty($parts['t']) || empty($parts['v1'])) return false;
if (abs(time() - (int)$parts['t']) > 300) return false;
$expected = hash_hmac('sha256', $parts['t'] . '.' . $rawBody, $secret);
return hash_equals($expected, $parts['v1']);
}
$raw = file_get_contents('php://input');
$ok = verify($raw, $_SERVER['HTTP_XATERN_SIGNATURE'] ?? '', getenv('XATERN_WEBHOOK_SECRET'));
http_response_code($ok ? 200 : 400);Python
import hashlib, hmac, time
def verify(raw_body: bytes, header: str, secret: str) -> bool:
parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
t, v1 = parts.get("t"), parts.get("v1")
if not t or not v1:
return False
if abs(time.time() - int(t)) > 300:
return False
signed = t.encode() + b"." + raw_body
expected = hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, v1)Retries
Any answer that is not 2xx, a timeout or a connection error is a failed attempt. Xatern One tries again after these waits, for 7 attempts in all:
| Attempt | Sent |
|---|---|
| 1 | Straight away |
| 2 | 1 minute after attempt 1 |
| 3 | 5 minutes after attempt 2 |
| 4 | 30 minutes after attempt 3 |
| 5 | 2 hours after attempt 4 |
| 6 | 6 hours after attempt 5 |
| 7 | 12 hours after attempt 6 |
After attempt 7 the delivery is marked failed. You can send it again from the delivery log or with the resend call.
Auto-disable
If an endpoint has 15 failed deliveries in a row and none has worked for 3 days, Xatern One turns it off. The endpoint shows disabled_at and disabled_reason. Fix your server, then set enabled to true again.
Roll the secret
If a secret leaks, use roll-secret. The old secret stops working at once. Events are kept for 30 days.
Xatern One API v1. Example data on these pages is made up.