---
title: "Use application data in a voice sequence"
description: "Ask an HTTPS receiver for typed data during a call, branch on it in a later step, handle technical failures explicitly, and keep private responses separate from retained call data."
canonical: "https://bird.com/docs/guides/voice/sequences/code/webhooks"
---

# Use application data in a voice sequence

Use `voice.webhook` when your application holds data the call needs: an order status, a team, or an appointment confirmation. The step returns that data on `success`; a later Branch step chooses the path from it.

## Define the response

Set `config.output_schema` to an object schema for the returned data. The step has two outputs: `success` carries data in that schema and `failure` handles technical problems. Omitted configuration means empty-object output.

Input contains a static HTTPS `url` and optional object `data`. Data supports a literal object, `$literal`, or a whole-object `$expr`. Dynamic URLs, authored headers, and credentials are not supported.

## Verify requests

Bird signs every request, continuations included, with your workspace callback secret using [Standard Webhooks](https://www.standardwebhooks.com/) headers: `webhook-id`, `webhook-timestamp` and `webhook-signature`. The signature covers the ID, the timestamp and the exact body. During the 24 hours after a secret rotation, `webhook-signature` carries one `v1,` entry per active secret, so either secret verifies. Calls can take about a minute to start signing with a new secret, so keep accepting the previous one until the overlap ends. Your response is not signed: Bird validates your receiver's TLS certificate, and the HTTPS connection authenticates what it returns.

Verify the headers over the raw body with a Standard Webhooks library's verify call before parsing. The SDKs' `webhooks.unwrap` parses platform events and is not for these requests. Rotate the secret under **Webhooks → Direct callbacks** first, because the initial one is never shown. The signature shows that a holder of your workspace secret signed the request. Your SMS callback receivers hold the same secret and can sign too, so keep it only on receivers you trust. Anyone who can edit the workspace's sequences chooses the URL and `data`. Check that `target_url` is your own URL to refuse a request Bird signed for another receiver, and authorize business actions in your receiver. Customer authorization is never forwarded.

Sequence webhook requests do not present a client certificate, even when your workspace enforces mTLS. Configure the receiver to accept a connection without a client certificate and authenticate the signed request instead.

Check the signed `webhook-timestamp`, a Unix timestamp in seconds, against a synchronized server clock. Reject requests more than five minutes in the past or future. Deduplicate on `webhook-id` before performing side effects. Store the ID and result atomically with the action where possible, and return the saved result for a repeated request. Each continuation has its own ID. Use a separate business idempotency key if distinct requests must not repeat the same action.

## Return data

The receiver returns this closed response shape:

```json
{
  "schema_version": 1,
  "type": "outcome",
  "output": { "status": "confirmed", "message": "Your appointment is confirmed." }
}
```

`output` must match the configured schema. The response returns data, not a port, node ID or new commands. A continuation is `{"schema_version":1,"type":"continue","url":"https://example.com/voice/next"}` on the original URL's canonical origin.

To route on the data, connect `success` to a `logic.branch` step with cases such as `steps.decision.output.status == 'confirmed'`. Declare the field as a string `enum` in the schema so the receiver can return only those values.

The request contains run/session/call identity, admitted sequence and node identity, your materialized `data`, and `results`. Keep business input in `data`; a direct private Gather result may appear once in `results`, and settled recordings carry an ID when available. These observations are not automatically expression fields.

## Read the receiver request

A draft test run sends this shape. IDs and timestamps below are fictional; the actual request uses the admitted run and current node occurrence:

```json
{
  "schema_version": 1,
  "method": "POST",
  "target_url": "https://example.com/decision",
  "run_id": "vsr_01k5yb7p000000000000000000",
  "sequence": {
    "id": "vsq_01k5yb7p000000000000000000",
    "version_id": null,
    "draft_revision": 1,
    "entry_node_id": "start",
    "mode": "test"
  },
  "session_id": "vcs_01k5yb7p000000000000000000",
  "call": { "id": "vcl_01k5yb7p000000000000000000", "orig": null, "dest": null },
  "node": { "id": "decision", "occurrence_id": "00000000-0000-4000-8000-000000000001" },
  "data": { "appointment_id": "appointment_123" },
  "results": []
}
```

For a published run, `version_id` identifies the selected version and `draft_revision` is null. For a call that ran an [inline definition](/docs/guides/voice/create-calls), `id`, `version_id` and `draft_revision` are all null. `mode` can be `test` or `production`. Origin and destination use nullable call-party objects. Verify the signature before trusting these identity fields.

A Gather observation in `results` has `type: "gather"`, `node_id`, UUID `command_id`, `digits`, and `reason`. A recording observation has `type: "recording"`, UUID `command_id`, `state`, and a `recording_id` when available. Consume private digits only for the intended request; they do not become retained expression outputs. Continuation requests keep the same node occurrence, with already-delivered observations removed from `results`.

## Handle failures

A technical sample uses the `failure` port with `{"code":"webhook_unavailable"}` or `{"code":"webhook_invalid_response"}`. A connected failure follows recovery; an unconnected one fails the run. An unconnected `success` ends the run naturally. Live webhook requests have a three-second deadline and are not retried automatically. Preview makes no request.

The three-second deadline includes reading the signing secret. Connect `failure` to a recovery path so the call can continue when the receiver is unavailable or returns an invalid response. A timed-out request may already have caused a side effect at the receiver.

Treat `data`, call identity, and Gather digits as private. Restrict access to the receiver and secret, avoid logging bodies or secrets, and retain caller input only as long as your application needs it. Do not put private data or credentials in URLs: the call trace retains the full URL, including its query. Authored URL and data and validated output are retained in run history; transient Gather digits are excluded from captured step input. Return only data the sequence needs.

Read returned data at `steps.decision.output`; read technical errors at `steps.decision.error.code`. If both paths meet, guard the wrapper with `has(steps.decision.output)` or `has(steps.decision.error)` before using it. An HTTP 200 from your receiver does not establish a valid outcome until its body passes the contract.

## Related resources

- [Voice sequences](/docs/guides/voice/sequence-builder) (docs)
