# Deck Webhooks

[Deck](https://deck.co) is an API for computer-use agents that log in to websites on a user's behalf. Deck pushes events to **event destinations**. This skill covers the `webhook` destination type: HTTP `POST` deliveries signed with Standard Webhooks.

## When to Use This Skill

* How do I receive Deck webhooks?
* How do I verify a Deck webhook signature?
* Where do I get my Deck webhook signing secret? (You generate it yourself.)
* How do I handle `task_run.interaction_required` and answer an MFA prompt?
* Why is my Deck `webhook-signature` verification failing?
* How do I rotate a Deck webhook secret without downtime?
* My Deck destination is stuck in `pending_verification`. What's wrong?
* Why did my Deck destination switch to `inactive`?

## The Secret Is Yours to Generate

Deck **does not issue** a signing secret. `webhook.secret` is optional and **caller-provided**: you generate it, send it when you create the destination, and Deck can't show it to you again. Without one, you can't verify deliveries.

```bash
# "whsec_" + base64 of random bytes (24–32 bytes is the Standard Webhooks
# convention; Deck's docs don't specify a length)
node -e "console.log('whsec_' + require('crypto').randomBytes(32).toString('base64'))"

```

```bash
curl -X POST https://api.deck.co/v2/event-destinations \
  -H "Authorization: Bearer sk_live_..." -H "Content-Type: application/json" \
  -d '{"name":"Production Webhook","type":"webhook",
       "webhook":{"url":"https://your-app.com/webhooks/deck","secret":"whsec_..."},
       "events":["credential.verified","task_run.completed","task_run.failed"]}'

```

Add an optional `filter` to deliver only matching events, for example `"filter": {"data": {"agent_id": {"$in": ["agt_..."]}}}` (operators include `$eq`, `$neq`, `$in`, `$nin`, `$gt`, `$startsWith`, `$exist`, `$and`, `$or`, `$not`). It applies to retries and `test.ping` too.

New destinations start as `pending_verification` and become `active` (firing `event_destination.activated`) once your endpoint returns a `2xx`. Deck doesn't document a challenge or handshake. Your endpoint has to be reachable and return a `2xx`. See [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/deck-webhooks/references/setup.md).

## Verification (core)

| Header | Value |
| --- | --- |
| `webhook-id` | Unique event identifier |
| `webhook-timestamp` | Unix seconds when the delivery was sent |
| `webhook-signature` | `v1,<base64 HMAC-SHA256>`, possibly several space-separated entries |

Signed content is `{webhook-id}.{webhook-timestamp}.{raw body}`. The key is the base64-**decoded** part of the secret after `whsec_`. Deck's docs say to use the official SDK (`standardwebhooks` on npm and PyPI). It enforces a 5-minute timestamp tolerance and compares in constant time.

```javascript
const { Webhook } = require('standardwebhooks');

// Current secret first; Deck keeps the previous one valid 24h after rotation.
const secrets = [process.env.DECK_WEBHOOK_SECRET, process.env.DECK_WEBHOOK_SECRET_PREVIOUS].filter(Boolean);
if (secrets.length === 0) throw new Error('DECK_WEBHOOK_SECRET not set'); // fail closed → 500

function verifyDeckWebhook(rawBody, headers) {   // rawBody: exact bytes, NOT re-serialised JSON
  let lastError;
  for (const secret of secrets) {
    try {
      return new Webhook(secret).verify(rawBody, headers); // strips whsec_, base64-decodes key
    } catch (err) { lastError = err; }
  }
  throw lastError;                                  // → 400 Invalid signature
}

```

Python: `from standardwebhooks.webhooks import Webhook`, then `Webhook(secret).verify(await request.body(), headers)`.

> **For complete handlers with tests**, see [examples/express/](https://github.com/hookdeck/webhook-skills/tree/main/skills/deck-webhooks/examples/express/), [examples/nextjs/](https://github.com/hookdeck/webhook-skills/tree/main/skills/deck-webhooks/examples/nextjs/), [examples/fastapi/](https://github.com/hookdeck/webhook-skills/tree/main/skills/deck-webhooks/examples/fastapi/).

## Event Envelope

```json
{ "id": "evt_...", "object": "event", "type": "task_run.completed",
  "data": { "id": "trun_...", "object": "task_run", "status": "completed", "result": "success", "...": "..." },
  "created_at": "2025-01-15T09:31:15Z", "request_id": "req_..." }

```

`type` is the discriminator (`object.action`). `request_id` is only present when an API call caused the event. Fields with no value yet arrive as empty strings, for example `"workflow_id": ""`. Exception: `workflow_run.*` uses `null` for `result` and `trigger_id`, and uses `workflow_run_id` instead of `id`.

## Common Event Types

| Event | Fired when |
| --- | --- |
| `task_run.interaction_required` | A run needs user input (`mfa`, `account_selection`, `security_question`). **Time-sensitive** (5-minute window) |
| `task_run.completed` / `task_run.failed` | Run finished; `result` is `success` / `failure` |
| `credential.verified` / `credential.invalid` | Deck Vault credential worked / was rejected by the source |
| `workflow_run.completed` / `workflow_run.failed` / `workflow_run.paused` | Workflow run state changes |
| `storage.created` | A file was captured during a task run |
| `event_destination.activated` / `event_destination.deactivated` | Destination status changed. `deactivated` also fires when Deck auto-deactivates a destination after 100 consecutive failures |
| `test.ping` | Console **Send test ping** or `POST /v2/event-destinations/{id}/test`. Not subscribable, never retried. Return a `2xx` |

Also: `agent.*`, `source.*`, `credential.created|unverified|deleted`, `session.*`, `task.*`, `task_run.queued|running|canceling|canceled`, `workflow.*`, `workflow_run.queued|running|canceling|canceled`, and `storage.deleted`. [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/deck-webhooks/references/overview.md) lists all of them with payloads.

To answer an interaction, read `data.interaction.fields` and call `POST /v2/task-runs/{data.id}/interaction` with `{"input": {"<field name>": "<value>"}}`.

## Delivery Semantics

* Return a `2xx` within **5 seconds**, or Deck counts the delivery as failed and retries it even if your original request is still running. Enqueue the event and acknowledge it; do the processing asynchronously.
* Up to **10 retries** with exponential backoff (base 2, starting at 30s). Inspect failures with `GET /v2/event-destinations/{id}/event-deliveries?status=failure`.
* **Auto-deactivation:** if deliveries to a destination fail **100 times in a row**, Deck sets it to `inactive` and stops delivering (a success resets the count). Deck emits `event_destination.deactivated`, so subscribe a *different* destination to it. Once the endpoint is fixed, `PATCH /v2/event-destinations/{id}` with `{"status": "active"}`.
* **At-least-once delivery.** Dedupe on the event `id` (`evt_...`) and return `200` for duplicates.
* **Order isn't guaranteed.** Compare the event's `created_at` with your last-seen state and drop stale updates.
* Static delivery IPs (`34.139.110.242`, `35.237.129.153`, `34.24.255.163`) are optional defence in depth and don't replace signature checks.

## Environment Variables

```bash
DECK_WEBHOOK_SECRET=whsec_your_generated_base64_secret   # required; handlers fail closed without it
DECK_WEBHOOK_SECRET_PREVIOUS=                            # optional, during the 24h rotation window

```

## Local Development

```bash
# Express / Next.js (use 8000 for FastAPI). No install or account required.
npx hookdeck-cli listen 3000 deck --path /webhooks/deck

```

Use the printed URL as `webhook.url`. Deck's own docs recommend Hookdeck for local testing. To look at payloads before you have a receiver, choose **Test endpoint** in the Deck Console. Deck also has a native `hookdeck` destination type (`hookdeck.token` = Hookdeck source token), which routes events through the Event Gateway without Standard Webhooks signing on Deck's side.

## Reference Materials

* [references/overview.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/deck-webhooks/references/overview.md) - Full event catalog, payload shapes, interactions
* [references/setup.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/deck-webhooks/references/setup.md) - Generating the secret, creating the destination, filters, activation, auto-deactivation, rotation, IPs
* [references/verification.md](https://github.com/hookdeck/webhook-skills/blob/main/skills/deck-webhooks/references/verification.md) - SDK and manual verification, gotchas, debugging