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

# Webhooks

> Receive a signed POST when something happens on your account.

Instead of polling for changes, register an endpoint and Truscan will POST a
signed JSON body to it when an event occurs.

```json theme={null}
{
  "id": "evt_9f2c4a1b8d3e5f6a7b8c9d0e",
  "type": "credits.low",
  "created_at": "2026-09-19T14:03:11Z",
  "data": {
    "balance": "$0.84",
    "threshold": "$1.00"
  }
}
```

Every delivery carries two headers:

| Header              | Example                   |
| ------------------- | ------------------------- |
| `Truscan-Signature` | `t=1758290591,v1=5a9f...` |
| `Truscan-Timestamp` | `1758290591`              |

Always [verify the signature](/webhooks/verify) before trusting a payload.
Your endpoint URL is not a secret, and anyone who learns it can post to it.

## Create an endpoint

Choose which [events](/webhooks/events) it should receive.

```bash theme={null}
curl https://api.truscan.co/api/webhooks \
  -H "Authorization: Bearer $TRUSCAN_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/truscan",
    "events": ["credits.low", "credits.exhausted"],
    "description": "Production alerts"
  }'
```

<Warning>
  The response is the only time the signing secret is readable. Store it
  before you close the connection. It cannot be retrieved later, only
  replaced by deleting the endpoint and creating a new one.
</Warning>

```json theme={null}
{
  "success": true,
  "result": {
    "id": "whe_4c1f8a2b9d7e3f6a5b0c2d1e",
    "url": "https://example.com/hooks/truscan",
    "events": ["credits.exhausted", "credits.low"],
    "enabled": true,
    "secret": "whsec_2f9a...",
    "created_at": "2026-09-19T14:01:00Z"
  },
  "message": "Endpoint added. Copy the signing secret now, it is not shown again."
}
```

## Responding

Return any `2xx` status. Truscan reads the status line and nothing else, so an
empty `200` is a perfectly good response.

Acknowledge first and process afterwards. The delivery attempt is subject to a
**10 second timeout**, and work done before you respond counts against it.

| Response                    | What happens                                                   |
| --------------------------- | -------------------------------------------------------------- |
| `2xx`                       | Marked delivered.                                              |
| `410 Gone`                  | Delivery stops permanently. Use this when an endpoint retires. |
| Anything else               | [Retried](/webhooks/retries) up to 5 times.                    |
| Timeout or connection error | Retried the same way.                                          |

## Endpoint requirements

<AccordionGroup>
  <Accordion title="Must be publicly reachable over HTTPS" icon="lock">
    Truscan resolves the hostname and refuses to connect to private, loopback,
    link-local or metadata addresses. `localhost` and internal ranges will not
    work. For local development, use a tunnelling service that gives you a
    public hostname.
  </Accordion>

  <Accordion title="Redirects are not followed" icon="arrow-right">
    A `301` or `302` is treated as a failed delivery. Register the final URL.
  </Accordion>

  <Accordion title="Credentials do not belong in the URL" icon="key">
    A URL containing a username and password is rejected. Authenticate the
    delivery with the signature instead.
  </Accordion>
</AccordionGroup>

## Testing

`POST /api/webhooks/{id}/test` queues a synthetic `ping` event so you can
verify an endpoint before real activity happens.

```bash theme={null}
curl -X POST https://api.truscan.co/api/webhooks/whe_4c1f.../test \
  -H "Authorization: Bearer $TRUSCAN_API_KEY"
```

It arrives within a few seconds and appears in the delivery log like any other
event.

## Pausing and removing

`PATCH /api/webhooks/{id}` with `"enabled": false` stops delivery without
losing the endpoint or its secret. Queued deliveries for a paused endpoint are
abandoned rather than held.

`DELETE /api/webhooks/{id}` removes the endpoint and its delivery history.

## Limits

* **10 endpoints** per account
* **10 second** timeout per delivery attempt
* Delivery history is kept for **30 days**
