Guide to Pipecat Cloud Webhooks: Features and Best Practices
Pipecat Cloud is Daily's hosted platform for running Pipecat voice and multimodal agents. You push agent code, it builds an image, deploys it and starts sessions as users connect. Webhooks let you follow that lifecycle without polling the REST API: a build fails, a deployment replaces its pods, spend nears the limit, an API key is rotated.
Here we cover Pipecat Cloud's 20 events, the dashboard-only setup, the svix-* header names, and the events that can be skipped or lost.
What are Pipecat Cloud webhooks?
Pipecat Cloud webhooks are organisation-level endpoints that receive a signed JSON POST whenever a subscribed event occurs. The webhooks guide says deliveries follow Standard Webhooks, but with the header names of the delivery provider, Svix: svix-id, svix-timestamp and svix-signature. Pipecat's advice is to map them to the webhook-* names if your library expects those; nothing else about the scheme differs.
Every event uses one envelope: event_type, org (the organisation name), created_at (ISO 8601 UTC) and data.
Pipecat Cloud webhook features
| Feature | Details |
|---|---|
| Signature | Standard Webhooks HMAC-SHA256 |
| Signature header | svix-signature |
| Body | JSON |
| Event routing | Body field event_type |
| Configuration | Dashboard only (Settings > Webhooks) |
| Subscriptions | At least one event type per endpoint |
| Secret | whsec_, one per endpoint; lost if the endpoint is deleted |
| Success | Any 2xx |
| Retries | Anything else is retried; no schedule is documented |
| Guarantee | At least once; svix-id stable across retries |
| Test deliveries | webhook-test: true header |
| History | 90 days, with re-dispatch from the dashboard |
Events and payloads
Pipecat documents 20 types in its event reference:
| Event | Triggered When | data fields |
|---|---|---|
build.started | The build enters building (best-effort, not guaranteed) | build_id, region, status, triggered_by, commit_sha, ref, repo_full_name |
build.succeeded | CodeBuild reports SUCCEEDED | the above plus image_uri, build_duration_seconds |
build.failed | Build FAILED or STOPPED, or the trigger call failed | the above plus error_message, build_duration_seconds |
build.timeout | Build TIMED_OUT | the above plus error_message, build_duration_seconds |
deployment.created | A service's first deployment | deployment_id, service_id, service_name, region, build_id (null for image deploys) |
deployment.updated | A redeploy replaced the running pods | same as deployment.created |
service.suspended | Service torn down: org suspended or trial credits ran out | service_id, service_name, region, reason (trial_exhausted or account_suspended) |
service.resumed | A suspension was lifted | service_id, service_name, region |
spend.threshold_reached | Spend crosses 80% or 90% of the limit | percent, current_spend_cents, limit_cents |
spend.limit_reached | Spend reaches 100% of the limit | current_spend_cents, limit_cents |
spend.limit_cleared | The spend limit was removed (a limit of 0 is a kill switch, not a clear) | previous_limit_cents |
session.started | An agent session starts | session_id, service, region, transport (optional) |
org.trial_started | A trial code is redeemed for the first time | org_name, trial_credits_cents (nullable) |
org.trial_ended | Trial credits run out and usage is blocked | org_name, amount_due_cents |
api_key.created | An API key was created | key_id, name, type (public/private), actor_user_id |
api_key.rotated | An API key was rotated (id and name unchanged) | same as api_key.created |
api_key.deleted | An API key was revoked | same as api_key.created |
secret.created | A secret set was created | set_name, region, type (secret/imagePullSecret), scope (set/field), actor_user_id |
secret.updated | A secret value changed | same, plus field_names when scope is field |
secret.deleted | A secret set, or one key in it, was removed | same as secret.updated |
Pipecat doesn't say the list is final, so return 2xx for types you don't handle. The api_key.* and secret.* events never include key or secret values. There is no session.ended event, and deployment.deleted has been retired.
The webhook samples library has this build.succeeded example:
{
"event_type": "build.succeeded",
"org": "acme-co",
"created_at": "2026-08-25T11:40:32.000Z",
"data": {
"build_id": "9f1c3a52-6b0e-4c11-9a76-2d5f8b41e0c7",
"region": "us-west-2",
"status": "success",
"commit_sha": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4",
"ref": "refs/heads/main",
"repo_full_name": "acme-co/voice-agent",
"triggered_by": "github",
"image_uri": "123456789012.dkr.ecr.us-west-2.amazonaws.com/acme-co/voice-agent:e3b0c44",
"build_duration_seconds": 94
}
}
Payload conventions worth knowing:
- Entities come as
_idand_namepairs, such asservice_idandservice_name.session.startedis the exception and sendsservice. - Money is always an integer number of cents in a
*_centsfield. - Nullable fields are always present and may be
null; optional fields are left out when they don't apply. - The body has no event ID. Use the
svix-idheader to deduplicate.
Setting up Pipecat Cloud webhooks
Pipecat Cloud has no CLI command or API-key REST route for webhook endpoints; manage them in the dashboard.
- In the Pipecat Cloud dashboard, open Settings > Webhooks for your organisation.
- Click Add endpoint and enter the URL, for example
https://example.com/webhooks/pipecat-cloud. OnlyhttpandhttpsURLs are accepted; use HTTPS for anything on the internet. - Select event types. At least one is required, and only subscribed types are delivered.
build.succeeded,build.failed,deployment.updated,service.suspendedandspend.limit_reachedmake a sensible first set. - Use the key icon on the endpoint's row to reveal the
whsec_secret, and store it asPIPECAT_WEBHOOK_SECRET.
If the dashboard says "Webhooks are not configured on this environment", contact Pipecat Cloud support to have them enabled. To receive test deliveries locally, forward them with the Hookdeck CLI:
npx hookdeck-cli listen 3000 pipecat-cloud --path /webhooks/pipecat-cloud
With Hookdeck. Register the URL of a Pipecat Cloud source as the endpoint and paste the endpoint's secret into it. The source reads svix-* headers first and falls back to webhook-*. Hookdeck sends back a 200 for every POST, which Pipecat counts as success, and a request whose signature doesn't match is saved as a verification failure without being delivered. Other HTTP methods are rejected.
Securing Pipecat Cloud webhooks
The signed content is {svix-id}.{svix-timestamp}.{raw body}, HMAC-SHA256 with the base64-decoded secret after whsec_, and svix-signature is a space-separated list of v1,<base64> signatures. This function handles either header prefix:
const crypto = require('node:crypto');
function decodeBase64(value) {
if (typeof value !== 'string' || !/^[A-Za-z0-9+/]+={0,2}$/.test(value)) return null;
const bytes = Buffer.from(value, 'base64');
return bytes.length > 0 && bytes.toString('base64').replace(/=+$/, '') === value.replace(/=+$/, '')
? bytes : null;
}
function verifySignature(rawBody, headers, secret) {
if (!Buffer.isBuffer(rawBody) || typeof secret !== 'string' || !secret) return false;
const prefix = headers['svix-id'] ? 'svix' : 'webhook';
const id = headers[`${prefix}-id`];
const timestamp = headers[`${prefix}-timestamp`];
const signature = headers[`${prefix}-signature`];
if (typeof id !== 'string' || !id || typeof timestamp !== 'string' ||
!/^[0-9]+$/.test(timestamp) || typeof signature !== 'string') return false;
const key = decodeBase64(secret.replace(/^whsec_/, ''));
if (!key) return false;
const message = Buffer.concat([Buffer.from(`${id}.${timestamp}.`, 'utf8'), rawBody]);
const candidates = signature.split(/\s+/).filter((item) => item.startsWith('v1,')).map((item) => item.slice(3));
const expected = crypto.createHmac('sha256', key).update(message).digest();
return candidates.some((candidate) => {
const received = decodeBase64(candidate);
if (!received) return false;
return received.length === expected.length && crypto.timingSafeEqual(received, expected);
});
}
A receiver for build, spend and suspension alerts that also handles test deliveries:
const express = require('express');
// Returns the parsed body when it is a JSON object, otherwise null.
function parseJsonObject(rawBody) {
try {
const value = JSON.parse(rawBody.toString('utf8'));
return value && typeof value === 'object' && !Array.isArray(value) ? value : null;
} catch {
return null;
}
}
const app = express();
app.post('/webhooks/pipecat-cloud', express.raw({ type: '*/*' }), async (req, res) => {
const secret = process.env.PIPECAT_WEBHOOK_SECRET;
if (!secret) return res.sendStatus(500);
if (!verifySignature(req.body, req.headers, secret)) return res.sendStatus(401);
if (req.headers['webhook-test'] === 'true') return res.sendStatus(200);
const payload = parseJsonObject(req.body);
if (!payload) return res.sendStatus(400); // signed, but not a JSON object
const { event_type, org, data } = payload;
if (typeof event_type !== 'string' || !data || typeof data !== 'object') return res.sendStatus(400);
const message_id = req.headers['svix-id'] ?? req.headers['webhook-id'];
try {
if (event_type === 'build.failed' || event_type === 'build.timeout') {
await alerts.send(message_id, `${org}: build ${data.build_id} ${event_type}: ${data.error_message}`);
} else if (event_type === 'spend.threshold_reached' || event_type === 'spend.limit_reached') {
await alerts.send(message_id, `${org}: spend ${data.current_spend_cents / 100} of ${data.limit_cents / 100}`);
} else if (event_type === 'service.suspended') {
await alerts.send(message_id, `${org}: ${data.service_name} suspended (${data.reason})`);
}
} catch {
return res.sendStatus(503); // not processed, so don't acknowledge
}
return res.sendStatus(200);
});
alerts is your own notifier, which should drop a message_id it has seen.
Pipecat's guide relies on a Standard Webhooks library to reject timestamps more than five minutes from the server's clock, and asks you to keep that clock in sync. That suits direct delivery. Hookdeck applies no timestamp tolerance, and its retries and replays send the original svix-timestamp, so the library check would fail on them. Behind Hookdeck, verify without an age check as the function above does.
Make Pipecat Cloud webhooks production-ready. Hookdeck Event Gateway keeps every build and spend alert, retries your service on your terms, and lets you replay any event past Pipecat's 90-day history.
Pipecat Cloud webhook limitations and pain points
Some events are best-effort or never sent
The Problem: You expect an event and it never arrives.
Why It Happens: build.started is best-effort. Deployment events are sent after the deploy commits, and a process restart in that window can drop the event even though the deploy succeeded. A deploy that changes nothing emits no event, there is no session.ended, and deployment.deleted is retired.
Workarounds:
- Treat the terminal build events as authoritative and don't pair them with
build.started. - Check deployment state through the API when an expected event is missing.
How Hookdeck Can Help: Hookdeck can't create events Pipecat didn't send, but its log of received events makes a missing one easy to spot.
Deleting an endpoint changes the secret
The Problem: After recreating an endpoint at the same URL, every delivery fails verification.
Why It Happens: Deleting an endpoint destroys its secret and delivery history, and a new endpoint gets a new secret.
Workarounds:
- Disable an endpoint to pause deliveries instead of deleting it.
- If you do recreate one, update the stored secret at the same time.
How Hookdeck Can Help: Keep a single Hookdeck source registered and change destinations behind it instead of endpoints in Pipecat.
Undocumented retry schedule
The Problem: You don't know how long a failing endpoint will keep receiving retries.
Why It Happens: Pipecat says non-2xx responses are retried but gives no schedule or limit.
Workarounds:
- Use the dashboard's 90-day history to re-dispatch deliveries after an outage.
How Hookdeck Can Help: Hookdeck accepts each event straight away and applies the retry policy you configure.
Dashboard-only management
The Problem: Endpoints can't be created from infrastructure-as-code or a deploy script.
Why It Happens: There is no CLI or API-key REST route for endpoints.
Workarounds:
- Document endpoints and their event subscriptions alongside your infrastructure, and keep their number small.
How Hookdeck Can Help: With one endpoint pointed at Hookdeck, routing to services and environments becomes connections and filters you can change through Hookdeck's API.
Best practices
Skip test deliveries in business logic
Test deliveries carry a webhook-test: true header that real events don't have. Verify them, answer 200, and don't trigger alerts.
Handle spend events once
org.trial_ended and the spend.* events are generated by periodic jobs that carry an idempotency key, so overlapping runs don't send duplicates. Retries can still repeat a message, so key on svix-id as well; our idempotency guide shows how.
Read fields defensively
Optional fields such as transport may be missing, nullable ones may be null, and session.started names the service differently. Validate before use.
Answer fast
Acknowledge as soon as the signature checks out and do slow work elsewhere, so a slow dependency doesn't turn into a wave of retries.
Conclusion
Pipecat Cloud's webhooks cover the life of a hosted agent, from build to deployment to spend. Verify the svix-* headers against each endpoint's whsec_ secret, disable rather than delete endpoints, ignore test deliveries, and don't rely on the best-effort events.
To keep alerts through outages and route them to several services from a single endpoint, put Hookdeck Event Gateway in front. Try it free with a Pipecat Cloud source.