Skip to content
PaalChat Docs

Build and operate

Webhooks and callbacks

Receive events safely - verify signatures, acknowledge fast, handle retries.

.md

In a nutshell

PaalChat POSTs signed JSON events to your callback URL. Verify the X-PaalChat-Signature on the raw body, answer 2xx within 10 seconds, de-duplicate on X-PaalChat-Delivery, and do slow work in a queue.

Set up

A PaalChat operator registers your callback URL and generates the signing secret:

cURL
php artisan products:callback multi-skuul --url=https://skuuls.paaltech.org/api/paalchat/callback

The secret (pcs_...) is printed once - keep it in your secret store. Rotate it with --rotate-secret.

Several endpoints

A product can have more than one webhook endpoint - for example one service for incoming messages and another for delivery receipts. Each endpoint has its own secret, and can be limited to some events (message.received, message.status, template.status, whatsapp.connection), some businesses (by external_id) or some channels. Each matching endpoint gets its own delivery, with its own X-PaalChat-Delivery id and its own retries.

cURL
php artisan products:webhook multi-skuul add --url=https://receipts.example.com/hook --event=message.status
php artisan products:webhook multi-skuul            # list

Ask a PaalChat operator to add, filter, pause or remove endpoints; self-service comes with the developer portal.

Design a reliable endpoint

  • Use HTTPS, accept only POST.
  • Verify the signature before parsing JSON.
  • Record the event, then answer 2xx quickly. Do slow work afterwards, in a queue.
  • Make processing idempotent: the same event may be delivered more than once.

The request

HTTP
POST /api/paalchat/callback
Content-Type: application/json
X-PaalChat-Event: message.status
X-PaalChat-Delivery: 7f0c2d9e-5a41-4f7b-9b0e-3c1d2e4f5a6b
X-PaalChat-Signature: t=1759258468,v1=5d41402abc4b2a76b9719d911017c592e0d5ad0d4e6a1b4c9e3f2a1b0c9d8e7f
JSON
{
  "id": "7f0c2d9e-5a41-4f7b-9b0e-3c1d2e4f5a6b",
  "event": "message.status",
  "occurred_at": "2026-09-30T18:54:28+00:00",
  "business": {"external_id": "presec"},
  "data": {}
}

id equals X-PaalChat-Delivery.

Verify the signature

v1 is the hex HMAC-SHA256 of "<t>.<raw body>" with your secret. Reject the request if it does not match (constant-time compare) or if t is more than 300 seconds from now.

function paalchatSignatureValid(string $header, string $rawBody, string $secret): bool
{
    if (! preg_match('/^t=(\d+),v1=([a-f0-9]{64})$/', $header, $m)) return false;
    if (abs(time() - (int) $m[1]) > 300) return false;
    return hash_equals(hash_hmac('sha256', $m[1].'.'.$rawBody, $secret), $m[2]);
}

Warning

Verify against the exact bytes received. Re-encoding parsed JSON changes spacing and breaks the signature.

Retries

  • Any 2xx within 10 seconds acknowledges the delivery.
  • Anything else, a timeout or a redirect is a failure. PaalChat retries after 1 min, 5 min, 30 min, 2 h, 6 h and 12 h (7 attempts, about 21 hours), then marks the delivery failed; an operator can resend it.
  • Redirects are not followed - register the final URL.

Events

Event When Reference
message.received A customer sent the business a message message.received
message.status An outgoing message was sent, delivered, read or failed, or its outcome became unknown message.status
template.status Meta approved, rejected or paused a template template.status
whatsapp.connection A WhatsApp account connected, disconnected, broke or its token is expiring whatsapp.connection

Verify the final state

Callbacks can arrive out of order. Apply message statuses only forward, and for anything important (a connection change, a failed payment receipt), read the current state from the API before acting.