Need some help using Cookaborough?
Webhooks
How to use and setup Cookaborough Webhooks

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:

  1. Read the unmodified raw request body.
  2. Parse t and v1 from X-CB-Signature.
  3. Confirm that header t exactly matches body datetime.
  4. Calculate HMAC SHA-256 over t + "." + raw_request_body using your signing secret.
  5. Compare your calculated signature with v1 using 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.

Did this answer your question?