> ## Documentation Index
> Fetch the complete documentation index at: https://docs.sajn.se/llms.txt
> Use this file to discover all available pages before exploring further.

# Test webhooks locally

> Receive real sajn webhook deliveries on your own machine while you build

sajn delivers only to public HTTPS URLs, so it can't reach `localhost` directly. To test on your own machine, expose your local server through a tunnel, point a webhook in a sandbox organization at the tunnel, and send test events or trigger real ones.

## Before you begin

* Run your receiver locally, for example one of the [verification samples](/webhooks/verify-signatures#verify-with-a-standard-webhooks-library) on port 3000.
* Create a [sandbox](/get-started/sandbox) and an API key in it. A sandbox behaves like production, but BankID is mocked and nothing is billed, so you can send and sign documents quickly. Its events have `environment` set to `SANDBOX`.

## Expose your local server

Start a tunnel to your local port. Each tool prints a public HTTPS URL that forwards to your machine.

<Tabs>
  <Tab title="ngrok">
    ```bash theme={null}
    ngrok http 3000
    ```

    The URL is shown on the `Forwarding` line, such as `https://TUNNEL_ID.ngrok-free.app`.
  </Tab>

  <Tab title="cloudflared">
    ```bash theme={null}
    cloudflared tunnel --url http://localhost:3000
    ```

    The URL is shown in the output, such as `https://TUNNEL_ID.trycloudflare.com`.
  </Tab>
</Tabs>

On free plans, the URL changes every time you restart the tunnel. When it changes, [update the webhook's URL](/webhooks/manage-endpoints#update-an-endpoint).

## Create a test endpoint

In the sandbox, create a webhook that points at the tunnel:

```bash theme={null}
curl -X POST https://app.sajn.se/api/v1/webhooks \
  -H "Authorization: Bearer SANDBOX_API_KEY" \
  -H "Sajn-Version: 2026-10" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://TUNNEL_HOST/webhooks/sajn",
    "events": ["contact.created", "document.completed"]
  }'
```

Replace the following:

* `SANDBOX_API_KEY`: an API key from your sandbox.
* `TUNNEL_HOST`: the host name that your tunnel printed.

Store the `secret` from the response and start your receiver with it, for example with `SAJN_WEBHOOK_SECRET` set to that value.

### Dashboard save check

When you save a webhook in the dashboard, sajn first sends a `POST` request with the body `{"test": true}` and the user agent `sajn-webhook-validator/1.0` to the URL. This request isn't signed and isn't an event. The save fails if the URL doesn't answer within 5 seconds, redirects, returns `404 Not Found`, or returns a `2xx` HTML page. Any other answer passes, including the `400 Bad Request` that your signature check returns for this unsigned request.

The API runs a lighter check: it sends a `GET` request and only rejects a `2xx` HTML page.

## Send a test event

To check that your receiver gets and verifies deliveries, send it a `webhook.test` event:

```bash theme={null}
curl -X POST https://app.sajn.se/api/v1/webhooks/WEBHOOK_ID/test \
  -H "Authorization: Bearer SANDBOX_API_KEY" \
  -H "Sajn-Version: 2026-10"
```

Replace `WEBHOOK_ID` with the `id` of your test endpoint.

sajn sends a signed event to this endpoint only, with the following body:

```json theme={null}
{
  "id": "1f0c7d2e-5b8a-4c3f-9e6d-2a7b8c9d0e1f",
  "type": "webhook.test",
  "createdAt": "2026-10-01T09:30:00.000Z",
  "apiVersion": "2026-10",
  "workspaceId": "cm4k2x9p10001abcd1234efgh",
  "environment": "SANDBOX",
  "actor": { "type": "API_KEY", "id": "cm4k2x9p10002abcd1234efgh" },
  "data": {
    "object": {
      "id": "cm4k2x9p10013abcd1234efgh",
      "url": "https://TUNNEL_HOST/webhooks/sajn"
    }
  }
}
```

The response is the delivery, with `status` set to `PENDING`. To see your receiver's answer, pass its `id` to [`GET /api/v1/webhooks/{id}/deliveries/{deliveryId}`](/api-reference/get-a-single-webhook-delivery). The test event is retried like any other event if your receiver fails, and `GET /api/v1/events` doesn't list it.

To test your receiver without sajn, sign a request yourself with the [`openssl` script](/webhooks/verify-signatures#send-a-signed-test-request).

## Trigger a real event

To test how your receiver handles real data, trigger an event in the sandbox. Creating a contact is the quickest:

```bash theme={null}
curl -X POST https://app.sajn.se/api/v1/contacts \
  -H "Authorization: Bearer SANDBOX_API_KEY" \
  -H "Sajn-Version: 2026-10" \
  -H "Content-Type: application/json" \
  -d '{"firstName": "Alex", "lastName": "Lind", "email": "alex@example.com"}'
```

Your receiver gets a `contact.created` delivery within seconds. For the request, see [Create a contact](/api-reference/create-a-new-contact). To test the signing flow, send a document to yourself in the sandbox and sign it with the mocked BankID; the endpoint gets `document.completed` after sajn seals the PDF.

## Resend a real delivery to your machine

To test with an event that your endpoint received before, [retry its delivery](/webhooks/replay-and-reconcile#retry-a-delivery). A retry goes only to the endpoint that the delivery belongs to, with its original body, and it keeps the event `id`. To test with an event from another endpoint, read it with [`GET /api/v1/events/{id}`](/api-reference/get-an-event) and pass the response to your handler directly.

## Inspect deliveries

Every delivery, with each attempt's request headers, your status code, and your response body, appears in **Inställningar > Utvecklare > Loggar** in the workspace, and through [`GET /api/v1/webhooks/{id}/deliveries`](/api-reference/list-webhook-deliveries). To find the delivery for a request that your receiver logged, pass its `webhook-id` as the `eventId` filter. To send a logged delivery again, open it and click **Sänd om** (Resend).

## Go to production

Before you point a production endpoint at your receiver, check the following:

* The receiver [verifies signatures](/webhooks/verify-signatures) against the raw body with the production endpoint's secret.
* It deduplicates on the event `id` and returns `2xx` within 30 seconds. For both, see [How delivery behaves](/webhooks/overview#how-delivery-behaves).
* It runs on a stable public HTTPS URL, not a tunnel.
* You deleted the tunnel endpoints that you no longer use, so they don't fail and get [paused](/webhooks/delivery-and-retries#automatic-pausing).


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.