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

# Webhook best practices

> Build reliable, resilient event consumers for Deck webhooks.

[Webhook destinations](/events/destinations-and-deliveries) deliver events as HTTP `POST` requests to your endpoint. If you haven't set one up yet, start with [Destinations & deliveries](/events/destinations-and-deliveries).

Once events are flowing, your endpoint needs to handle them reliably. Network issues, duplicate deliveries, and out-of-order events are normal in any webhook-based system.

## Local development

For local testing, we recommend [Hookdeck](https://hookdeck.com). Create a Hookdeck destination, then use the [Hookdeck CLI](https://hookdeck.com/docs/cli) (`hookdeck listen <port> <source-name>`) to forward deliveries to a localhost port. Payloads and replays are available in the Hookdeck dashboard.

## Respond quickly

Return a `2xx` status code within **5 seconds**. Deck treats anything else as a failure and schedules a retry.

Do your processing asynchronously. Accept the event, persist it, and return immediately.

```javascript theme={null}
app.post("/webhooks/deck", async (req, res) => {
  const event = req.body;

  await queue.enqueue(event);

  res.status(200).json({ received: true });
});
```

If your endpoint takes longer than 5 seconds, Deck retries the delivery even though your original request may still be running, which leads to duplicate processing.

## Verify signatures

Verify every delivery before processing it. Cloud destinations like SQS and Pub/Sub authenticate through their native IAM credentials, but webhook destinations require signature verification.

Deck signs every webhook delivery using the [Standard Webhooks](https://www.standardwebhooks.com/) specification. Every delivery includes `webhook-id`, `webhook-timestamp`, and `webhook-signature` headers. Use the official Standard Webhooks SDK — it handles signature computation, timestamp tolerance, and constant-time comparison.

```javascript theme={null}
import { Webhook } from "standardwebhooks";

const wh = new Webhook(process.env.DECK_WEBHOOK_SECRET);

app.post("/webhooks/deck", (req, res) => {
  try {
    const payload = wh.verify(req.rawBody, req.headers);
    await processEvent(payload);
    res.status(200).json({ received: true });
  } catch (err) {
    res.status(401).json({ error: "Invalid signature" });
  }
});
```

When you rotate your signing secret, Deck keeps the previous secret valid for 24 hours. During the rollover window, verify against both the old and new secrets.

## Handle duplicates

Deck guarantees **at-least-once delivery**. The same event may arrive more than once, especially after retries. Use the event `id` as a deduplication key and design your processing to be idempotent — use upserts instead of inserts, and check state before mutating.

```javascript theme={null}
app.post("/webhooks/deck", async (req, res) => {
  const event = req.body;

  const alreadyProcessed = await db.eventLog.findUnique({
    where: { event_id: event.id },
  });

  if (alreadyProcessed) {
    return res.status(200).json({ received: true });
  }

  await processEvent(event);

  await db.eventLog.create({
    data: { event_id: event.id, processed_at: new Date() },
  });

  res.status(200).json({ received: true });
});
```

## Handle out-of-order delivery

Events may not arrive in order. A `task_run.completed` event could arrive before `task_run.running` if the first delivery was retried. Use the event's `created_at` timestamp to determine the true sequence — compare it against your last-known state and skip anything older.

```javascript theme={null}
async function handleTaskRunEvent(event) {
  const { task_run_id } = event.data;
  const lastSeen = await db.taskRunState.findUnique({
    where: { task_run_id },
  });

  if (lastSeen && new Date(event.created_at) <= new Date(lastSeen.last_event_at)) {
    return; // Stale event, skip
  }

  await db.taskRunState.upsert({
    where: { task_run_id },
    update: {
      status: event.data.status,
      last_event_at: event.created_at,
    },
    create: {
      task_run_id,
      status: event.data.status,
      last_event_at: event.created_at,
    },
  });
}
```

## Build retry-safe endpoints

Design your endpoint to handle the same payload multiple times safely:

* **Return `200` for events you've already processed.** Never return an error for duplicates.
* **Don't assume sequential delivery.** Your endpoint may receive events from different task runs interleaved.
* **Handle missing context gracefully.** If an event references a resource you haven't seen yet, fetch it from the API or queue the event for later.

## Monitor delivery health

Track delivery status through the API to catch integration issues early.

### Check for failed deliveries

```bash theme={null}
curl -X GET "https://api.deck.co/v2/event-destinations/{destination_id}/event-deliveries?status=failure" \
  -H "Authorization: Bearer sk_live_..."
```

## Endpoint availability

Deck retries failed deliveries up to **10 times** with exponential backoff (base 2, starting at 30 seconds). Persistent failures don't auto-disable a destination, but they do accumulate `failure` rows you can inspect via the deliveries endpoint.

If you take a destination offline intentionally, set its status to `inactive` via the API or Console while you recover, then back to `active` to resume deliveries.
