Testing
The salaf.ping event and how to develop against it.
There is no sandbox tenant and no test-mode key, so the way to prove a
receiver works is to send it a real event that changes nothing: salaf.ping.
Send a test event
In the dashboard: Settings → Developer → Webhooks, open an endpoint, and choose Send test event.
The ping is not a shortcut. It is recorded in the outbox, fanned out, signed
with your endpoint's actual secret, delivered by the same worker, retried on
the same ladder, and written to the same delivery log as any other event. A
ping that arrives is therefore real evidence that an order.created would —
and a ping that fails is debuggable with the same detail view, showing the
exact payload sent and the response received.
{
"id": "evt_01J4X8G9ABCDEFGHJKMNPQRSTV",
"type": "salaf.ping",
"api_version": "2026-08",
"created_at": "2026-08-11T10:00:00.000Z",
"company_id": "e07b4a29-6c13-4d85-b920-3f8e1c65d704",
"store_id": "b21c5f04-9d7e-4c11-8f3a-6e0d2b915a77",
"data": {
"object": {
"store_id": "b21c5f04-9d7e-4c11-8f3a-6e0d2b915a77",
"message": "This is a test event from Salaf. If you can read this, your endpoint and signature verification are working.",
"sent_at": "2026-08-11T10:00:00.000Z"
}
}
}The endpoint must also be active. Sending a test to a disabled or auto-disabled endpoint answers "Enable the endpoint before sending a test event."
Reading the result
The delivery history for an endpoint shows every attempt: status, HTTP code, attempt number, duration, next retry, and the response your server sent (first kilobyte). Open one and you also get the exact payload we signed — byte for byte, which is what makes a signature mismatch diagnosable rather than mysterious.
The failures worth recognising:
| What you see | What it usually is |
|---|---|
No response within 10s | The handler is doing the work inline. Queue it and answer first. |
HTTP 400 with your own error text | Your signature check is rejecting us. Run the test vectors. |
Redirects are not followed… | The URL redirects (often http → https, or a trailing-slash rule). Register the final URL. |
| A DNS or address error | The hostname does not resolve publicly. Tunnels and private addresses are refused, deliberately. |
HTTP 404 | The route exists in your framework but not for POST, or a CSRF/auth middleware is in front of it. |
Developing locally
Your URL must be public https — we resolve the hostname and refuse private
addresses, so localhost, 127.0.0.1 and anything on a private range cannot
be registered.
Two approaches that work:
- A public tunnel (ngrok, Cloudflare Tunnel, Tailscale Funnel) pointed at
your dev server. Register the tunnel's
httpsURL as an endpoint, subscribe it to*, and develop against real events. Remember to delete the endpoint when the tunnel dies, or it will fail for three days and auto-disable. - A replayed payload, offline. Copy the exact payload and the
X-Salaf-Signatureheader from a delivery in the log, and post them at your own handler from a test. This is the one that belongs in your test suite: it needs no network, and it catches the raw-body mistakes that only show up on payloads containing non-ASCII text.
A checklist before you go live
- The endpoint is subscribed to the events you actually handle, plus
salaf.ping. - Signature verification runs against the raw body and rejects on failure.
- The five-minute timestamp window is enforced.
- Both
v1=values are accepted, so a rotation does not take you down. - Events are de-duplicated on
X-Salaf-Event-Id. - Handlers upsert, and tolerate an update arriving before its create.
- The endpoint answers
2xxin well under 10 seconds and does its work asynchronously. - Unknown event types are ignored rather than throwing.