# Guide to Pipecat Cloud Webhooks: Features and Best Practices

[Pipecat Cloud](https://pipecat.daily.co/) 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](https://docs.pipecat.ai/pipecat-cloud/guides/webhooks) 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](https://docs.pipecat.ai/pipecat-cloud/guides/webhook-events):

| 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](https://github.com/hookdeck/webhook-samples/blob/main/providers/pipecat-cloud/latest/build.succeeded.json) has this `build.succeeded` example:

```json
{
  "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 `_id` and `_name` pairs, such as `service_id` and `service_name`. `session.started` is the exception and sends `service`.
* Money is always an integer number of cents in a `*_cents` field.
* 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-id` header 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.

1. In the [Pipecat Cloud dashboard](https://pipecat.daily.co/), open **Settings > Webhooks** for your organisation.
2. Click **Add endpoint** and enter the URL, for example `https://example.com/webhooks/pipecat-cloud`. Only `http` and `https` URLs are accepted; use HTTPS for anything on the internet.
3. Select event types. At least one is required, and only subscribed types are delivered. `build.succeeded`, `build.failed`, `deployment.updated`, `service.suspended` and `spend.limit_reached` make a sensible first set.
4. Use the key icon on the endpoint's row to reveal the `whsec_` secret, and store it as `PIPECAT_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](/docs/cli):

```bash
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:

```javascript
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:

```javascript
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](/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](/docs/retries) 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](/docs/connections) and [filters](/docs/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](/webhooks/guides/implement-webhook-idempotency) 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](https://hookdeck.com) in front. [Try it free](https://dashboard.hookdeck.com/signup) with a Pipecat Cloud source.