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

# Best practices

> Build a webhook receiver that is fast, safe against forged and repeated events, and correct when events arrive out of order.

## Answer with a 2xx first, then do the work

Runcrate waits 10 seconds for an answer. Anything slower counts as a failure and is retried, so a slow handler gets the same event again. Check the signature, store the event or put it on a queue, answer `200`, and do the work in a background worker.

```javascript theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
app.post('/runcrate/webhooks', express.raw({ type: 'application/json' }), async (req, res) => {
  if (!verifyRuncrateSignature(req.body, req.get('Runcrate-Signature'), SECRET)) {
    return res.status(400).send('bad signature');
  }
  await queue.add(JSON.parse(req.body.toString('utf8'))); // fast: a write, not the work itself
  res.sendStatus(200);
});
```

## Verify every signature

Your endpoint's URL is public. Check `Runcrate-Signature` on every request before you trust anything in it, compare in constant time, and reject a timestamp more than 5 minutes old. See [verifying signatures](/docs/webhooks/verifying-signatures).

## Drop duplicates by event id

The same event can arrive more than once: a retry after an answer that came too late, or a **Redeliver** from the log. Every copy has the same `id`. Record the ids you have processed and skip any you have seen. Keep them for at least as long as the retry window (about 3 hours 40 minutes), longer if you redeliver by hand.

```sql theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
-- Process an event only if its id is new.
INSERT INTO runcrate_events (id, type, created_at) VALUES ($1, $2, $3)
ON CONFLICT (id) DO NOTHING
RETURNING id;
```

Use `Runcrate-Delivery` to trace a single request, not to dedupe: each redelivery gets a new one.

## Do not rely on order

Events are sent independently and retried on their own schedule, so `instance.stopped` can arrive before the `instance.ready` that was retried. Order by `created_at` in the envelope, and ignore an event that is older than the state you already have.

## Read current state from the API before acting

An event says what happened at `created_at`. By the time you handle it, things may have changed: a budget was raised, an instance was relaunched. For a decision that costs money or deletes something, read the resource first with the [API](/docs/api-reference), the [SDKs](/docs/sdks/overview) or the CLI, using the ids in `data`:

```python theme={"theme":{"light":"github-light","dark":"catppuccin-mocha"}}
event = json.loads(raw_body)
if event["type"] == "instance.ready":
    instance = client.instances.get(event["data"]["deployment_id"])
    if instance.status == "running":
        start_training(instance)
```

Treat the event as a prompt to look, and the API as the answer.

## Keep the secret in a secret store

The signing secret is shown once. Put it straight into your secret manager or your platform's encrypted environment variables. Do not commit it, log it or paste it into chat. If it may have leaked, [rotate it](/docs/webhooks/verifying-signatures#rotating-the-secret): the old one keeps working for 24 hours while you deploy the new one.

Rotate it on a schedule too, and whenever someone who had it leaves.

## Subscribe only to what you use

Under **Events**, choose **Choose events** and tick only the types your receiver handles. Fewer events means less traffic, fewer retries to look at, and nothing new arriving unexpectedly when Runcrate adds an event type. Use a separate endpoint for each system that needs different events.

## Treat Slack and Discord URLs as secrets

Slack and Discord webhooks are not signed: anyone with the URL can post to your channel. Keep them out of repositories and tickets, and if one leaks, replace it in Slack or Discord and update the endpoint. See [Slack](/docs/webhooks/slack#the-url-is-the-secret) and [Discord](/docs/webhooks/discord#the-url-is-the-secret).

## Ignore fields you do not know

`data` is open: new keys can appear without a new `api_version`. Read the keys you need and ignore the rest. Do not validate events against a closed schema that rejects unknown keys.

## Watch the delivery log

An endpoint that keeps failing is [turned off](/docs/webhooks/retries#automatic-turn-off) after 3 days with no success. Subscribe a second endpoint (a Slack or Discord channel works) to `webhook.disabled`, so you hear about it when it happens.

## Related

> **[Events](/docs/webhooks/events)**
>
> Every event type, its fields and an example.

> **[Retries and the delivery log](/docs/webhooks/retries)**
>
> The retry schedule, Redeliver and automatic turn-off.


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