# 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.

```json
{
  "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"
    }
  }
}
```

> **Warning — Subscribe to salaf.ping, or the button refuses**
>
> The ping goes through the same subscription gate as every other event, so an
> endpoint that is not subscribed to `salaf.ping` has nothing to send. You get:
>
> *"This endpoint is not subscribed to salaf.ping. Add it to the endpoint (or
> subscribe to all events) and try again."*
>
> Add `salaf.ping` to the endpoint's event list — or use the `*` wildcard,
> which includes it. It is worth keeping subscribed permanently: it is the only
> event you can produce on demand when something looks wrong in production.

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](https://www.salafems.com/developers/webhook-signatures.md#verify-your-implementation). |
| `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:

1. **A public tunnel** (ngrok, Cloudflare Tunnel, Tailscale Funnel) pointed at
   your dev server. Register the tunnel's `https` URL 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.
2. **A replayed payload, offline.** Copy the exact payload and the
   `X-Salaf-Signature` header 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.

> **Note — Do not re-sign a captured payload with a new timestamp**
>
> If you replay a real delivery in a test, the timestamp is old, so your
> five-minute window check will reject it — correctly. Test the window by
> freezing your clock (or injecting `now`) rather than by loosening the
> tolerance. A receiver that accepts a day-old timestamp is a receiver anyone
> can replay.

## 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](https://www.salafems.com/developers/webhook-rotation.md) 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 `2xx` in well under 10 seconds and does its work
  asynchronously.
- Unknown event types are ignored rather than throwing.
