> ## Documentation Index
> Fetch the complete documentation index at: https://docs.orbit.devotel.io/llms.txt
> Use this file to discover all available pages before exploring further.

# Program an outbound voice call: answer_url, verbs, and transcript

> Originate a call with POST /api/v1/voice/calls, drive it with the verbs your answer_url returns, stream DTMF through it, and land on a recording plus a readable transcript.

# Program an outbound voice call

`POST /api/v1/voice/calls` originates a call, and the programmable voice
surface decides what that call does once the far end answers. You have two
options, picked per call:

* **answer\_url** — an HTTPS endpoint on your server. When the call answers,
  Orbit POSTs the call metadata to it and expects a JSON list of **verbs**
  (`say`, `play`, `gather`, `dial`, `hangup`, …) that runs the call — the
  Twilio `voiceUrl` / Vonage `answer_url` model, applied to outbound.
* **staticVerbs** — the verb list embedded in the create request itself
  (TwiML-Bin style), for flows that never change between calls.

Both run through the same verb engine documented in the
[Programmable Voice DSL reference](/reference/programmable-voice-dsl); this
guide walks the whole loop — provision a number, place the call, serve the
verbs, track progress, then fetch the recording and the transcript.

<Note>
  Every outbound leg terminates through the Devotel softswitch. You cannot
  route outbound voice through Telnyx, DIDWW, or an arbitrary carrier — a
  carrier-selection field a returned verb carries is ignored.
</Note>

## 1. Provision a voice-capable number

You need one owned DID for the caller ID. If you do not already have one,
work through [Buy and provision numbers](/guides/buy-numbers) — search
inventory with `capabilities=sms,voice`, purchase, and wait until the order
reports the line provisioned.

Two distinct models sit on that number:

* **Inbound** — a caller dials your DID and the number's inbound route
  decides what happens. Set it to an answer\_url route if you want the same
  verb-driven treatment inbound.
* **Outbound** — nothing to configure on the number. You pass it as `from`
  on `POST /api/v1/voice/calls`, and the call enters the verb surface the
  moment the far end picks up.

## 2. Place the call

Send `to`, optionally `from` (falls back to a tenant default when omitted),
and one of the two control modes. Also accepted here: `record` (capture the
call audio), `amd` (carrier-side answering-machine detection — judge the
result server-side before you deliver a message meant for a human),
`metadata` (an arbitrary JSON object persisted on the call row and echoed
back in every webhook), plus `campaignId` and `orgTimezone` for spend-cap
and quiet-hours bookkeeping.

```bash cURL theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/calls \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "from": "+16572262362",
    "answer_url": "https://app.example.com/voice/outbound-answer",
    "answer_method": "POST",
    "answer_fallback": "safe-default",
    "record": true,
    "amd": true,
    "metadata": { "order_id": "ord_8842" }
  }'
```

```javascript Node SDK — @devotel/orbit theme={null}
import { Orbit } from "@devotel/orbit";

const orbit = new Orbit({ apiKey: "dv_test_sk_YOUR_KEY" });

const { data: call } = await orbit.voice.calls.create({
  to: "+14155552671",
  from: "+16572262362",
  record: true,
  amd: true,
  metadata: { order_id: "ord_8842" },
});
// call.id ("call_…") — the handle for status polling, hangup, recordings.
```

```python Python SDK — orbit-sdk theme={null}
from orbit_sdk import OrbitClient

client = OrbitClient.from_api_key("dv_test_sk_YOUR_KEY")

call = client.voice.create(
    to="+14155552671",
    from_="+16572262362",
    record=True,
    amd=True,
    metadata={"order_id": "ord_8842"},
)
call_id = call["data"]["id"]
```

A `2xx` returns the created call in the standard envelope; `data.id` is the
`call_…` handle every later step uses.

**answer\_url or staticVerbs?** They are mutually exclusive — the API rejects
a request carrying both, and a request carrying neither places a plain call
with no programmable control. Reach for `staticVerbs` when the flow is
fixed; reach for `answer_url` the moment the flow depends on the callee,
your data, or input collected mid-call:

```bash cURL — staticVerbs variant theme={null}
curl -X POST https://api.orbit.devotel.io/api/v1/voice/calls \
  -H "X-API-Key: dv_test_sk_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "to": "+14155552671",
    "record": true,
    "staticVerbs": [
      { "verb": "say", "text": "Your appointment is confirmed for Friday." },
      { "verb": "hangup" }
    ]
  }'
```

`answer_fallback` governs what the call does when your `answer_url` cannot
be served — `safe-default` (a short apology then hangup), `voicemail`, or
`decline`. Between this and the
[production guardrails](#6-production-guardrails) below, that is the whole
reliability contract of the surface.

## 3. Implement the answer\_url handler

The full handshake — the request body Orbit sends, the response shapes, and
every verb — is in the
[Programmable Voice DSL reference](/reference/programmable-voice-dsl). In
short: Orbit POSTs `{ callSid, from, to, dialedE164, routeId, organizationId }`
to your URL, and your handler must reply with JSON — either
`{ "verbs": [...] }` or a bare verb array — within the fetch budget (5s
default, 10s hard cap). On timeout, non-2xx, non-JSON, or an empty array,
the `answer_fallback` you set on the route or the call runs instead, so a
callee never lands on dead air.

<Warning>
  **Verify the signature before you serve verbs.** Configure a signing secret
  on the route, and Orbit signs every request to your answer\_url with
  HMAC-SHA256 in `X-Devotel-Timestamp` / `X-Devotel-Signature`. Verify against
  the **raw** request body — re-serializing parsed JSON breaks the comparison.
  Hash the raw bytes, reject timestamps older than a few minutes, and answer
  `401` on mismatch. This scheme is separate from customer-webhook
  signatures; the exact contract is in
  [Verifying the request came from Orbit](/reference/programmable-voice-dsl#verifying-the-request-came-from-orbit).
</Warning>

A complete minimal handler — greet, play a file, bridge to a PSTN
destination, hang up:

```javascript Express theme={null}
import crypto from "node:crypto";
import express from "express";

const app = express();
const ANSWER_SECRET = "your_route_signing_secret";

app.post("/voice/outbound-answer", express.raw({ type: "*/*" }), (req, res) => {
  const timestamp = req.headers["x-devotel-timestamp"];
  const signature = req.headers["x-devotel-signature"];
  const raw = req.body.toString("utf-8");

  const expected =
    "sha256=" +
    crypto.createHmac("sha256", ANSWER_SECRET).update(`${timestamp}.${raw}`, "utf-8").digest("hex");
  const ok =
    signature &&
    expected.length === String(signature).length &&
    crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(String(signature)));
  if (!ok) return res.status(401).json({ error: "Invalid signature" });

  const call = JSON.parse(raw); // { callSid, from, to, dialedE164, … }

  res.json({
    verbs: [
      { verb: "say", text: "Hello — this is Acme confirming your appointment." },
      { verb: "play", url: "https://cdn.example.com/outbound/details.wav" },
      {
        verb: "dial",
        callerId: "+16572262362",
        timeout: 25,
        target: [{ type: "phone", number: "+14155550123" }],
      },
      { verb: "hangup", reason: "Call complete" },
    ],
  });
});

app.listen(3000);
```

**Branch on collected DTMF.** `gather` collects input mid-call and POSTs
the result to its `actionHook`, which returns the next verb list — the
IVR-menu primitive over the same channel:

```javascript Express — DTMF branch theme={null}
app.post("/voice/outbound-answer", (req, res) => {
  // …signature checks as above…
  res.json({
    verbs: [
      { verb: "say", text: "Press 1 to confirm the appointment, or 2 to reach an agent." },
      {
        verb: "gather",
        input: ["digits"],
        numDigits: 1,
        timeout: 8,
        actionHook: "https://app.example.com/voice/appointment-choice",
      },
    ],
  });
});

app.post("/voice/appointment-choice", (req, res) => {
  // …same signature check; the body carries the pressed digit(s)…
  const digits = req.body.digits ?? "";
  if (digits === "1") {
    return res.json({
      verbs: [
        { verb: "say", text: "Confirmed. Thank you." },
        { verb: "hangup", reason: "Appointment confirmed" },
      ],
    });
  }
  return res.json({
    verbs: [
      { verb: "dial", callerId: "+16572262362", timeout: 30, target: [{ type: "phone", number: "+14155550199" }] },
      { verb: "hangup" },
    ],
  });
});
```

The full surface — `gather` with speech input, `record` (single-file capture
with its own consent obligations), `enqueue`, `transfer`, `listen` (fork the
live audio to a WebSocket for transcription or a voice agent), and the
reserved `conference` / `config` verbs — is documented verb by verb in the
[DSL reference](/reference/programmable-voice-dsl#verbs).

## 4. Track progress

Every call moves through `ringing` → `in_progress` → a terminal
`completed` or `failed` (a ring that never answers ends as `no-answer`,
reported in the same terminal stream). Poll the call record…

```bash cURL theme={null}
curl https://api.orbit.devotel.io/api/v1/voice/calls/call_9f2a1c \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

```javascript Node SDK theme={null}
const { data: call } = await orbit.voice.calls.get("call_9f2a1c");
// call.status — ringing | in_progress | completed | failed
```

…or — in production — subscribe to the terminal events. Create a webhook
endpoint in **Settings → Webhooks**, select `call.completed` and
`call.failed`, and [verify the delivery signature](/guides/verify-webhook-signatures)
before trusting the body. Each event carries the call handle, direction,
final status, duration, and the machine-readable cause (`hangup_reason` +
the SIP code); your `metadata` from step 2 is echoed back so you can
correlate straight to your own record:

```json theme={null}
{
  "type": "call.completed",
  "data": {
    "call_id": "call_9f2a1c",
    "status": "completed",
    "duration_seconds": 86,
    "metadata": { "order_id": "ord_8842" }
  }
}
```

## 5. Fetch the recording and the transcript

Pass `record: true` at create time and the call's media lands as a
recording once the call is terminal. Subscribe to
`recording.completed` instead of polling; when the event lands the
download URL on the call row is ready:

```bash cURL — recording metadata + signed download URL theme={null}
curl https://api.orbit.devotel.io/api/v1/voice/recordings?call_id=call_9f2a1c \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"

curl https://api.orbit.devotel.io/api/v1/voice/recordings/rec_45de/download-url \
  -H "X-API-Key: dv_test_sk_YOUR_KEY"
```

```javascript Node SDK — finalized transcript theme={null}
const { data: transcript } = await orbit.voice.transcripts.get("call_9f2a1c");
// GET /api/v1/voice/calls/{id}/transcript
```

```python Python SDK theme={null}
transcript = client.request("GET", "/voice/calls/call_9f2a1c/transcript")
```

For a call you want to read as it happens, `GET
/api/v1/voice/calls/{callId}/transcript/stream` streams partial and final
transcript frames over SSE for the life of the call — one SDK call in Node:

```javascript Node SDK — live transcript stream theme={null}
const handle = orbit.voice.calls.streamTranscript("call_9f2a1c", {
  onPartial: (frame) => process.stdout.write(frame.text),
  onFinal: (frame) => console.log("\nfinal:", frame.text),
  onError: (err) => console.error(err),
});
// handle.close() to tear down.
```

## 6. Production guardrails

* **Concurrency.** Pace outbound originations per number and per API key —
  watch live usage and headroom in the
  [API rate limits & quotas console](/guides/api-rate-limits-console) and hold
  your originator below it; a `429` is the limiter telling you it is already
  over.
* **Voicemail detection.** `amd: true` at create time asks the carrier to
  flag machine pickups — treat an AMD-flagged answer as a distinct branch
  (different prompt or a scheduled retry), never as a human-answered call.
* **STIR/SHAKEN.** Devotel signs every outbound leg; what you own is the
  attestation policy, branded calling, and CNAM — see
  [STIR/SHAKEN attestation](/channels/voice/stir-shaken).
* **Recording consent.** Route-level capture and the DSL `record` verb have
  different consent gates — read the warning in the
  [DSL reference](/reference/programmable-voice-dsl#record) before you turn
  either on.
* **Fallback is your seatbelt.** Pick an explicit `answer_fallback` per call
  instead of inheriting `safe-default` silently — a flow with no fallback
  story is a flow that apologizes to your callees when your handler hiccups.

## 7. Failure modes

Terminal failures surface through `call.failed` and on the call record.
Map the common ones:

| Outcome                           | `status`                   | Typical `hangup_reason`        | What to do                                                                                       |
| --------------------------------- | -------------------------- | ------------------------------ | ------------------------------------------------------------------------------------------------ |
| No answer                         | `no-answer`                | `no_user_response` (SIP 480)   | Retry on a schedule you own, or leave it.                                                        |
| Busy                              | `failed`                   | `user_busy` (SIP 486)          | Redial later — the callee declined or was on another call.                                       |
| Unreachable / invalid destination | `failed`                   | SIP 404/503/604, network error | Clean the number out of your dial list — retries burn spend.                                     |
| answer\_url failed                | (fallback drives the call) | —                              | A `safe-default`/`voicemail`/`decline` fallback means the call still gets a sane path — ship it. |

Consume these from the webhook events and let them feed disposition codes
back into your list — the same signal the
[outbound dialer campaign](/guides/outbound-dialer-campaign) surface paces
with.

## 8. Next steps

* **Visual IVR for the inbound side** — when the same verb logic should be
  authored, validated, and versioned without JSON, rebuild it in the
  [IVR builder](/guides/build-ivr-flow).
* **Outbound at scale** — when you graduate from one-off calls to a paced
  dial list with disposition codes, move to the
  [outbound dialer campaign](/guides/outbound-dialer-campaign) surface.

The reference material this guide stood on: the
[Programmable Voice DSL reference](/reference/programmable-voice-dsl), the
[OpenAPI spec](/openapi) for `POST /api/v1/voice/calls`, and the recipe
collection in [API recipes](/guides/api-recipes#11-place-an-outbound-voice-call).
