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

# Dialer API

> Configure the OrbitForms power dialer from code or an AI agent: settings and caller IDs, call lists built from saved views, call scripts, calls, transcripts, AI feedback, outcomes and statistics, and the do-not-call list. Calls are only ever placed by a person.

The dialer lets a salesperson work through a call list from the browser, one contact after the next. This API covers everything around that: an agent or script can **configure** the dialer, **build call lists** (they are saved views of the Contacts table), **write the call scripts** a rep reads on a call, **read** sessions, calls, transcripts, AI feedback, outcomes and statistics, **set** outcomes and notes on finished calls, and **manage the do-not-call list**. The same endpoints power the dialer tools of the [MCP server](https://docs.useorbitcrm.ai/mcp/tools.md).

> [!WARNING]
>
> **The API never places a call, buys a number or changes billing.** There is no endpoint to start a dialing session, dial a number, obtain a calling token, buy or release a phone number, or accept the Dialer terms, and no scope grants one. Those actions exist only inside the dashboard for a signed-in person (billing changes: an admin in Settings → Dialer). An agent prepares the list, assigns numbers and reads the results and the costs; a person makes the calls. Settings can still change what those calls cost: a `dialer:write` credential can turn `transcribe_calls` on, which bills every transcribed minute of later calls ([pricing](#phone-numbers-and-pricing)), and `ai_call_feedback`, which uses one AI credit per call.

## Scopes

| Scope | Allows |
| - | - |
| `dialer:read` | Read dialer settings, the workspace's phone numbers, its calling costs and Dialer terms acceptance, preview call lists, list sessions, calls, call transcripts and AI feedback, statistics, call scripts, voicemail greetings and the do-not-call list |
| `dialer:write` | Change dialer settings (including call recording, paid transcription and AI feedback), assign and label phone numbers, set call outcomes and notes, permanently delete call recordings and transcripts, create, change and delete call scripts, add to and remove from the do-not-call list |
| `crm:read` | Also accepted by the call-list preview, since call lists are saved contact views; with `crm:read` alone the preview returns counts only, no `sample` contacts |

Changing `caller_id`, assigning or labelling phone numbers, deleting a call's recording and transcript, and removing numbers from the do-not-call list additionally need a credential whose owner is a team admin (`403 forbidden` otherwise). The **CRM agent** API key preset in [Settings → API keys](https://app.useorbitcrm.ai/settings/api-keys) includes both dialer scopes.

## Endpoints

| Method | Endpoint | Description |
| - | - | - |
| `GET` | `/api/v1/dialer/settings` | The team's dialer settings (defaults when none are saved) |
| `PATCH` | `/api/v1/dialer/settings` | Change any subset of the settings below; unknown keys are rejected |
| `GET` | `/api/v1/dialer/caller-ids` | The values `caller_id` accepts: the workspace's own numbers in caller-ID form (`phone_number`, `friendly_name`, `kind`). `503 unavailable` when calling is not configured |
| `GET` | `/api/v1/dialer/numbers` | `{ data: { numbers, members } }`: the phone numbers the workspace owns, oldest first, and a map of assignee IDs to name and email. No `POST`: numbers are bought in the dashboard |
| `GET` | `/api/v1/dialer/numbers/:id` | One number: `phone_number`, `number_type` (`local`, `toll_free`), `friendly_name`, `country`, `capabilities`, `assigned_user_id`, `status`, `monthly_price_cents`, purchase dates, the incoming-call settings and `forwarding` (whether unanswered calls are forwarded; the forwarding number itself is never returned to API clients) |
| `PATCH` | `/api/v1/dialer/numbers/:id` | `{ assigned_user_id?: uuid \| null, friendly_name?: string }`: hand the number to a rep, take it back with `null`, or relabel it. Admins only; `409 conflict` when that person already holds a number. No `DELETE`: numbers are released in the dashboard |
| `GET` | `/api/v1/dialer/billing` | What the workspace pays for calling: `numbers` by type and their `monthly_cents`, billable `minutes` this month and their cost, plus `pending_minutes` / `pending_cents` for minutes not yet reported to billing, `transcript_minutes` (minutes transcribed and billed this month, their cost and `per_minute_cents`, plus `pending_minutes` / `pending_cents`), list `prices` (including `transcript_minutes_cents`), `feedback` (AI call feedback written this month: `calls`, the AI `credits` they used and `per_call_credits`), `monthly_total_cents` (numbers only; metered minutes and transcript minutes excluded), the `subscription` record (`status`: `none`, `active`, `canceling`, `past_due`; `cancel_at`; `current_period_end`), the `terms` record (version, path, who accepted it and when, `current`), all in cents, and whether purchase is `configured` |
| `GET` | `/api/v1/dialer/call-lists/preview?view_id=` | What a session built from a saved view (or, with `list_id=`, a static list of contacts) would dial: `matching`, `examined`, `dialable`, `skipped` by reason, `truncated`, and a `sample` of the first contacts in order (only for credentials that also hold `contacts:read` or `dialer:read`). Only numbers in the United States are dialable |
| `GET` | `/api/v1/dialer/sessions` | Dialing sessions, newest first (`user_id`, `status`: `active`, `paused`, `completed`, `abandoned`; `page`, `per_page`). Read-only: there is no `POST` |
| `GET` | `/api/v1/dialer/sessions/:id` | `{ data: { session, items } }`; the session carries the `script_id` it was started with; each item has the contact, `phone_e164`, `status` (`queued`, `calling`, `done`, `skipped`), `skip_reason`, `last_call_id` and `outcome` |
| `GET` | `/api/v1/dialer/calls` | Calls, newest first: the team's dials and calls to its numbers (`contact_id`, `session_id`, `user_id`, `outcome`, `direction` = `outbound` or `inbound`, `since`, `until`, `page`, `per_page`). Inbound calls carry `inbound_result` (`answered`, `forwarded`, `voicemail`, `missed`) and `forwarded_to`; their `from_number` is the caller. Each call carries an `insights` summary (`transcript_status`, `feedback_status`), or `null` when nothing was requested |
| `GET` | `/api/v1/dialer/calls/:id` | One call: numbers, `status`, `answered_by`, `outcome`, `notes`, timings, `duration_seconds`, recording metadata and the `insights` summary |
| `PATCH` | `/api/v1/dialer/calls/:id` | `{ outcome?, notes?, callback_at? }`; `outcome` is one of the workspace's [outcomes](#outcomes) (`400 unknown_outcome` otherwise, `400 outcome_hidden` for one an admin hid). `do_not_call` also adds the dialed number to the do-not-call list. `callback` with `callback_at` (an ISO 8601 date-time with its UTC offset, `Z` or `±hh:mm`, in the next year) creates a `call` task for the contact at that time (a `todo` if an admin hid Call), assigned to whoever made the call, and returns it as `callback_task`; sending another `callback_at` moves that same task while it is open. `callback_at` also needs the `tasks:write` scope (`403 insufficient_scope` otherwise). Nothing is dialed |
| `GET` | `/api/v1/dialer/outcomes` | `{ data: { outcomes, require_outcome } }`: the workspace's outcomes in picker order (see [Outcomes](#outcomes)). Read-only: the list is edited in Settings → Dialer |
| `GET` | `/api/v1/dialer/calls/:id/insights` | The call's transcript and AI feedback (see [Transcripts and AI feedback](#transcripts-and-ai-feedback)). `404 not_found` until a transcript was requested for the call |
| `DELETE` | `/api/v1/dialer/calls/:id/recording` | Deletes the call's recording and transcript at the providers and keeps the call (see [Deleting recordings](#deleting-recordings)). Admins only; `409 conflict` when the call has neither. Returns the call with `insights: null` |
| `GET` | `/api/v1/dialer/stats` | Statistics for a window (`since`, `until`, default the last 30 days; `user_id`, `session_id`): calls, reached, reach rate, voicemails, talk time, and breakdowns by outcome, person and day |
| `GET` | `/api/v1/dialer/scripts` | The team's call scripts, the default first then by name (see [Call scripts](#call-scripts)) |
| `POST` | `/api/v1/dialer/scripts` | `{ name, content, is_default?, questions?, show_on? }` → the new script (`201`) |
| `GET` | `/api/v1/dialer/scripts/:id` | One script with its full `content` |
| `PATCH` | `/api/v1/dialer/scripts/:id` | `{ name?, content?, is_default?, questions?, show_on? }` (at least one). `is_default: true` takes the flag off the previous default; `409 conflict` when two callers race for it |
| `DELETE` | `/api/v1/dialer/scripts/:id` | Removes the script. Sessions and call insights that used it keep working; their `script_id` becomes `null` |
| `GET` | `/api/v1/dialer/voicemails` | Pre-recorded voicemail greetings, metadata only (`user_id`). Audio is uploaded and played from the dashboard |
| `GET` | `/api/v1/do-not-call` | The do-not-call list (`search` matches digits or the source email, `reason`, `page`, `per_page`) |
| `POST` | `/api/v1/do-not-call` | `{ phones: string[] \| string, reason?, note?, contact_id? }` → `{ added, already_listed, invalid }` (`201` when anything was added) |
| `DELETE` | `/api/v1/do-not-call` | `{ phones?: string[], ids?: string[] }` → `{ removed }`. Admins only |

### Settings

Defaults apply until a team saves its own. `caller_id` is the workspace's **default** outbound number; it must be one of the numbers the workspace owns. `caller_id` and `require_call_outcome` are the settings a non-admin credential cannot change.

| Setting | Type | What it does |
| - | - | - |
| `caller_id` | E.164 string \| null | The workspace's default outbound number, used by anyone without a number assigned to them. Must be one of the phone numbers the workspace owns (`GET /api/v1/dialer/numbers`; bought in Settings → Dialer). Admins only. The first number a workspace buys becomes the default automatically |
| `ring_timeout_seconds` | integer 5–60 | Seconds to let a number ring before treating it as no answer and moving on. Default 25 |
| `auto_advance_delay_seconds` | integer 0–60 | Seconds between one call ending and the next contact being dialed. Default 3 |
| `wrap_up_seconds` | integer 0–300 | After a conversation, seconds the outcome picker waits before saving `connected` and moving on; 0 waits for the person. Default 20 |
| `voicemail_mode` | `live` \| `drop` \| `hangup` | What happens when a call reaches voicemail: `live` (the person hears it and decides), `drop` (play their default pre-recorded greeting after the beep, then move on) or `hangup` (hang up as soon as a machine is detected, then move on). Default `live` |
| `machine_detection` | boolean | Detect answering machines. Required for `drop` and `hangup` and for `transcribe_calls`; when off, every answered call is treated as a person. Default `true` |
| `record_calls` | boolean | Record calls from the moment they are answered (dual channel); recordings are listed on each call. Default `true` |
| `recording_announcement` | boolean | Play a short notice to the called party when they answer ("This call may be recorded for quality and training purposes") before connecting them. Only plays when `record_calls` is on. Helps with all-party-consent recording laws; it does not replace your own consent obligations. Default `false` |
| `skip_recently_called_hours` | integer 0–720 | Leave contacts called within this many hours out of new sessions; 0 never skips. Default 24 |
| `max_attempts_per_contact` | integer 1–50 \| null | Leave contacts with at least this many finished calls out of new sessions; `null` means no limit. Default `null` |
| `default_country` | ISO 3166-1 alpha-2 | Country assumed for phone numbers stored without a country code. Default `US` |
| `transcribe_calls` | boolean | Transcribe recorded conversations (speaker-labelled) once the call ends. Needs `record_calls` and `machine_detection`. Voicemail and unanswered calls are never transcribed. Default `true` |
| `ai_call_feedback` | boolean | After each transcribed conversation, have the AI coach write feedback for the rep: summary, what went well, what to improve, objections and next steps. Needs `transcribe_calls`. Each feedback uses one of the workspace's AI credits; when none are left the call's `feedback_status` is `skipped`, not failed. Default `false` |
| `ai_feedback_focus` | string \| null | Optional instructions for the AI coach: your sales methodology, products, what to look for. Up to 2,000 characters; blank or `null` clears it. Default `null` |
| `require_call_outcome` | boolean | After every conversation the rep must pick an outcome before moving on: the wrap-up timer no longer saves `connected` by itself. Calls that end without a conversation still get their automatic outcome. Admins only. Default `false` |

The transcript settings form a chain: `transcribe_calls` needs both `record_calls` and `machine_detection` on (answering-machine detection is what keeps voicemail out of transcripts), and `ai_call_feedback` needs `transcribe_calls` on. A patch that breaks the chain is rejected with `400 invalid_request` and a message naming the fix; turning a parent off never silently turns its dependents off, so send them together: `{ "record_calls": false, "transcribe_calls": false, "ai_call_feedback": false }` to stop recording, or `{ "machine_detection": false, "transcribe_calls": false, "ai_call_feedback": false }` to stop detecting answering machines. The reverse holds too: `{ "transcribe_calls": true, "machine_detection": true }` turns transcription on where detection was off.

```bash theme={null}
curl -X PATCH https://app.useorbitcrm.ai/api/v1/dialer/settings \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "voicemail_mode": "drop", "machine_detection": true, "skip_recently_called_hours": 48 }'
```

### Outcomes

Every finished call carries one outcome. The system sets the automatic ones from how the call ended; a person (or `PATCH /api/v1/dialer/calls/:id`) picks a manual one after a conversation. `reached` outcomes count toward the reach rate in statistics.

Each workspace starts with the fourteen built-in outcomes below. In Settings → Dialer an admin can rename the manual ones, give them descriptions, hide them, change their order and add the workspace's own. **Keys never change**: a renamed outcome keeps its key, and a custom one gets a `custom_` key generated from its first name (`Left with gatekeeper` → `custom_left_with_gatekeeper`) that stays the same through every rename, so calls, the `call.completed` webhook, statistics and filters keep matching. Outcomes are hidden rather than deleted: calls that already have one keep it, but it can no longer be set (`400 outcome_hidden`). `connected` and `do_not_call` are always available. Each custom outcome says whether it counts as reaching the contact (`reached`), which the reach rate in statistics uses. An admin can also require an outcome after every conversation (`require_call_outcome`).

`GET /api/v1/dialer/outcomes` (MCP: `list_call_outcomes`) returns the workspace's list:

```json theme={null}
{
  "data": {
    "require_outcome": false,
    "outcomes": [
      { "key": "connected", "label": "Spoke to them", "description": "Spoke with the contact", "kind": "manual", "reached": true, "hidden": false, "built_in": true, "position": 0 },
      { "key": "custom_left_with_gatekeeper", "label": "Left with gatekeeper", "description": null, "kind": "manual", "reached": false, "hidden": false, "built_in": false, "position": 1 },
      { "key": "no_answer", "label": "No answer", "description": "Rang until the ring timeout", "kind": "auto", "reached": false, "hidden": false, "built_in": true, "position": null }
    ]
  }
}
```

The built-in outcomes:

| Value | Label | Set by | Meaning |
| - | - | - | - |
| `no_answer` | No answer | auto | Rang until the ring timeout |
| `busy` | Busy | auto | Busy signal |
| `failed` | Failed | auto | The number could not be reached (invalid or unroutable) |
| `canceled` | Canceled | auto | Hung up before the contact answered |
| `voicemail` | Voicemail | auto | Reached voicemail; no message left |
| `voicemail_dropped` | Voicemail dropped | auto | A pre-recorded greeting was left |
| `voicemail_left` | Voicemail left | manual | The caller left a message in person |
| `connected` | Connected | manual | Spoke with the contact (reached) |
| `interested` | Interested | manual | Spoke with the contact; they want to continue (reached) |
| `not_interested` | Not interested | manual | Spoke with the contact; they declined (reached) |
| `callback` | Call back | manual | Asked to be called again later (reached) |
| `meeting_booked` | Meeting booked | manual | A meeting was scheduled on the call (reached) |
| `wrong_number` | Wrong number | manual | The number does not belong to the contact |
| `do_not_call` | Do not call | manual | Asked not to be called again; the number goes on the do-not-call list (reached) |

## Phone numbers and pricing

The Dialer is **part of every plan**; there is no flat fee. A workspace needs a phone number to call from: an admin buys numbers in **Settings → Dialer** (also reachable from Billing), and Orbit's platform phone account buys each one on the workspace's behalf. The first purchase asks the admin to accept the Dialer terms at [`/dialer-terms`](https://useorbitcrm.ai/dialer-terms). Numbers and outbound minutes are charged on a separate **Orbit Dialer** subscription next to the workspace's plan, on the same card; the first number creates it.

| Item | Price | How it is billed |
| - | - | - |
| Local number | \$5 / month | Monthly rental; the first number starts the workspace's monthly Dialer billing cycle, later ones are prorated to it |
| Toll-free number | \$10 / month | Monthly rental; the first number starts the workspace's monthly Dialer billing cycle, later ones are prorated to it |
| Outbound calling | \$0.02 / minute | Per started minute of each answered call, metered and added to the next invoice |
| Incoming calls | $0.02 / minute ($0.04 on a toll-free number) | Per started minute the caller is on the line (ringing the rep, the greeting and the voicemail included), metered with outbound minutes; a toll-free minute counts as two calling minutes |
| Forwarded calls | \$0.02 / minute | The forwarded leg, per started minute from when the forwarding phone answers (the call screen included), metered with outbound minutes on top of the incoming call |
| Call transcription | \$0.05 / minute | Per whole minute of each conversation transcribed (`transcribe_calls` on; voicemail and unanswered calls are never transcribed), metered and added to the next invoice |
| Voicemail transcription | \$0.08 / minute | Per started minute of each voicemail message left on a workspace number with transcription on (English; messages up to two minutes). `GET /api/v1/dialer/billing` reports it as `voicemail_transcript_minutes` |

How billing behaves:

* **Terms with the first number.** The first purchase requires an admin to accept the current Dialer terms (version `2026-11-02`); the acceptance is recorded before anything is bought. `GET /api/v1/dialer/billing` reports in `terms` which version was accepted, by whom and when, and `terms.current` tells you whether that is still the version in force.
* **One subscription, created on demand.** The first number creates the workspace's Orbit Dialer subscription with the number and the metered minutes on it; later numbers change the quantity. Releasing a number credits the unused part of the month on the next invoice; releasing the last one leaves the subscription with only its metered items, so nothing more is charged until the next purchase.
* **Subscription state.** `subscription.status` is `none` until the first number, then `active`. `past_due` means Stripe is retrying a failed payment; dialing continues meanwhile. `canceling` (with `cancel_at`) only happens when support schedules a cancellation; when it takes effect, or when the subscription ends for non-payment, **every number the workspace owns is released**.
* **Dashboard only.** Buying and releasing numbers, and accepting the terms, change what the workspace is charged, so none of them has an API endpoint or MCP tool. The API reads the costs (`GET /api/v1/dialer/billing`) and the numbers, and can assign and label numbers.

Incoming calls (set per number in Settings → Dialer → Phone numbers, admins):

* **Who it rings.** A call to a workspace number rings its assigned rep in the browser, anywhere in Orbit (they answer from the card that slides in). A number nobody is assigned to rings whoever last called that person from it, so a prospect calling back reaches their rep; if nobody did, the workspace admins are notified of a missed call and the first admin gets the call-back task. A rep already on a call is not rung.
* **Who sets it up.** Admins configure every number; the rep a number is assigned to can change its ring time, forwarding number and greeting. Callers hear that the call may be recorded before it rings when the workspace records calls (a per-number setting, on by default).
* **Then.** After the ring time (5 to 60 seconds) the call goes to the number's forwarding number, if it has one (a US landline or mobile number, or Canadian with Canada turned on; never premium-rate or shared-cost). Whoever answers there must press 1 to take the call; otherwise it goes on to voicemail with a spoken or recorded greeting. Voicemail messages are recorded like calls and, with transcription on, transcribed.
* **Logged and followed up.** Every incoming call is logged on the contact whose phone (or a phone-type custom field) matches the caller, with `direction: "inbound"`. A missed call or a voicemail creates a call-back task for the rep and notifies them (the `calls` notification topic).
* **Numbers can be moved to Orbit** from another carrier (Settings → Dialer → Move numbers to Orbit, admins). Orbit staff handle the transfer; the numbers then appear here, billed like bought ones.

Numbers and reps:

* **One number per rep.** An admin assigns each number to a team member; that person's calls go out on it. A rep can hold one active number at a time (`409 conflict` otherwise).
* **The default number.** Reps without a number of their own use the workspace default, `caller_id` in the settings. The first number a workspace buys becomes the default; if the default is released, the oldest remaining number is used.
* **Assign and label over the API**, buy and release in the dashboard. `GET /api/v1/dialer/numbers` lists what the workspace owns and who holds what; `PATCH /api/v1/dialer/numbers/:id` reassigns or relabels. There is no `POST` or `DELETE` on this resource; a signed-in admin buys and releases from Settings → Dialer. Releasing a paid number credits the unused part of the month on the next invoice.
* **Costs.** `GET /api/v1/dialer/billing` reports the active numbers by type and their monthly total, the billable minutes so far this calendar month and their cost, the transcribed minutes billed so far this month and their cost (`transcript_minutes`), the list prices (`prices`, with `transcript_minutes_cents` for transcription), `feedback` (how many calls the AI coach wrote feedback for this month and the AI credits that used, one per call), `monthly_total_cents` (numbers only; metered usage excluded), the `subscription` state and the `terms` acceptance. Unanswered calls bill nothing, and neither does a call that was not transcribed.

```bash theme={null}
curl -X PATCH https://app.useorbitcrm.ai/api/v1/dialer/numbers/NUMBER_ID \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "assigned_user_id": "USER_ID", "friendly_name": "Jane - West coast" }'
```

## Call lists are saved views

A call list is nothing more than a saved view of the Contacts table (or a [static list](https://docs.useorbitcrm.ai/developers/api/crm.md#static-lists) of contacts, dialed in the Contacts table's default order): its filters pick the people, its sort sets the dialing order. Build one with `POST /api/v1/contact-views` (or `create_contact_view` over MCP), list existing ones with `GET /api/v1/contact-views`, then check what it would dial with `GET /api/v1/dialer/call-lists/preview?view_id=`. The dialer only calls numbers in the **United States**; the country comes from the number itself, so a `+1` number in Jamaica, the Bahamas or Puerto Rico is outside it. The preview applies the dialer settings and reports who is left out and why:

| Skip reason | Why |
| - | - |
| `do_not_call` | The contact is flagged do-not-call or unsubscribed |
| `no_phone` | No phone number on the contact |
| `invalid_phone` | The number cannot be normalised to E.164 with `default_country` |
| `unsupported_country` | The number is outside the United States (or its country cannot be told) |
| `recently_called` | Called within `skip_recently_called_hours` |
| `max_attempts` | Already has `max_attempts_per_contact` finished calls |
| `duplicate_phone` | Another contact earlier in the list has the same number |

A session holds at most 500 dialable contacts (`truncated: true` when the view has more); the first 2,000 contacts of the view are examined. A person then opens the view on the Dialer page and starts dialing. The API does not start sessions; it can only read them afterwards.

## Call scripts

A call script is the text a rep reads during a call. The Dialer page shows it beside the call with the merge fields filled in from the contact being dialed, and highlights the ones it could not fill. Scripts belong to the team; one can be the **default**, which new sessions start with unless the person picks another when they set up the session (the session's `script_id` records which one).

| Field | Notes |
| - | - |
| `name` | 1–120 characters |
| `content` | Plain text, up to 20,000 characters, with merge fields written `{{first_name}}` (case-insensitive; spaces inside the braces are fine). Unknown fields are left as written so a typo is visible |
| `is_default` | At most one script per team. Setting it on one script clears it on the previous default; `false` leaves the team without a default |
| `show_on` | Record pages the script also shows on as a **playbook**: `["contact"]`, `["deal"]`, both, or `[]` (the default: the Dialer only) |
| `questions` | Up to 25 discovery questions, `[{ "id", "label", "object": "contact" \| "deal", "field", "help"? }]`. Each answer saves into `field` of the record the playbook is opened on (see below). Sending `questions` replaces the whole list; omit `id` on a new question and one is assigned |

Merge fields: `{{first_name}}`, `{{last_name}}`, `{{full_name}}`, `{{company}}`, `{{job_title}}`, `{{email}}`, `{{phone}}` from the contact, and `{{rep_name}}` for the person calling.

```bash theme={null}
curl -X POST https://app.useorbitcrm.ai/api/v1/dialer/scripts \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "name": "Cold intro", "content": "Hi {{first_name}}, this is {{rep_name}} from Orbit. I saw {{company}} is hiring SDRs...", "is_default": true }'
```

Scripts are read by a person on a call; nothing about them places one. The AI coach is given the session's script, rendered for the contact, so it can judge how closely the conversation followed it.

### Playbooks

In the app, scripts are managed as **playbooks** (Settings → Playbooks): a call script, a battle card or a list of discovery questions. A playbook with `show_on` appears as a Playbooks tab on contact and/or deal pages, its merge fields filled from the contact (on a deal, its primary contact), and its contact questions are also answered in the Dialer beside the call.

A question's `field` is one of the object's custom fields (`GET /api/v1/crm/fields`) or one of these built-in columns: `first_name`, `last_name`, `job_title`, `company`, `phone` on contacts; `name`, `amount`, `close_date`, `description` on deals. Each field takes one question per playbook, and a field that doesn't exist is a `400`. Answers are saved by people from the record page or the Dialer, under the same rules as any edit there: record access, the field's type and validation rules (`422` with `errors[]`), and the pipeline's rules. To write those fields from code, update the contact or deal directly.

```json theme={null}
{
  "name": "Discovery call",
  "content": "Hi {{first_name}}, thanks for making time...",
  "show_on": ["deal"],
  "questions": [
    { "label": "What budget is set aside for this?", "object": "deal", "field": "amount" },
    { "label": "Who else signs off?", "object": "deal", "field": "decision_makers", "help": "Names and roles" }
  ]
}
```

## Transcripts and AI feedback

With `transcribe_calls` on, every recorded **conversation** (a call a person answered; voicemail and unanswered calls are skipped) is transcribed once it ends, and with `ai_call_feedback` on the AI coach then writes feedback for the rep. Both land on `GET /api/v1/dialer/calls/:id/insights`, one record per call; the call itself carries an `insights` summary with the two statuses so a list can show what is ready. Transcription happens in the background: poll while a status is `queued` or `processing`.

| Field | Meaning |
| - | - |
| `transcript_status` | `queued`, `processing`, `completed`, `failed` or `skipped` (not worth transcribing: voicemail, too short, or the setting was off) |
| `transcript_text` | The whole conversation as speaker-labelled plain text (`Rep: …` / `Contact: …`) |
| `transcript_sentences` | `[{ "speaker": "rep" \| "contact", "start": 1.2, "end": 3.4, "text": "…", "confidence": 0.93 }]`, times in seconds from the start of the recording; `confidence` may be `null` |
| `transcript_language`, `transcript_minutes` | Detected language and the whole minutes of audio transcribed (rounded up); the minutes are metered at the transcription price and show up under `transcript_minutes` in `GET /api/v1/dialer/billing` once billed |
| `transcript_error` | Why a transcript `failed`, when it did |
| `feedback_status` | Same statuses as the transcript; `skipped` when `ai_call_feedback` is off or the workspace had no AI credits left (`feedback_error` says which) |
| `feedback` | The coach's structured feedback (below), or `null` |
| `feedback_model`, `feedback_error` | Which model wrote it; why it `failed`, when it did |
| `script_id` | The script the session used, so `feedback.script_adherence` has something to judge against; `null` when there was none |

`feedback` has this shape:

```json theme={null}
{
  "summary": "Two or three sentences on what happened.",
  "score": 7,
  "went_well": ["Opened with the trigger event", "Asked about the current process"],
  "improve": ["Let the objection on pricing go unanswered"],
  "objections": [{ "objection": "We already use a tool for this", "handled": false, "suggestion": "Ask what the current tool misses before pitching" }],
  "next_steps": ["Send the case study", "Book the follow-up for Thursday"],
  "script_adherence": { "followed": "partly", "notes": "Skipped the qualifying questions" },
  "suggested_outcome": "interested"
}
```

`score` is 1–10 or `null` when the call was too short to judge; `script_adherence.followed` is `yes`, `partly`, `no` or `no_script`; `suggested_outcome` is one of the [outcomes](#outcomes) for the rep to compare with what they picked. The whole record is read-only over the API: the background job writes it.

## Deleting recordings

`DELETE /api/v1/dialer/calls/:id/recording` (or `delete_call_recording` over MCP) removes a call's media: the audio recording is deleted at the carrier, and the transcript, its sentences and the AI feedback are deleted at the transcription service and from Orbit. The call itself stays, with its numbers, timings, `duration_seconds`, outcome and notes, so statistics and the contact's call history are unchanged; its `recording_status` becomes `absent`, its `insights` summary `null`, and `GET /api/v1/dialer/calls/:id/insights` answers `404 not_found` from then on. The deletion is recorded in the workspace's audit log and **cannot be undone**. Admins only (`403 forbidden` otherwise); `409 conflict` when the call has neither a recording nor a transcript.

Deleting a contact also deletes the media of every call made to them, but the two ways of deleting differ when a provider fails. In the dashboard the contact is kept until every call's media is gone, so the delete can simply be retried. `DELETE /api/v1/contacts/:id` purges best effort: if the carrier or transcription service fails for a call, the contact is still deleted and that call keeps its recording or transcript, now with `contact_id: null`. Find such calls with `GET /api/v1/dialer/calls` and remove their media with this endpoint.

```bash theme={null}
curl -X DELETE https://app.useorbitcrm.ai/api/v1/dialer/calls/CALL_ID/recording \
  -H "Authorization: Bearer $ORBIT_API_KEY"
```

## Do-not-call list

The list is keyed by phone number, stored in E.164 and matched by digits, so `+1 (555) 010-0100` and `5550100100` are the same entry. Numbers in any format are normalised with the team's `default_country`; the response separates `added`, `already_listed` and `invalid`.

* Contacts carrying a listed number are flagged `do_not_call` and skipped by every call list; the pre-dial check blocks them even if a list was built earlier.
* Email unsubscribes and SMS opt-outs add the contact's number automatically (`reason`: `email_unsubscribe`, `sms_opt_out`). The outcome `do_not_call` on a call adds it with `reason: call_outcome`.
* Removing a number from this list does not remove the email unsubscribe or SMS opt-out behind it, and the next unsubscribe adds it back.
* Only admins remove numbers; any credential with `dialer:write` can add them.

```bash theme={null}
curl -X POST https://app.useorbitcrm.ai/api/v1/do-not-call \
  -H "Authorization: Bearer $ORBIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "phones": ["+15550100100", "(555) 010-0101"], "reason": "import", "note": "Legacy suppression file" }'
```

## Responses

Lists return `{ "data": [...], "meta": { "total", "page", "per_page", "total_pages" } }`; single records return `{ "data": {...} }`. Errors return `{ "error", "error_description" }` with a 4xx or 5xx status, for example `403 forbidden` when a non-admin changes `caller_id`, assigns a number or removes a do-not-call entry, `409 conflict` when assigning a number to someone who already holds one or when two callers make different scripts the default at once, `404 not_found` for a call that has no insights yet, and `503 unavailable` when calling is not configured on the instance.

> [card] [MCP tools](https://docs.useorbitcrm.ai/mcp/tools.md)
>
> The same operations as MCP tools

> [card] [CRM API](https://docs.useorbitcrm.ai/developers/api/crm.md)
>
> Saved contact views and the rest of the CRM


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.