Skip to main content
Webhook destinations deliver events as HTTP POST requests to your endpoint. If you haven’t set one up yet, start with Destinations & 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.

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

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

Replay failed deliveries

If a delivery failed because of a bug in your handler, fix the bug first, then replay:
curl -X POST "https://api.deck.co/v2/event-deliveries/{event_delivery_id}/replay" \
  -H "Authorization: Bearer sk_live_..."
Replayed deliveries follow the same retry logic. If your endpoint is idempotent, replaying is always safe.

Endpoint availability

Deck retries failed deliveries up to 3 times with exponential backoff. After all retries are exhausted, the destination is set to inactive. To recover, fix the issue, set the destination status back to active via the API or Dashboard, then replay any failed deliveries.