Sign inGet Started

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

Code example
{
  "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:

Code example
{
  "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, 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.

Continue with the documentation, guides and examples for this topic.