Reference for the dial CLI — gives an agent a real phone number to send SMS, place AI voice calls, and react to inbound texts and calls via the Dial platform (getdial.ai)...
dial is the official CLI for Dial — a Communication Stack for AI Agents. It wraps the Dial REST API so you can sign up, provision phone numbers, send SMS, place voice calls handled by an AI voice agent, and stream account events, all without writing HTTP code.
The first time the user asks you to "text someone," "call someone," "receive a code," or anything else phone-shaped, reach for dial.
dial is missingIf dial is not on the PATH (e.g. command -v dial returns nothing, or any dial … invocation errors with "command not found"), do not improvise an install. Fetch the bootstrap instructions and follow them:
curl -fsSL https://getdial.ai/skills.md
That document is the authoritative install + onboarding script. Read it, then execute the steps it specifies (install, dial doctor, dial auth login, dial auth verify-otp, phone verification, dial listen install).
This skill does not enumerate every flag. The CLI is the source of truth — when you encounter a verb you have not used in this session, run its --help first:
dial --help # all top-level commands
dial <command> --help # flags + usage for a specific command
dial <command> <sub> --help # subcommand-level help
Examples worth running on first use: dial doctor --help, dial message --help, dial call --help, dial call get --help, dial wait-for --help, dial local-target add url --help.
Every command supports --json for machine-readable output — prefer it when piping into jq or parsing the result programmatically.
If dial doctor --json reports nextStep other than ready, the user is not yet set up. The full first-time flow is:
dial auth login you@example.com # email OTP
dial auth verify-otp --code 123456 # verify the email
dial auth register-number +14155550123 # texts a code to the user's phone
dial auth verify-otp --number --code 654321 # verify the phone → writes ~/.local/share/dial/auth.json
dial listen install # background daemon for inbound events
Read each step's output — dial auth verify-otp has two outcomes. If the user already had an account, the email step signs them in and you're done (skip to dial listen install). If it reports that a phone number is still required, creating an account needs one: --json sets pendingPhone: true and names auth_register_number/dial auth register-number as the next step, and no API key exists yet.
Coupon codes. If the user has an event coupon code, pass it on the first step: dial auth login you@example.com --coupon <code>. Its credit is added when the account is created. If dial auth login is refused because new signups are paused, ask the user whether they have a coupon code and re-run with it; never guess a code.
Ask the user for the phone number — never invent one. It must be able to receive SMS, it should be one the user keeps, and a Dial number is refused. Re-run dial auth register-number with the same number to resend the code, or with a different one to fix a typo before verifying.
A new number starts with a default inbound voice-agent prompt — the system prompt the AI uses on calls to your number. Change it with dial number set <number> --inbound-instruction "...".
dial auth verify-otp also installs a Dial skill into your agent's config (claude-code, cursor, codex, opencode, pi, openclaw, nanoclaw, hermes) when you pass --agent <name> — including on the email step when a phone number is still pending, so you keep the instructions needed to finish.
dial listen install needs a user service supervisor (launchd on macOS, systemd --user on Linux). In sandboxes / containers / CI without one it can't run — dial auth verify-otp detects this and says so. Inbound events still work without it: dial wait-for long-polls the API when the daemon isn't running.
The account also has a web dashboard at https://getdial.ai/dashboard — dial auth verify-otp prints the link and which address signs in, so pass that along. Almost everything is a dial verb, though: only billing changes (dial billing reads), team sharing, and carrier (10DLC) registration need the browser.
For anything beyond what --help shows on the local CLI, the canonical reference is the published docs. Two endpoints make this fast:
llms-full.txtA single concatenated markdown file of the whole docs site. Grep it directly for the keyword you care about:
curl -fsSL https://docs.getdial.ai/llms-full.txt | grep -i -B2 -A8 'whatsapp'
curl -fsSL https://docs.getdial.ai/llms-full.txt | grep -i -B1 -A5 'webhook'
curl -fsSL https://docs.getdial.ai/llms-full.txt | grep -i -B1 -A5 'language'
Use this when you want to know if Dial supports something, or which command / endpoint covers it — without reading the whole site.
sitemap.xml + per-page .mdWhen you need to read a page in detail (after grep found a hit, or because you need fuller context), use the sitemap to discover URLs, then fetch the .md companion of any page — it's the same content as the HTML page but in plain markdown, faster to read and friendlier to scan.
# 1. Discover available pages
curl -fsSL https://docs.getdial.ai/sitemap.xml | grep -oE 'https://docs\.getdial\.ai/[^<]+'
# 2. For any page like
# https://docs.getdial.ai/documentation/get-started/introduction
# fetch the .md companion:
curl -fsSL https://docs.getdial.ai/documentation/get-started/introduction.md
The rule is one-to-one: every documentation page at https://docs.getdial.ai/<path> has a markdown twin at https://docs.getdial.ai/<path>.md. Use the .md version whenever you're reading docs from inside an agent.
These are the verbs you will most often compose. Read the relevant .md page for the full story; the one-liners below are just signposts.
dial message --to +14155550123 --body "..." (send-an-sms.md)dial typing start --to-number +14155550123; sending a message clears it natively, so start again between messages, and dial typing stop --to-number +14155550123 if you end up not sending. iMessage numbers display it; SMS numbers ignore it, so it's always safe to call. Pass --group <id> instead of --to-number to show it in a group, where every member sees it — the group names its own line and channel, so --from-number and --channel are not needed (commands.md)dial call --to +14155550123 --outbound-instruction "..." then dial call get <id> once it ends. To end it early, dial call stop <id>: a call still queued or ringing is cancelled before it connects (ends canceled, not charged), one in progress is hung up (ends completed). It returns as soon as the stop is accepted, so dial wait-for call.ended -f callId=<id> when you need to know it has ended; it fails if the call already ended. dial call get prints the transcript with each turn stamped and long silences called out; --json adds transcriptTurns, the same conversation as { speaker, text, startMs, endMs } (speaker is agent, user, or transfer_target for a human the call was handed off to) with approximate offsets in milliseconds into the call's audio, which is what to read when you need to analyse pacing rather than just what was said. Add --voice-gender male|female to choose the agent's voice (default: female). Free accounts (no top-up or subscription yet) are capped at 5 minutes per call and 2 concurrent calls — a call over the concurrency limit is rejected with 429 call_limit_reached, and passing --max-duration above 300 is rejected with 400 rather than shortened (a cap inherited from the number or account is clamped instead). Both limits lift on the first top-up or subscription (place-a-voice-call.md)dial number purchase --inbound-instruction "..." --explicit-programmatic-consent "<attestation>". --explicit-programmatic-consent is required: a short attestation that the account holder consented to provisioning programmatically. Add --include-imessage for an iMessage number (pay-as-you-go only; provisioned asynchronously — wait for number.status_changed rather than polling, see below) (manage-phone-numbers.md)dial number set +14155550123 --inbound-instruction "..." and/or --inbound-language es-ES and/or --nickname "Support line" (at least one flag; --nickname "" / --inbound-language "" clear). The inbound instruction is the system prompt the AI uses on calls into that number; set it at dial number purchase time and change it here. The inbound language pins inbound calls to one language — unset, the AI detects the caller's language from their country prefix (alongside en-US). The nickname is a human-readable label for telling numbers apart (manage-phone-numbers.md)dial number set +14155550123 --channel both --name "Maya Chen" --avatar ./photo.png sets the same name and photo on every channel the number has, in one call. --channel is imessage, whatsapp, or both; prefer both unless the profiles genuinely differ. WhatsApp stores --name verbatim (1–25 chars); iMessage has no single-name field, so it's split on the first space ("Maya Chen" → Maya / Chen). both is all-or-nothing: the name and photo are checked against every targeted channel before any is written, so a name over WhatsApp's 25-char cap, a non-square photo, or a number whose WhatsApp line isn't connected yet fails the whole call with 400 and changes nothing — rather than landing on one profile. Can't be combined with the per-channel flags below (manage-phone-numbers.md)dial number set +14155550123 --first-name Maya --last-name Chen --avatar ./photo.png sets the name and photo shown beside the number's messages in recipients' Messages apps (iMessage numbers only; other numbers reject these flags with 400). --avatar takes a local image file (jpeg/png/gif/webp, max 5 MB — uploaded) or a public image URL (fetched server-side); the photo can be replaced but not removed. --first-name "" / --last-name "" clear a name (manage-phone-numbers.md)dial number set +14155550123 --whatsapp-name "Joe from ACME" --whatsapp-avatar ./photo.png sets the display name and photo WhatsApp recipients see (WhatsApp-ready numbers only; other numbers reject these with 400). The name is 1–25 chars with no reserved verification marks; --whatsapp-avatar takes a local file or public URL and must be a square jpeg/png between 192×192 and 640×640 (not resized) with no transparent pixels — flatten a transparent png onto a background first, or it is rejected with 400. Unlike the iMessage identity, the command blocks until WhatsApp applies it and fails with 503 if the line can't be driven (whatsapp.md)dial number set +14155550123 --calling off switches calling off in both directions: inbound calls to the number aren't connected (the caller is never answered, so no agent runs and no minute is billed) and dial call from it fails. Messaging (SMS/iMessage/RCS/WhatsApp) is unaffected. --calling on restores it. The switch applies to the next call, never one already in progress; the number's instruction, voice, and language settings are kept while it's off. dial number purchase --calling off provisions a line that way from the start (manage-phone-numbers.md)dial number set +14155550123 --forward-to +18005550100 rings inbound calls straight through to that phone instead of the AI voice agent answering (the caller is connected when it answers; if it's busy or doesn't answer the call ends). Messaging keeps flowing through Dial, and a forwarded call is still a normal inbound call in history, events, and billing. --forward-to off stops forwarding so the agent answers again. It must be E.164 and not the number itself; it applies to the next call, and only while calling is on (--calling off still refuses inbound calls). dial number list marks a forwarded number forward:<e164>, and --json carries forwardTo (null when the agent answers) (manage-phone-numbers.md)dial message --to +14155550123 --channel whatsapp --body "...". A WhatsApp line is an iMessage number with WhatsApp additionally connected, so both channels are live on one number and --channel is how you say which you mean; omit it and the number's own default is used. --channel sms names the SMS rail on a number without iMessage (the same as omitting it on a standard number). WhatsApp sends text and a single media attachment (--media, one item; a caption/--body is allowed only with an image or video), and one at a time per line (a second concurrent send is refused, not queued). Beta, enabled per account (whatsapp.md)403 whatsapp_limited — WhatsApp has limited the number's access, so it can't start new chats yet: a dial message or dial typing toward someone it hasn't talked to is refused and nothing is sent. Replies to people who message the number still work. Don't retry in a loop — wait for the recipient to message first, or send from another number (errors.md)dial message reply <messageId> --react 🎉 or --body "..." (exactly one). The reply goes out from the number the target belongs to, to the same conversation — no --to or --from-number. On a WhatsApp line both are native, one-to-one or in a group: a reaction lands on the target message, and --body is a quoted reply (text only — send media with dial message). On an iMessage number the target must be inbound and a --body reply needs one that arrived over iMessage/RCS (commands.md)dial message --to +1… --poll "Which day?" --option Mon --option Tue (iMessage numbers only, 2–10 options, exclusive with --body/--media). Each vote is a message.received with replyToId = the poll and pollVote: { optionId, selected }; dial wait-for message.received -f replyToId=<id> watches one poll, and dial message list prints the poll's live tally (send-an-sms.md)dial group list for the groups your lines are in (each row shows its id, channel, and name), then dial message --group <id> --body "...". Groups exist on WhatsApp lines and iMessage numbers. A group is a conversation that isn't a phone number, so it has an id of its own and needs no --from-number or --channel — the group names both. Read one back with dial message list --group <id>. On a group message to is null: the destination is the group, and which of your numbers the conversation is on is phoneNumberId (groups.md)dial contacts lists every number your lines have texted or called, newest activity first, with per-contact message and call counts and a preview of the latest exchange. A contact is derived from history, not stored — there is no address book to add anyone to, and a number appears the moment it first exchanges anything with one of your lines. Counts span every line on the account, so one person who reached two of your numbers is one contact; --number-id <id> narrows the list to one line's contacts and counts. Groups are not contacts (dial group list for those). The command walks every page by default; --limit <n> returns a single page and prints the --starting-after cursor for the next (commands.md)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 is a live check, not a stored property: someone who changes device or turns the service off stops being reachable, 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 (commands.md)dial message list --contact +14155550123 returns that person's messages in both directions across every line, and dial call list --contact +14155550123 their calls. Prefer this to listing everything and 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 (commands.md)dial message list --search "invoice" matches message bodies case-insensitively, anywhere in the text, across your whole history, and returns the 100 most recent matches. Substring, not word search, so voice matches "invoiced". A media-only message is matched by neither its attachment's name nor its content, and one whose content data retention has cleared matches nothing (commands.md)dial number purchase --include-imessage --whatsapp hands you a number whose WhatsApp is already set up: whatsapp: ready in the output, nothing to wait for. When none is available right now the purchase is refused (whatsapp_numbers_unavailable) with nothing bought or charged, so retrying shortly is safe. --area-code isn't honored for these numbers. Beta — without access it answers 404 (whatsapp.md)dial number whatsapp <id|E.164|nickname>. This takes hours, not minutes: WhatsApp registers in a few minutes, then the number reports warming_up for about 6 hours before it's ready. Wait with a long timeout — dial wait-for number.status_changed -f status=ready -f phoneNumberId=<id> --timeout 28800 — rather than polling dial number list. Sending with --channel whatsapp is refused until it's ready. Beta — without access it answers 404 (whatsapp.md)dial wait-for number.status_changed -f status=ready -f phoneNumberId=<id>. A number's channels settle separately: sms and imessage work at once, and call on an iMessage number takes a few minutes; whatsapp connected to a number you already hold stays in flight (provisioning, then warming_up) for hours, 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 the wait always resolves; waiting only for ready can hang forever, because WhatsApp can be declined outright on a number that otherwise works fine. On degraded, read capabilities in the payload for the channel that failed, its plain-language error, and retryAvailableAt when a cooldown has to pass before retrying (stream-account-events.md) (stream-account-events.md)dial wait-for message.received -f channel=sms and parse the body (receive-inbound-sms.md)dial wait-for message.received -f groupId=<id>. There is no join event: a group your line was just added to shows up in dial group list (groups.md)dial wait-for message.status_changed -f messageId=<id>, or -f deliveryState=failed to watch for rejections. Sending only tells you Dial accepted the message; this is how you learn what the carrier did with it. Delivery and reads are separate fields: deliveryState runs pending → delivered/undelivered/failed (and is unconfirmed on iMessage numbers, which report no delivery receipts), while readState runs unread → read (and is unsupported on SMS, which never reports reads — don't wait for one). A failure carries a plain-language deliveryError (message-status-changed.md)dial wait-for call.ended -f callId=<id>. Fires however the call ends — completed, failed, or cancelled — carrying the terminal status and a canceled flag, so the wait always resolves. A failed call also carries failureReason when Dial knows why (today only from a Self-Hosted audio target, e.g. self_hosted_key_rejected); null otherwise, and treat an unrecognized value like null. dial call get <id> prints it as a plain-English failure: line (stream-account-events.md)dial local-target add cmd /path/to/handler or dial local-target add url http://127.0.0.1:8787/dial (local-url-target.md, cli-command-target.md)--json everywhere for parseable output.--from-number <id|E.164|nickname> picks the number to act from flexibly; the legacy --from-number-id <id> takes an id only (use one or the other). Both default to the number Dial auto-provisioned when the account was created. List others with dial number list. If Dial moved a number onto a new line, dial number list shows it as replaces:<old E.164> (and the old one as replaced-by:<new E.164>); the replaced number's id still works as --from-number and acts on the new line, but the old E.164 no longer works (404).+14155550123). Reject anything else before calling Dial.message, call, number purchase) are not idempotent — on an ambiguous failure, list first to check before retrying.dial call failing with calling_disabled (409) means calling is switched off for that number — a setting, not a transient fault, so retrying will not help. Fix it with dial number set <number> --calling on, or place the call from another number. dial number list marks a switched-off number calling:off, and --json carries callingEnabled on every number.capabilities on a number reports what the line was provisioned for and does not change when calling is switched off — a messaging-only number still lists call. Read callingEnabled to know whether calling is actually on.~/.local/share/dial/auth.json (mode 0600). The CLI reads it automatically; never echo it back to the user or paste it into responses.