Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.exowizz.com/llms.txt

Use this file to discover all available pages before exploring further.

Every webhook delivery includes a cryptographic signature so you can verify the request came from your Exo app, not from a third party.

How signing works

When Exo sends a webhook, it:
  1. Serializes the JSON payload
  2. Computes an HMAC-SHA256 hash using the subscription’s secret as the key
  3. Sends the hash in the X-Exo-Signature header, prefixed with sha256=
The request also includes a User-Agent: Exo-Webhook/1.0 header. For traceability, Exo also sends X-Exo-Event and X-Exo-Delivery headers.

Verifying in your receiver

To verify a webhook delivery, compute the same HMAC hash on your end and compare it to the signature header.
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_EXO_SIGNATURE'] ?? '';
$secret = 'your-webhook-secret';

$expected = 'sha256=' . bin2hex(
    hash_hmac('sha256', $payload, $secret, binary: true)
);

if (! hash_equals($expected, $signature)) {
    http_response_code(401);
    exit('Invalid signature');
}

$data = json_decode($payload, true);
// Process the webhook...

Important notes

  • Always use a constant-time comparison function (like hash_equals in PHP or hmac.compare_digest in Python) to prevent timing attacks.
  • The signature is computed on the raw JSON body of the request, not on any parsed or reformatted version. Make sure you read the raw body before parsing.
  • Each webhook subscription has its own unique secret, generated when the subscription is created. If you have multiple subscriptions, use the correct secret for each one.

Webhook request format

Every webhook delivery is an HTTP POST with these headers:
HeaderValue
Content-Typeapplication/json
X-Exo-Signaturesha256= followed by the hex-encoded HMAC hash
X-Exo-EventEvent UUID
X-Exo-DeliveryDelivery UUID
User-AgentExo-Webhook/1.0
The request body is a JSON object:
{
  "event": "on_create",
  "resource": "order",
  "event_id": "01JT1M2X3B2W3PW8ZAF2X36JMY",
  "delivery_id": "01JT1M2Y9HF38TEHSEK5EN9V5C",
  "data": {
    "id": 42,
    "order_number": "ORD-001",
    "status": "pending"
  },
  "timestamp": "2026-03-28T14:30:00.123456Z"
}

Handling failures

If your webhook receiver returns a non-2xx status code or times out, Exo retries using configured attempt limits and backoff values (global settings in config/exo.php, or per Resource overrides through webhookMaxAttempts() and webhookBackoff()).
Failed deliveries can also be retried manually through POST /exo-api/webhook-deliveries/{delivery}/retry, subject to the configured manual retry limit.