# Dial — Communication Stack for AI Agents

Dial gives AI agents a real phone number, SMS, and WhatsApp address. One API to call, text, and message worldwide.

API base: `https://api.getdial.ai`

**This page only bootstraps you** — install the CLI, create or sign in to an account, and install the Dial skill into your agent. After that, the installed skill plus `dial --help` and the full docs are your reference for everything else (sending SMS, placing calls, receiving 2FA codes, the event stream). This page intentionally does **not** duplicate that surface.

---

## Quick Start

### Step 1 — Install the CLI

```bash
curl -fsSL https://getdial.ai/install | bash
```

Installs the `dial` command. Requires Node 22+ and git. **No sudo needed** — if your global npm prefix isn't writable, the installer falls back to a user-local prefix and prints the one `export PATH=…` line to add to your shell profile.

### Step 2 — Check state

```bash
dial doctor --json
```

Read the `nextStep` field to decide what to do:

- `signup` → run **Step 3**
- `onboard` → run **Step 4** (pending OTP is still valid)
- `register_number` → run **Step 4b** (email verified; a phone number is still needed)
- `resend_otp` → run **Step 3** again (pending OTP expired)
- `install_listen` → run **Step 5**
- `ready` → nothing to do

### Step 3 — Sign up

```bash
dial auth login you@example.com
```

An OTP is emailed. The verification id is stored locally at `~/.local/share/dial/pending-signup.json`.

### Step 4 — Verify the email OTP

```bash
dial auth verify-otp --code 123456 --agent claude-code
```

**Read the output — this step has two outcomes:**

- **Already had an account** → you're signed in and done. Skip to Step 5.
- **New account** → your email is verified, but creating an account also needs a **verified phone number**. The output names `dial auth register-number` as the next command; go to Step 4b. No API key is issued yet.

Either way, `--agent <name>` installs the Dial skill into that agent's config directory right now — match it to whichever agent you (the agent reading this) are running in. It installs on this step even when signing up isn't finished, so you keep durable on-disk knowledge of the `dial` surface and don't need this page re-fetched.

Supported `--agent` values: `claude-code`, `cursor`, `codex`, `opencode`, `pi`, `openclaw`, `nanoclaw`, `hermes`. For `nanoclaw` only, run the command from inside the NanoClaw project root (its skills are project-scoped). Re-run `dial auth verify-otp --agent <name>` after a CLI upgrade to refresh the installed skill.

### Step 4b — Register and verify a phone number (new accounts only)

**Ask the user for a phone number that can receive SMS.** Don't invent one, and don't reuse a Dial number — a Dial number is refused here. It should be one the user keeps.

```bash
dial auth register-number +14155550123
```

A 6-digit code is texted to that number. Ask the user to read it back, then:

```bash
dial auth verify-otp --number --code 654321
```

This creates the account. On success it writes your API key to `~/.local/share/dial/auth.json` (mode 0600) and prints a masked fingerprint; the CLI reads the key from that file for subsequent commands, so you never copy it. Your first number is provisioned with a default inbound voice-agent prompt — change it any time with `dial number set <number> --inbound-instruction "..."`.

If the code is wrong or has expired, re-run `dial auth register-number` with the same number to send a fresh one.

**Finalize onboarding — but only if `dial auth verify-otp` offers it.** Whether to install the background notification listening service is the CLI's decision, not yours; its output tells you which way to go:

- If `dial auth verify-otp` **offers** the service — `--json` sets `agentHint.action` to `"ask_user"`, and the human output prints an "ask the user" finalization block — then ask the user the question below before continuing. If the original request was something else (e.g. "call X", "send Y"), briefly pause that task, ask, get an answer, then resume.
- If `dial auth verify-otp` reports the service **unavailable** and says it's skipping the offer — `--json` sets `agentHint.action` to `"skip"`; this is normal in sandboxes / containers / CI without launchd or systemd — then **don't ask**. Just continue with the original task; inbound events still work via `dial wait-for`.

When you do ask:
- Call it **"the background notification listening service"**. Don't say plist / daemon / launchd / systemd — that's not how to talk to a non-technical user.
- Explain the trade-off: with it, the agent on this machine learns about inbound SMS (2FA codes, replies) and call-ended events in real time. Without it the agent is send-only — it can place calls and send messages but can't observe anything inbound.
- Use your structured ask-user / clarification tool if you have one; otherwise ask in your regular reply and wait for the answer.

### Step 5 — (Conditional) Install the background notification listening service

Only run this if `dial auth verify-otp` offered the service **and** the user answered **yes** in Step 4/4b (skip it entirely if it reported the service unavailable):

```bash
dial listen install
```

This needs a user service supervisor — **launchd** on macOS, **systemd `--user`** on Linux. In sandboxes, containers, or CI without one, `dial auth verify-otp` detects this and reports the service as unavailable; **skip this step there**. Inbound events still work without the daemon: `dial wait-for` automatically long-polls the API when the daemon isn't running.

After Steps 4–5, run `dial doctor` again — `nextStep` should be `ready`.

### Step 6 — Tell the user where to manage the account

Onboarding leaves the user with an account they can't find on their own: they don't know it has a dashboard, and they don't know which email address you signed up with. Close the loop in your reply — `dial auth verify-otp` prints both, and `--json` returns them as `dashboardUrl` and `email`:

> You can manage your account at https://getdial.ai/dashboard — sign in with `<the email you used>` and it emails you a code.

Then keep working from the CLI. Numbers, messages, calls, and events are all `dial` verbs, and `dial billing` already reports balance and plan — don't send the user to the browser for anything you can do or read yourself. The dashboard is only for the three things the CLI doesn't cover: **paying** (adding credits, subscribing, payment methods), **sharing the account** with a teammate, and **carrier (10DLC) registration**.

---

## Everything else lives in the installed skill + `dial --help`

Once you've onboarded (and ideally installed the skill with `--agent`), use that — not this page — for day-to-day work. The CLI is the source of truth:

```bash
dial --help              # all commands
dial <command> --help    # flags + usage for one command
```

Every command takes `--json` for machine-readable output. The common verbs:

- **Send an SMS** — `dial message --to +1… --body "…"`
- **Show a typing indicator while composing** — `dial typing start --to-number +1…`; sending a message clears it natively, so start again between messages, and run `dial typing stop --to-number +1…` if you end up not sending. iMessage numbers display it; SMS numbers ignore it, so it's always safe
- **Place an AI voice call** — `dial call --to +1… --outbound-instruction "…"`, then `dial call get <id>` once it ends
- **Set a number's inbound behavior** — `dial number set <number> --inbound-instruction "…"` (add `--inbound-language <bcp47>` to pin inbound calls to one language instead of auto-detecting from the caller)
- **Set a number's display identity** — `dial number set <number> --channel both --name "Maya Chen" --avatar ./photo.png` (the name and photo recipients see). `--channel` is `imessage`, `whatsapp`, or `both`; `both` sets one identity everywhere in a single call and is checked against every channel before any of them is written, so the profiles can't drift apart. To give the two channels *different* identities, drop `--channel` and use the per-channel flags instead: `--first-name`/`--last-name`/`--avatar` (iMessage) and `--whatsapp-name`/`--whatsapp-avatar` (WhatsApp). A photo can be replaced but not removed.
- **Run a number messaging-only** — `dial number set <number> --calling off` switches calling off in both directions: inbound calls are never connected (the caller isn't answered) and `dial call` from that number fails with `calling_disabled`. Messaging is unaffected. `--calling on` restores it; the switch applies to the next call, never one in progress. `capabilities` still lists `call` (it reports what the line was provisioned for), so read `callingEnabled` to know whether calling is actually on
- **Forward inbound calls to a person** — `dial number set <number> --forward-to +18005550100` makes inbound calls ring that phone instead of the AI voice agent; the caller is connected when it answers, and the call ends if it's busy or unanswered. `--forward-to off` hands calls back to the agent. The number's `forwardTo` shows the current target (null → the agent answers); it has no effect while calling is off
- **See who you have talked to** — `dial contacts` lists every number your lines have texted or called, newest activity first, with each contact's message and call counts and a preview of the latest exchange. Derived from history, not an address book — a number appears the moment it first exchanges anything with one of your lines, and counts span every line on the account (`--number-id <id>` narrows the list to one line's contacts and counts)
- **Check what a number can receive before sending** — `dial lookup +1…` reports which channels reach that number — today iMessage and WhatsApp. Works on ANY number, not just yours, and without having messaged it before. It's a live check, not a stored property, so treat a yes as a strong signal for picking a channel rather than a guarantee the send lands; a lookup that fails exits non-zero instead of reporting no, so a no always means the number genuinely isn't reachable there. It needs nothing from your account — you don't have to hold an iMessage or WhatsApp number to ask about either channel.
- **Read one conversation, or find a message by its text** — `dial message list --contact +1…` returns that person's messages in both directions across every line; `dial message list --search "invoice"` matches message bodies case-insensitively across your whole history. Prefer both to filtering yourself: a bare `dial message list` returns the newest 100 messages **across all conversations**, so an older thread can be missing from it entirely
- **Receive a 2FA code / react to a call ending** — `dial wait-for message.received -f channel=sms` / `dial wait-for call.ended -f callId=<id>`
- **Wait for a new number to become usable** — `dial wait-for number.status_changed -f status=ready -f phoneNumberId=<id>`. A number's capabilities settle separately: `sms` and `imessage` work at once, and `call` on an iMessage number takes a few minutes. A number bought with WhatsApp arrives with it `ready`; connecting WhatsApp to a number you already hold takes hours — it registers, then reports `warming_up` for about 6 hours before `ready` — so give that wait a long timeout. The top-level `status` folds them — `unsettled` while any is still working, `ready` when all are, `degraded` when everything finished and something failed. Wait for `ready` or `degraded`; both are terminal, so a wait always resolves. Don't poll `dial number list`. On `degraded`, read `capabilities` in the payload to see which channel failed, its plain-language `error`, and `retryAvailableAt` when a cooldown must pass before retrying
- **Confirm a message was delivered, or catch a carrier rejection** — `dial wait-for message.status_changed -f messageId=<id>`, or `-f deliveryState=failed` to watch for rejections across a number. Delivery and reads are separate fields: `deliveryState` is `pending` → `delivered`/`undelivered`/`failed` (`unconfirmed` on iMessage numbers, which report no delivery receipts), and `readState` is `unread` → `read` (`unsupported` on SMS, which never reports reads — don't wait for one). A failure carries a plain-language `deliveryError`

`dial wait-for` reads from the local listen log when the daemon is running and **transparently long-polls the REST API when it isn't** — same filter semantics either way. A match exits `0`; a timeout or fallback error exits non-zero. Run `dial wait-for --help` for its filter flags.

**iMessage sending limits** — each iMessage number can start **50 new conversations per day** (a new conversation is the first message to a recipient that number hasn't messaged; replies inside an existing thread don't count). Past the cap, sends fail with `429` until the window resets. Sustained overall volume is capped separately — ask before building a high-volume workload. Separately, a recipient who has **never replied** accepts only a limited number of messages — that clears when they reply, so treat its `429` as "not opted in yet" and don't retry in a loop.

**WhatsApp lines and group conversations are in beta** (enabled per account) — see https://docs.getdial.ai/documentation/capabilities/whatsapp and https://docs.getdial.ai/documentation/capabilities/groups. A **WhatsApp-only number** (`capabilities: ["whatsapp"]` on POST /api/v1/numbers) is a line whose only channel is WhatsApp — no calls, no SMS — and the cheapest WhatsApp presence; its `setupStatus` is the WhatsApp registration itself, and sends from it default to WhatsApp.

**Full reference:** https://docs.getdial.ai — every page has a plain-markdown twin (append `.md` to any docs URL). To search capabilities without reading the whole site, grep `https://docs.getdial.ai/llms-full.txt`.

---

## Security Rules

- Never expose your API key (`sk_live_...`) in client-side code or logs — store it in environment variables / the auth file only.
- All requests must use HTTPS.
- Emergency services (911, crisis lines) are blocked.

---

## Pricing

- **$5 signup credit** — start free, no credit card required.
- **Pay-as-you-go:** $3/month per phone number plus metered usage (calls per minute, SMS priced by destination), drawn from your prepaid credit; spend pauses at $0.
- **Free accounts** (no top-up or subscription yet) are capped at **5 minutes per call** and **2 concurrent calls**; both limits lift permanently on the first top-up or subscription.
- **Subscription (optional):** switch any number to a flat monthly or annual per-number plan (annual discounted) with unmetered usage.
- **iMessage numbers are pay-as-you-go only** — they can't be added to a subscription, and an account holding one can't subscribe until it's released.

Current rates and billing management: `https://getdial.ai/dashboard/billing`

---

Full docs: https://docs.getdial.ai
Support: founders@getdial.ai
