Agent skill

Deck Webhooks Skill

Receive and verify Deck (deck.co) webhooks, the event stream for Deck's computer-use agents API (agents, sources, Deck Vault credentials, sessions, tasks, task runs, workflows, storage). Use when setting up a Deck webhook event destination, generating the caller-provided whsec_ signing secret, debugging Standard Webhooks signature verification (webhook-id, webhook-timestamp, webhook-signature), rotating secrets, or handling events like task_run.completed, task_run.failed, task_run.interaction_required (MFA prompts), credential.verified, credential.invalid, workflow_run.completed, storage.created and test.ping.

Install this skill

npx skills add hookdeck/webhook-skills --skill deck-webhooks


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

# "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'))"
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.

Verification (core)

HeaderValue
webhook-idUnique event identifier
webhook-timestampUnix seconds when the delivery was sent
webhook-signaturev1,<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.

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/, examples/nextjs/, examples/fastapi/.

Event Envelope

{ "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

EventFired when
task_run.interaction_requiredA run needs user input (mfa, account_selection, security_question). Time-sensitive (5-minute window)
task_run.completed / task_run.failedRun finished; result is success / failure
credential.verified / credential.invalidDeck Vault credential worked / was rejected by the source
workflow_run.completed / workflow_run.failed / workflow_run.pausedWorkflow run state changes
storage.createdA file was captured during a task run
event_destination.activated / event_destination.deactivatedDestination status changed. deactivated also fires when Deck auto-deactivates a destination after 100 consecutive failures
test.pingConsole 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 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

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

# 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


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 2, 2026

View on GitHub →