Custom webhooks let Cookaborough send order updates to your system automatically.
Available events

Delivery format
Cookaborough sends each webhook as an HTTP POST request with a JSON body.
POST <https://your-domain.com/webhooks/cookaborough>
Content-Type: application/json
X-CB-Signature: t=1783312496,v1=<signature>
The request body is an envelope:
{
"event": "order.created",
"datetime": 1783312496,
"payload": {}
}

The complete order data shape is documented in the Order payload reference.
All money amounts in the order payload are integer cents. For example, 26800 means $268.00.
Signature verification
Every request includes a timestamp and HMAC signature:
X-CB-Signature: t=<timestamp>,v1=<signature>
Cookaborough signs the timestamp, a literal period, and the exact raw request body using your webhook signing secret. The signature is a lowercase hexadecimal HMAC SHA-256 digest.
signed_payload = timestamp + "." + raw_request_body
signature = HMAC_SHA256(signed_payload, webhook_secret)
To verify a request:
- Read the unmodified raw request body.
- Parse
tandv1fromX-CB-Signature. - Confirm that header
texactly matches bodydatetime. - Calculate HMAC SHA-256 over
t + "." + raw_request_bodyusing your signing secret. - Compare your calculated signature with
v1using a timing-safe comparison.
Always verify the signature before processing the payload. We also recommend rejecting timestamps outside a short tolerance, such as five minutes, to reduce replay risk.
PHP example
$secret = 'your-webhook-secret';
$body = file_get_contents('php://input');
$signatureHeader = $_SERVER['HTTP_X_CB_SIGNATURE'] ?? '';
$parts = [];
foreach (explode(',', $signatureHeader) as $part) {
[$key, $value] = array_pad(explode('=', $part, 2), 2, null);
$parts[$key] = $value;
}
$timestamp = isset($parts['t']) ? (int) $parts['t'] : null;
$receivedSignature = $parts['v1'] ?? null;
$webhook = json_decode($body, true, flags: JSON_THROW_ON_ERROR);
if (
$timestamp === null
|| $receivedSignature === null
|| ($webhook['datetime'] ?? null) !== $timestamp
) {
http_response_code(401);
exit('Invalid signature');
}
$expectedSignature = hash_hmac('sha256', $timestamp . '.' . $body, $secret);
if (!hash_equals($expectedSignature, $receivedSignature)) {
http_response_code(401);
exit('Invalid signature');
}
Node.js example
import crypto from 'crypto';
const secret = 'your-webhook-secret';
const rawBody = req.rawBody;
const signatureHeader = req.get('X-CB-Signature') ?? '';
const parts = Object.fromEntries(
signatureHeader.split(',').map((part) => part.split('=', 2)),
);
const timestamp = Number(parts.t);
const webhook = JSON.parse(rawBody.toString('utf8'));
if (!Number.isInteger(timestamp) || webhook.datetime !== timestamp || !parts.v1) {
return res.status(401).send('Invalid signature');
}
const expectedSignature = crypto
.createHmac('sha256', secret)
.update(`${timestamp}.${rawBody.toString('utf8')}`)
.digest('hex');
const expected = Buffer.from(expectedSignature, 'hex');
const received = Buffer.from(parts.v1, 'hex');
if (expected.length !== received.length || !crypto.timingSafeEqual(expected, received)) {
return res.status(401).send('Invalid signature');
}
Your framework must provide the raw request body. Parsing and re-encoding JSON before verification changes the signed bytes and causes verification to fail.
Signing secret rotation
Your signing secret can be refreshed in Cookaborough. Future deliveries use the new secret immediately, so update your receiving system when rotating it.
Retry behaviour
Cookaborough retries deliveries when it cannot connect, the request times out, or your endpoint returns an HTTP 4xx or 5xx response.

Return a successful response quickly after accepting the webhook, then process longer-running work asynchronously. Because deliveries can be retried, handlers should be idempotent. Use the order gid from the linked payload reference as the stable order identifier.
URL requirements
Webhook URLs must use HTTPS and be publicly reachable. URLs targeting localhost, private or reserved IP addresses, or hosts that cannot be resolved are not accepted.
