Agent skill

Pipecat Cloud Webhooks Skill

Receive and verify Pipecat Cloud webhooks (Daily's hosted platform for Pipecat voice agents, delivered via Svix). Use when setting up Pipecat Cloud webhook handlers, debugging svix-signature verification, or handling events like build.succeeded, deployment.updated, service.suspended, spend.limit_reached, and session.started.

Install this skill

npx skills add hookdeck/webhook-skills --skill pipecat-cloud-webhooks


Pipecat Cloud (pipecat.daily.co) signs every webhook delivery following Standard Webhooks, with the delivery provider's svix-* header names. Each delivery is an HTTP POST with a signed JSON body and three headers: svix-id, svix-timestamp, and svix-signature. The body is an envelope: { "event_type": "build.succeeded", "org": "acme-co", "created_at": "2026-08-25T11:40:32.000Z", "data": { ... } }. org is the organization name and created_at is ISO 8601 UTC. The body has no event id (use svix-id), and no header carries the event type.

When to Use This Skill

  • How do I receive Pipecat Cloud webhooks?
  • How do I verify Pipecat Cloud webhook signatures (svix-signature)?
  • How do I handle build.succeeded, deployment.updated or session.started events?
  • How do I react to spend.threshold_reached, spend.limit_reached or service.suspended?
  • Why is my Pipecat Cloud webhook signature verification failing, or rejecting Hookdeck retries?

Verification (core)

Signed content is {svix-id}.{svix-timestamp}.{raw body}. Svix signs it with HMAC-SHA256 using the base64-decoded secret (the part after whsec_), base64-encodes the result, and sends one or more space-separated v1,<signature> entries. Use the official svix library, which compares in constant time. Read the svix-* headers, and when svix-id is absent fall back to the webhook-* headers (webhook-id, webhook-timestamp, webhook-signature), as Hookdeck does.

import { Webhook } from 'svix'; // svix 2.x is ESM-only

const prefix = req.headers['svix-id'] ? 'svix' : 'webhook';
const id = req.headers[`${prefix}-id`];
const timestamp = req.headers[`${prefix}-timestamp`];
const signature = req.headers[`${prefix}-signature`];
// rawBody: the raw Buffer/string, never re-serialized JSON
new Webhook(process.env.PIPECAT_CLOUD_WEBHOOK_SECRET).verify(rawBody, {
  'svix-id': id, 'svix-timestamp': timestamp, 'svix-signature': signature,
  'webhook-id': id, 'webhook-timestamp': timestamp, 'webhook-signature': signature,
}); // throws on a bad signature or a timestamp outside the window
const event = JSON.parse(rawBody.toString('utf8')); // parse only after verify passes
from svix.webhooks import Webhook, WebhookVerificationError

wh = Webhook(os.environ["PIPECAT_CLOUD_WEBHOOK_SECRET"])  # "whsec_..."
wh.verify(raw_body, {"svix-id": id, "svix-timestamp": ts, "svix-signature": sig,
                     "webhook-id": id, "webhook-timestamp": ts, "webhook-signature": sig})

For complete handlers with tests, see examples/express/, examples/nextjs/, examples/fastapi/.

Each endpoint has one signing secret, revealed with the key icon on the endpoint's row under Settings > Webhooks in the Pipecat Cloud dashboard. It looks like whsec_.... Keep the full value in PIPECAT_CLOUD_WEBHOOK_SECRET. Deleting and recreating an endpoint issues a new secret. Handlers fail closed: with the variable unset (or not a whsec_ value) they return 500 instead of accepting anything. Pipecat Cloud's own sample uses the standardwebhooks library after mapping svix-* to webhook-*; the svix library used here reads either set, so no mapping is needed beyond the fallback shown.

Hookdeck retries and replays hit the 5-minute window

The svix and standardwebhooks libraries always enforce a fixed 5-minute timestamp window, and it can't be configured. Hookdeck retries and replays resend the original svix-timestamp, so the library rejects any retry or replay that happens more than 5 minutes after the original delivery. If you verify behind Hookdeck (or replay old events), use the manual verifier in references/verification.md, where the window can be dropped or widened. Deduplicate on svix-id instead of relying on the timestamp.

Common Event Types

20 events are documented (the list may not be exhaustive). The name is event_type in the JSON body, and the examples handle all of them:

EventTriggered When
build.startedA build enters building (best-effort: fast builds may emit only a terminal event)
build.succeededA build succeeds; data.image_uri names the image
build.failedA build fails or is stopped, or the trigger call failed
build.timeoutA build times out
deployment.createdA service's first deployment
deployment.updatedA redeploy replaced the running pods
service.suspendedService torn down: reason is trial_exhausted or account_suspended
service.resumedA suspension was lifted
spend.threshold_reachedSpend crossed 80% or 90% of the limit
spend.limit_reachedSpend reached 100% of the limit
spend.limit_clearedThe spend limit was removed
session.startedAn agent session starts (service name is service, not service_name)
org.trial_started / org.trial_endedA trial code is redeemed / trial credits run out
api_key.created / .rotated / .deletedAn API key changes (the key value is never sent)
secret.created / .updated / .deletedA secret set or key changes (values are never sent)

See references/overview.md for the full catalog and payload shape.

Environment Variables

# Signing secret of the webhook endpoint (Svix format, starts with whsec_)
PIPECAT_CLOUD_WEBHOOK_SECRET=whsec_xxxxx

Local Development

# Start tunnel (no account needed)
npx hookdeck-cli listen 3000 pipecat-cloud --path /webhooks/pipecat-cloud

Register the tunnel URL as an endpoint under Settings > Webhooks in the Pipecat Cloud dashboard, then use the endpoint's send test event action (deliveries carry a webhook-test: true header) or trigger a real build.

Reference Materials


Repository

hookdeck/webhook-skills

v0.1.0 · MIT · Updated Nov 13, 2026

View on GitHub →