diff --git a/README.md b/README.md index a0a83f8..91bd002 100644 --- a/README.md +++ b/README.md @@ -161,7 +161,7 @@ For the full command reference, see [`docs/COMMANDS.md`](docs/COMMANDS.md) (auto ## For LLM agents -If you're an LLM (Claude, Codex, Cursor, Aider, custom agent) and the user has asked you to do anything Plivo-related — **read [`cli-skill/SKILL.md`](cli-skill/SKILL.md) first**. It's the single-file reference written for agent consumption: every command with its required args, flag table, and "when to use" — plus the universal invariants (`--dry-run`, `--yes` for spend verbs, the stable error-envelope codes, JSON-output rules). +If you're an LLM (Claude, Codex, Cursor, Aider, custom agent) and the user has asked you to do anything Plivo-related — **read [`cli-skill/SKILL.md`](cli-skill/SKILL.md) first**. It's the single-file reference written for agent consumption: the rules (`--dry-run` before `--yes` for spend and destructive verbs, the stable error-envelope codes, JSON-output rules), a command map, and the behaviour `--help` does not show. For flags, `plivo --help` is the source of truth. **TL;DR for an agent starting cold:** diff --git a/audio-streaming-skill/SKILL.md b/audio-streaming-skill/SKILL.md index b307e1b..b1fe149 100644 --- a/audio-streaming-skill/SKILL.md +++ b/audio-streaming-skill/SKILL.md @@ -1,268 +1,185 @@ --- name: plivo-audio-streaming -description: Connect a WebSocket voice bot to phone calls with Plivo Audio Streaming (the element) and take it from local testing to production, using the Plivo CLI. Load this whenever a Plivo task involves , WebSocket audio, a voice bot or agent, Pipecat, "connect my bot to a number", "caller hears nothing", "invalid answer XML (8011)" on a streamed call, "error reaching answer URL (7011)", a hangup code, "why did this call fail", recording or transfer-to-human for an AI agent, India KYC, 140-series or UCC for a calling agent, or "am I ready for production". Readiness gates first, then eight go-live stages with a check at each, a CLI-first debugging loop (diagnose, get, check the body), a plain-language XML checklist you apply before serving it, and a read-only readiness checklist. Self-contained: it carries everything the WebSocket bot journey needs, and points at plivo-voice-xml and plivo-cli as separate installs for work outside that journey. +description: "Connects a WebSocket voice bot or AI voice agent (Pipecat or custom) to Plivo calls through the Stream XML element, from local test to go-live, using the Plivo CLI. Use for Plivo audio streaming, a caller hearing silence, errors 7011/8011, streamed-call hangup codes, bot-to-human handoff, India KYC for an agent number. Not for SIP trunking (plivo-sip-trunking) or plain Voice XML (plivo-voice-xml)." license: Apache-2.0 --- # Plivo Audio Streaming: connect a bot to calls and go live -You are guiding a developer, or their coding agent, to a working and monitored voice agent. Go stage by stage. Do not skip a check because the user is confident. Speak plainly. Use the Plivo CLI for every Plivo-side step and `-o json` when you need to read a value. Every CLI flag here was taken from the CLI's own `--help`; when this file and `plivo --help` disagree, the CLI is right. If a step has no CLI command, this file says so and gives the API or console path. This skill covers Plivo call control and the WebSocket boundary; it does not cover the STT, LLM or TTS pipeline inside the bot. +Take a developer, or their coding agent, from a WebSocket bot to a working, monitored phone agent. This file covers Plivo call control and the WebSocket boundary, not the STT, LLM or TTS inside the bot. Related skills, if installed: `plivo skill install first-agent` (a guided first call from nothing, not in releases up to v1.1.2: `plivo skill list` shows what yours bundles), `plivo skill install voice-xml` (Plivo XML without a stream), `plivo skill install` (the CLI's own skill). Without them, use `plivo --help` and the docs pointers below. -**What this file assumes you have: nothing but this file, the `plivo` CLI and your own bot.** Everything the WebSocket bot journey needs is here: the readiness gates, the Stream XML with documents to copy, the XML checks that matter for a streamed call, the callbacks and signature recipe, the WebSocket protocol, the hangup codes and the India prerequisites. Other Plivo skills are separate single files that you may not have. Install one only if the task moves outside this journey: +## Rules for every step -- `npx skills add https://www.plivo.com/docs --skill plivo-voice-xml` for general Plivo XML: every element other than `` in full, its complete attribute tables, and IVR, conference and voicemail flows that have no stream in them. -- `plivo skill install` for `plivo-cli`, the CLI's own reference (every command, the JSON envelope, exit codes, headless auth). The CLI writes that file out itself. +- Use the CLI for every Plivo step and `-o json` to read values. `plivo --help` wins on which commands and flags exist; this file wins on how Plivo behaves, including tested behaviour the help text omits. Never invent a flag: where the CLI has none, use `plivo api ` or the console, and say so. +- Every write is two commands: first with `--dry-run` (prints the request, sends nothing), then, after you show the preview and the user agrees, the same command with `--yes`. That covers renting, re-pointing a number, application changes, calls, compliance filings and `plivo api` writes. If a command rejects `--dry-run`, show the current state from `get` or `list` instead and ask. +- Before re-pointing a number, record its current `application` (`plivo numbers get -o json`). Rollback: `plivo numbers update --app-id `. +- Report each stage below as observed, not known or failed. Observed means you saw the evidence in this session. Never say "ready" and never infer account state. A `request_uuid`, an HTTP 2xx, a passing XML check or a WebSocket handshake is not success: a call worked only when the call record, the callbacks, the bot's log and a person who heard the audio agree. +- Verdicts: **will break** when a documented rule or an observed failure says so (name the failure); **risky** when plausible or untested (say what to check, name no hangup code); **style** when nothing changes. A rule here with no label is risky. -If neither is installed, do not stall and do not guess: use `plivo --help` for CLI questions and for XML questions, and say which source you used. +## Known issues (time-sensitive) -Why the checks matter: most first calls that fail do so before the agent is involved. The usual causes are a number attached to an application that is not the one serving your XML, an answer URL that rejects Plivo's request, a stopped tunnel, or a socket the bot closes on connect. Each stage below catches one of them for free, before a billed call. +- `plivo voice streams forward` cannot carry a live call on releases up to v1.1.2 or on a `-dev` or unrecognised build (`plivo --version`): its answer XML has no `keepCallAlive`, and from v1.1.0 it checks the stream upgrade's signature over `wss://` while Plivo signs it over `http://`, so it answers 403 (`--insecure-skip-signature` fixes only the 403). Treat it as fixed only on a later release whose notes say so (`plivo upgrade --check` reports any newer release). Until then, from a laptop (writes: `--dry-run`, then `--yes` after approval): + 1. An HTTPS tunnel to the bot, for example `ngrok http 7860`. + 2. The bot serves document A with the tunnel host in its `wss://` URL. Pipecat's example (`plivo docs show voice-agents/audio-streaming/integration-guides/pipecat/overview`) does, minus `statusCallbackUrl`, at the tunnel root for GET only (its README): in step 3, `--answer-url https:/// --answer-method GET`. + 3. `plivo account applications create --app-name voice-agent-test --answer-url https:/// --dry-run` + 4. Record the number's current `application`, then `plivo numbers update --app-id --dry-run`. + 5. Call (stage 5), then roll back to the recorded application. +- Docs: when piped, `plivo docs show` prints a JSON envelope, so add `-o table`. A page cut off at a `# ` in a code sample is complete at `https://www.plivo.com/docs/.md` (not the HTML page). In `plivo docs search`, quoted words match as a phrase; unquoted words must all appear. -## What a finding licenses you to say +## Stages 1 to 8: one flow from account to operations -Three tiers, and they decide your verdict. +First ask what is not known yet: inbound, outbound or both; what runs the agent (Pipecat, another framework, a speech-to-speech endpoint, your own server) and whether it has a public `wss://` URL; which country the numbers are in (India: read "India" first). Then run the stages in order and stop at the first failure. Stages 1 to 4 are free: do not place a call to discover configuration. Before launch, a tunnel host, a missing fallback URL or a `` without `statusCallbackUrl` count as failures. -- **Will break.** A documented rule says so, or a call with this shape is known to have failed. Say the call breaks, and name the failure. -- **Risky.** Plausible, unverified, or seen to work in some deployments. Say what could go wrong and what to check. Name no hangup code. -- **Style.** No functional effect. Say so. - -Nothing outside the first tier is a reason to tell someone their call will break. The checklist below is split into those tiers; when a rule elsewhere in this file does not say which tier it is in, it is risky. A true observation about a document is not by itself a verdict: a design risk you would raise in review is still a document that connects a call. - -What is in this file, in order: the readiness checklist, prerequisites by country, eight go-live stages, the CLI debugging loop with a hangup-code table, what not to do, how to compose and check the XML with documents you can copy, then the deep sections (India, outbound agents, callbacks and signatures, the WebSocket protocol, hangup causes, questions the docs do not answer). - -## Before you start: three questions - -1. Inbound (people call you), outbound (you call them), or both? -2. What runs the agent: Pipecat, another framework, a hosted pipeline or speech-to-speech model behind one realtime endpoint, or your own WebSocket server? Is it reachable at a public `wss://` URL yet, or only on a laptop? (Docs have step-by-step Pipecat and Cloudflare guides. The protocol is the same for all.) -3. Which country are the numbers in? Read "Prerequisites by country" before renting anything. - -## Readiness: five gates, each reported as observed, unknown or failed - -Report a gate as observed only when you saw the evidence in this session (a command's output, a log line, a console page). Write "not known" for anything you did not see; never assume it. Never say "ready". Do not place a live call merely to discover basic configuration; gates 1 to 4 are free. - -Run this checklist in order. Stop at the first failure and fix it there. - -1. `plivo auth whoami -o json`. Look for: the account you meant, and credits above zero. -2. `plivo numbers get -o json`. Look for: the number is on this account and `voice_enabled` is true. For an Indian number also look for `compliance_status`: it should read `accepted`, and `submitted` is not `accepted`. `compliance_status` is returned by the live API but is not in the published phone number schema, so treat a missing field as "not known" rather than as a failure, and confirm with `plivo numbers compliance list --country IN --status accepted -o json`. Note the `app_id`: it is your rollback value. -3. `plivo account applications get -o json`. Look for: an answer URL on a real host (https, not `localhost`, not a temporary tunnel host), a fallback answer URL, and a hangup URL. A number with no application attached cannot route a call. A number attached to a different application runs that application, not your XML, so your answer URL is never fetched: check what the number actually points at before you debug your own server. -4. `curl -s -i -X POST -d 'CallUUID=readiness&From=%2B10000000000&To=%2B&Direction=inbound&CallStatus=ringing&Event=StartApp'`. Look for: HTTP 200, a non-empty body, a response in well under 15 s, and no credential prompt. Then apply the XML checklist in "Check the XML before you serve it" to the body. -5. `plivo voice streams test --to --bidirectional --duration 5`. Look for: `Received N frames back`. -6. One real call, then `plivo voice calls get -o json` and `plivo voice calls diagnose `. - -| Gate | Observed when | Checklist step | -|---|---|---| -| 1. Account and number | the number exists, is voice-enabled, and (India) an accepted compliance application is attached | 1 and 2 | -| 2. Application | the number points at your XML application with answer, fallback and hangup URLs on a real host | 3 | -| 3. Answer URL | one request returned Plivo XML that passes the XML checklist | 4 | -| 4. WebSocket | `plivo voice streams test --bidirectional` received frames back | 5 | -| 5. A real call | an answered call with agent audio both ways, callbacks received, call record read | 6 | - -Steps 1 to 5 are static or synthetic. Only gate 5 proves signed HTTP from Plivo's edge, audio both ways, callbacks and the call record. Before launch, tighten the same checklist: a tunnel host, a missing fallback URL and a `` without `statusCallbackUrl` all count as failures rather than warnings. - -There is no single CLI command that runs the whole checklist. Run the steps by hand, in order. - -If your answer URL validates the Plivo signature (recommended), step 4 will be rejected unless you sign the request yourself. Either sign it with the recipe in "Callbacks, signature validation, timeouts", or read the response of a real call from the console instead. A 401 from an unsigned probe proves nothing. - -## Prerequisites by country - -India first, because nothing works until this is done. Detail and every command: "India: what a voice agent needs before its first call". Docs: , , , , , . Never infer country rules from a phone prefix; check the account. - -1. Account. An India data-region organisation is required and cannot be changed later. Create a new org from the console switcher if you are in the US region. Only India-registered businesses may rent Indian numbers. INR accounts can only call within India. `plivo auth whoami -o json` shows the account. There is no CLI for data region. -2. KYC. One rule: run `plivo numbers compliance requirements --country IN --number-type local --user-type business -o json` and supply every document type it returns. - The public pages disagree on the count, so do not hard-code one or two. The first application must be sealed and signed. - Review for 080 and 022 numbers is automated, typically about 5 minutes. `submitted` is not `accepted`. - An agent can run the whole flow from the CLI: `requirements`, `create` with the files, poll `get`, then `buy` or `link`. - It needs the certificate files and the exact legal details from the user and must not fill any of them in itself. Preview, then ask before every `--yes`. -3. Compliance must be `accepted` to rent and to call. Otherwise rent fails with `compliance_application_id is required` and `calls make` fails with `from number +91... cannot place calls as its compliance application is not in 'accepted' status`. Attach an application to an existing number with `plivo numbers compliance link --link +91...=`. -4. Number series. Landline (022, 080): service and transactional calls only. 140-series: promotional only (Tata DLT registration, declaration, NOC, header and template approval; about 5 to 10 business days; no CLI). 160-series: BFSI only (about 7 to 14 business days). The wrong series makes every complaint count as UCC even with consent. Which series allow `` is not documented. -5. Media anchoring. Both legs of every call must stay in India, your server too. Otherwise the call fails with 2070 `violates_media_anchoring`. -6. Consent. Cold calling is prohibited. A UCC complaint needs opt-in proof within 5 business days, or the compliance ID is blocked for 15 days. Five or more complaints in 10 days means suspension. Never call a complainant again. UCC API via `plivo api` (no typed command). -7. Capacity. Default concurrency limit of 50 (CPS = concurrency / 25). Over the limit is rejected instantly with 5030. Raise it through a support ticket before a campaign. - -US. No KYC and no 10DLC for voice. Detail: "Outbound voice agents". - -- Use a Plivo number you own as caller ID. It is the only way to get STIR/SHAKEN attestation A. A verified external number may be used but gets no attestation A and no protection from spam labels; Caller Reputation is optional and paid. -- Stay under the quality thresholds: abandoned calls under 20% and short calls (6 s or less) under 10% of monthly volume, or surcharges apply. -- Default 2 CPS; overflow is queued, not rejected. Professional plans can call only US and India. -- US-region trial organisations may see "Voice capability is currently disabled for this account" on outbound. Request access from the console. This is not in the docs. - -Other countries: check coverage, geo permissions and caller-ID rules on the public pages. If a rule is absent, say "not known" and ask Plivo support. - -## Stage 1: account and number (5 minutes) +### Stage 1: account and number ```bash -plivo auth whoami -o json # right account? credits > 0? -plivo numbers list --services voice -o json # a voice-enabled number? -plivo numbers compliance list --country IN --status accepted -o json # India only: must be non-empty -plivo numbers search --country IN --type local --limit 5 -plivo numbers buy --dry-run # preview; then, after the user agrees: --yes (spends money) +plivo auth whoami -o json # the account you meant, credits above zero +plivo numbers get -o json # on this account, voice_enabled true; record `application` +plivo numbers search --country US --type local --limit 5 # no number yet +plivo numbers buy --dry-run # spends money ``` -Check: the number exists and `voice_enabled` is true. India: compliance is `accepted`. A number hosted at another carrier can still reach the agent: forward it to a Plivo number, or have the carrier send SIP to `sip:@app.plivo.com` with SIP authentication (docs: ). +- `application` is a URI ending `/Application//`; table output shows it as `app_id`. +- Wrong organisation: `plivo auth list`, then `plivo auth use `. Running `plivo login` for another organisation saves one profile per organisation. Data region is console only. +- A number at another carrier: forward it to a Plivo number, or have the carrier send SIP to `sip:@app.plivo.com` with SIP authentication: `plivo docs show voice/use-cases/connect-external-numbers` (). +- Observed when: the number is on this account and voice-enabled and, for India, its compliance application is `accepted`. -## Stage 2: an application with all three URLs (5 minutes) +### Stage 2: an application with answer, fallback and hangup URLs ```bash -plivo numbers get -o json # record the current application id first: it is your rollback plivo account applications create --app-name voice-agent \ - --answer-url https://YOUR-HOST/plivo/answer --answer-method POST \ - --fallback-answer-url https://YOUR-HOST/plivo/fallback \ - --hangup-url https://YOUR-HOST/plivo/hangup --dry-run # preview, then --yes -plivo numbers update --app-id --dry-run # preview, then --yes -plivo numbers get -o json # confirm the application points at the new app + --answer-url https://HOST/plivo/answer --answer-method POST \ + --fallback-answer-url https://HOST/plivo/fallback \ + --hangup-url https://HOST/plivo/hangup --dry-run +plivo numbers update --app-id --dry-run +plivo numbers get -o json # confirm the new application ``` -Check: the number points at your XML application, the one whose answer URL returns ``. The most common first-call failure is a number still pointing at a different application, so your answer URL is never fetched and the caller hears whatever that application does. A console flow application that has no working flow answers with JSON rather than Plivo XML, which the call record reports as 8011. Never leave the fallback URL empty. `applications update` has no fallback flag: set it at create time, or use the API, previewing first: +- The most common first-call failure: the number points at another application, so your answer URL is never fetched. A console flow application with no working flow answers with JSON, which the call record reports as 8011. Check what the number points at before you debug your server. +- `applications update` has no fallback flag. Set it at create time, or `plivo api POST /Application// --body '{"fallback_answer_url":"https://HOST/plivo/fallback"}' --dry-run`, then `--yes`. +- Observed when: the number points at your application and all three URLs are on a real https host (a tunnel only while testing). -```bash -plivo api POST /Application// --body '{"fallback_answer_url":"https://YOUR-HOST/plivo/fallback"}' --dry-run # shows the exact request -plivo api POST /Application// --body '{"fallback_answer_url":"https://YOUR-HOST/plivo/fallback"}' --yes # only after the user approves -``` - -Rollback: `plivo numbers update --app-id ` (preview it with `--dry-run` first). - -## Stage 3: prove the WebSocket, no phone involved (2 minutes) +### Stage 3: prove the WebSocket, no phone involved ```bash -plivo voice streams test --to ws://localhost:7860/ws --bidirectional --duration 5 # laptop first (loopback only) -plivo voice streams test --to wss://YOUR-HOST/ws --bidirectional --duration 5 # then the public host +plivo voice streams test --to ws://localhost:7860/ws --bidirectional --duration 5 # laptop +plivo voice streams test --to wss://HOST/ws --bidirectional --duration 5 # public host ``` -Check: you see `Received N frames back`. If not, your server accepts the connection but never sends `playAudio`, so callers will hear silence. If the connection fails, the server is not public, not TLS, or not a WebSocket at that path. Fix here; this is free. Two limits worth knowing. The CLI's own help calls this a pure-client pre-flight: no call is placed and Plivo's backend is not involved, so frames coming back prove your endpoint speaks the Plivo stream shape and nothing more. And the client ends the run by sending a JSON `{"event":"stop"}` text frame and then closing the socket, so a bot that waits for `stop` will see it here. Handle both anyway: treat either the `stop` frame or the WebSocket close as the end of the stream, because a real call can drop without a clean stop. Run it with `--codec mulaw` and `--codec l16` separately if you support both, and read the codec your bot actually received from the `start` frame rather than assuming. +Observed when: `Received N frames back` (`frames_read_back` above 0 in JSON). That proves the endpoint accepts a connection and replies, nothing more: -What the bot must speak (schemas and handling rules in "The WebSocket protocol"): +- No call is placed and no signature is sent. A bot that rejects unsigned upgrades fails here: test a local copy with the check off, or report the stage as not known. +- The frames are reduced: no `sequenceNumber`, `extra_headers`, `start.tracks` or media `streamId`, and `chunk` is a string. Any reply counts, not only `playAudio`. +- With `--bidirectional` the CLI streams for `--duration` seconds, reads for another `--duration`, then drops the TCP connection: no `stop` frame and no close frame. Only without `--bidirectional` does it send `{"event":"stop"}` and close normally. Real calls can drop too, so end the stream on a closed or dropped socket, never only on `stop`. +- If you support both codecs, run `--codec mulaw` and `--codec l16 --rate 16000` separately (`--duration` max 30). -- Plivo sends JSON text frames: `start` (callId, streamId, mediaFormat, your `extra_headers`), then about 20 ms `media` chunks (base64 raw audio, no WAV header), `dtmf` on keypress, `playedStream` when a checkpoint plays, `clearedAudio` after a clear. Read the codec from `start.mediaFormat`, not from what you think the XML said. -- The bot sends `playAudio` (contentType and sampleRate must match the XML), `clearAudio` for barge-in, `checkpoint` to learn when a sentence finished, `sendDTMF` to drive an external IVR. -- Send audio at real-time cadence. Bursting fills the playback buffer and makes barge-in late. If the initial WebSocket connection fails, Plivo attempts twice more before disconnecting the stream, and closes the socket when the call ends (, "WSS Socket Connection Failures"). +A connect failure means not public, not TLS, the wrong path, or a rejected unsigned upgrade. No frames back means the bot never replies or chokes on the reduced frames. -## Stage 4: the XML (5 minutes) +### Stage 4: the answer URL returns valid XML -Use the XML section below: answer the questions, copy the closest document, edit it, then check what your server actually returns. +Compose the document from "The XML" below, then probe what the server really returns: ```bash -curl -s -X POST https://YOUR-HOST/plivo/answer \ - -d 'CallUUID=test&From=%2B91...&To=%2B91...&Direction=inbound&Event=StartApp' +curl -s -i -X POST https://HOST/plivo/answer \ + -d 'CallUUID=readiness&From=%2B10000000000&To=%2B&Direction=inbound&CallStatus=ringing&Event=StartApp' ``` -Check: the body passes every line of "Check the XML before you serve it". That proves the document only: not that Plivo can fetch it (a 7011 leaves no trace in the body), not that a call connects. The document must have ``, and `keepCallAlive="true"` on every document except the one that puts the caller in a MultiPartyCall room, where the room has to run after the stream starts (docs: ). Your answer URL must accept POST without Basic or bearer credentials and must not sit behind login middleware that a signed Plivo request cannot pass. Require a valid `X-Plivo-Signature-V3` instead (recipe in "Callbacks, signature validation, timeouts"). The XML overview gives Plivo a 15-second timeout for XML responses, so answer well inside that. Optional: `noiseCancellation="true"`. +Observed when: HTTP 200, `Content-Type` `application/xml` or `text/xml`, a body that passes "Check the XML before you serve it", and a reply well inside Plivo's 15-second XML timeout. -No CLI command fetches the answer URL the way Plivo does. Use the `curl` above and the checklist, and remember that `curl` sends no `X-Plivo-Signature-V3`: if your endpoint validates signatures, sign the probe yourself with the recipe in "Callbacks, signature validation, timeouts" or read the body of a real call from the console instead. +- The URL must accept the application's method without Basic or bearer credentials; require a valid `X-Plivo-Signature-V3` instead. If it validates signatures, this unsigned probe gets a 401 that proves nothing: sign it (see "Callbacks, signature validation, timeouts") or read the body Plivo fetched in the console debug logs. +- No CLI command fetches the answer URL the way Plivo does, and no body check can see a 7011. -## Stage 5: first real call, still on a laptop (10 minutes) +### Stage 5: the first real call -If the agent is only local: `plivo voice streams forward --number --app --to ws://localhost:7860/ws` saves the app's current answer URL, starts a tunnel and a local HTTP and WebSocket server, points the app at the tunnel, and restores the original answer URL on Ctrl-C. Its help says it needs no tunnel setup: it defaults to localhost.run over ssh, which needs no install and no account, and uses ngrok only when ngrok is already on the PATH or at `~/.plivo/bin/ngrok`. `--tunnel auto | ngrok | localhost.run` forces the choice. The only mutation is that one field on that one app. Read `plivo voice streams forward --help` before you run it, because it does change your application for the session. Then dial the number from a phone, or: +Test inbound first: a failed outbound answer URL is a billed call that drops when the callee answers. Dial the number from a phone, or call yourself: ```bash -plivo voice calls make --from --to --answer-url https://YOUR-HOST/plivo/answer --answer-method POST --dry-run # preview, then --yes +plivo voice calls make --from --to \ + --answer-url https://HOST/plivo/answer --answer-method POST --dry-run plivo voice calls list --limit 1 -o json -plivo voice calls diagnose +plivo voice calls get -o json ``` -Check: the call is answered, you hear the agent, and the call record ends with `Normal Hangup` (4000) or `End Of XML Instructions` (4010). 4010 is the normal end of a keepCallAlive stream. A call that ends 4010 within a few seconds of answer is worth taking to the bot's connect handler first, but the call record does not say who closed the socket: confirm in the bot's logs and in the stream status callbacks before you call it a bot fault. A `request_uuid` from `calls make` means Plivo accepted the request, nothing more. Anything else: go to "When a call fails". +`calls make --answer-method` defaults to GET. A `request_uuid` means Plivo accepted the request, nothing more. -## Stage 6: move to production hosting (before anyone else dials) +A bot only on a laptop: the "Known issues" recipe or, on a fixed release, `plivo voice streams forward --number --app --to ws://localhost:7860/ws`. It points that application's answer URL at a tunnel and restores it on Ctrl-C, so every number on the application reaches your laptop meanwhile: use a test application. Its `` has no `statusCallbackUrl`, so it does not test your XML, and it reaches your bot without signature headers. Without a terminal, pass `--yes` and `-o table` (JSON mode prints nothing until it exits). -- Own domain with a valid certificate, hosted near the callers (Mumbai for India, US East or West for the US; the docs' latency budget is under 1 s end to end). Free tunnel URLs are temporary: a stopped or rotated tunnel turns every call into a 7011. Use a stable host before launch. -- Re-run stage 4's `curl` and checklist and stage 3's `streams test` against the production host. -- `plivo account applications update --answer-url https://PROD-HOST/plivo/answer --hangup-url https://PROD-HOST/plivo/hangup --dry-run`, show the previewed request and the current values you are replacing, then run the same command with `--yes` once the user approves (fallback via `plivo api`, see stage 2). -- Set `statusCallbackUrl` on ``. Callbacks surface `DroppedStream` to your own monitoring; the console's Audio Streams debug logs remain a manual fallback. Make every callback handler idempotent on `CallUUID` (Plivo retries). -- Alert on the 7011 rate. An answer URL that fails under load with no fallback URL produces recurring 7011s long after launch; treat that as an availability incident, not a setup problem. -- Walk the readiness checklist once more against the production host before the first external caller. +Observed when: a person heard the agent and the agent heard them, the callbacks arrived, and the record ends 4000 (Normal Hangup) or 4010 (End Of XML Instructions, the normal end of a keepCallAlive stream) after a real conversation. -## Stage 7: handoff to a human (only if you need it) +Silence, then 4010 within seconds: the call record does not say who closed the socket, but the console's Audio Streams log gives each stream's hangup reason (API request, call hangup, connection error, stream timeout). Most likely first: -Production pattern: your backend calls `plivo voice calls transfer --legs aleg --aleg-url https://PROD-HOST/plivo/transfer/`. The transfer URL returns `...` or `sip:...`, optionally followed by another `` so the caller returns to the bot if nobody answers. Read `DialStatus` (`completed|busy|failed|cancel|timeout|no-answer`) on the action URL. +1. The XML: no `keepCallAlive` or `bidirectional`, or the number on another or a stale application. +2. The socket never connected: tunnel, TLS or path, or the bot rejected the upgrade by checking its signature over `wss://`, not `http://`. +3. The bot crashed on start or on the first `media` frame. +4. Nothing playable came back: wrong `contentType` or `sampleRate`, a WAV header inside the base64, or audio not paced in real time. +5. A fixed ceiling: `streamTimeout` or the bot's own idle timer. -Ordering. Both points below are risky rather than documented. No page states either, so verify them once on your own account and write down what you saw. +### Stage 6: production hosting -- Call the Transfer API first, then close the socket. The API answers 202 at once; the transfer URL appears to be fetched when the bot closes the WebSocket. -- Avoid `DELETE .../Stream/` first. With `keepCallAlive` and nothing after ``, stopping the stream is the end of the document, and a document that runs out ends the call, so the transfer may find no live call to act on. That is an inference from the documented `keepCallAlive` behaviour, not a rule Plivo publishes, and no page names a hangup code for it. -- Alternatives without the API, in the same document: `` after ``, or `` after `` to a URL that decides what happens next. Closing the socket runs it (docs: keepCallAlive, ). +- Your own domain with a valid certificate, near the callers (Mumbai for India, US East or West for the US; the docs target under 1 s end to end). A stopped or rotated tunnel turns every call into a 7011. +- Re-run stages 3 and 4 against the production host. Then `plivo account applications update --answer-url https://PROD/plivo/answer --hangup-url https://PROD/plivo/hangup --dry-run`, show the current values being replaced, and apply after approval (fallback via `plivo api`, stage 2). +- Set `statusCallbackUrl` on ``: it is the only push signal for `DroppedStream` and `DegradedStream`. +- Make every callback handler idempotent, because Plivo retries: key stream callbacks on `StreamID` plus the event, call callbacks on `CallUUID`. +- Alert on the 7011 rate: an answer URL that fails under load keeps producing 7011s long after launch. Plivo's Voice Alerts also email you when callback failures pass 5%: `plivo docs show voice/concepts/voice-alerts` (). +- Walk stages 1 to 5 once more against production before the first external caller. -Copy documents F1 and F2 below for the two-document form, or D for the redirect form. The docs recommend SIP over a phone number for contact centres: `sip:queue@your-cc.example.com`. Failures come back as 4240 `sip_auth_failed` or 4250 `sip_auth_timeout`. The `` attributes a handoff actually needs are all used in F2 and listed in the defaults table below: `callerId`, `timeout`, `redirect`, `action`, `method`, plus `dialMusic` and `timeLimit`. If you need the complete `` attribute table, the simultaneous and sequential dialling shapes, or the full callback parameter list, that is general XML: install `plivo-voice-xml` (`npx skills add https://www.plivo.com/docs --skill plivo-voice-xml`) or read . Allow Plivo's media IPs at the contact centre. Set `dialMusic` so the caller does not hear silence. Many human legs never answer (busy, cancelled, SIP endpoint offline 2020, no answer): handle `DialStatus` and put `` or `` after ``. A busy or unanswered B-leg is a handoff outcome, not a Stream failure. +### Stage 7: hand off to a human -Check: one test transfer, `DialStatus=completed` on your callback, the caller and the human hear each other, and the bot's audio has stopped. A transfer URL that fails shows as 7013 or 8013 on the call. +Your backend calls `plivo voice calls transfer --legs aleg --aleg-url https://PROD/plivo/transfer/`, and that URL returns document E below. -Warm transfer with the AI inside a MultiPartyCall (`role="ai-agent"`): the API fields are documented, the runtime behaviour is not verified. Read "AI agent as a MultiPartyCall participant" before proposing it. +- Call the Transfer API first, then close the socket, and keep `keepCallAlive="true"`: the transfer XML is subsequent XML, so it runs only when the stream ends. To stop the stream through the API instead (`plivo voice calls streams stop --yes`), transfer first, then stop: stopping first lets the leg finish its current document, which can end the call. +- In the same document instead: a `` or `` after `` runs when the socket closes. +- The Dial `action` URL receives `DialStatus` (`completed`, `busy`, `failed`, `cancel`, `timeout`, `no-answer`), `DialRingStatus`, `DialHangupCause`, `DialALegUUID` and `DialBLegUUID`. The human leg's `DialBLegHangupCauseCode`, `DialBLegHangupCauseName` and `DialBLegHangupSource` go only to the Dial `callbackUrl`. +- Contact centres: prefer SIP, `sip:queue@cc.example.com`; failures are 4240 (auth failed) and 4250 (auth timeout). The centre must allow Plivo's SIP signalling and RTP ranges: `plivo docs show voice/concepts/firewall-network-configuration` (). Set `dialMusic` so the caller does not hear silence. +- Many human legs never answer: handle `DialStatus` and keep a `` or `` after ``. A busy human is a handoff outcome, not a stream failure. +- Observed when: one test transfer reports `DialStatus=completed`, caller and human hear each other, and the bot's audio has stopped. A failing transfer URL shows as 7013 or 8013. +- An AI as a MultiPartyCall participant (`role="ai-agent"`) is documented (`plivo docs show voice/xml/multiparty-call`, ; `plivo docs show voice/api/multiparty-calls`, ), but its runtime behaviour is not verified and the docs do not say what `to` should be for a participant reached over a WebSocket: ask Plivo before you build on it. `participant add --role` has no `ai-agent`, so it needs `plivo api POST /MultiPartyCall/name_/Participant/`. -## Stage 8: operating it +### Stage 8: operating it | Watch | How | |---|---| -| Calls that did not connect to the agent | `plivo voice calls list --limit 50 -o json`, filter `hangup_cause_name` not in `Normal Hangup`, `End Of XML Instructions` | -| Any one failure, explained | `plivo voice calls diagnose ` (see the loop below) | -| Stream drops | your `statusCallbackUrl` receiving `DroppedStream` or `DegradedStream`; log the raw `Event` string, the docs use two naming schemes | -| Humans hanging up in the first seconds | common on streamed calls; speak first and fast, keep any `` before `` short. A short call is not proof of a bot fault | -| Ceilings cutting conversations | calls ending 4010 at exactly the same second every time: your `streamTimeout` or your own timer | -| Recordings | `plivo voice recordings list --call-uuid `; `` before ``, `recordSession="true"` | -| Outbound campaigns | measure your own answer and connect rate by destination, list source and time window; `--machine-detection true` (async: `Machine=true` hits `machine_detection_url`, set via `plivo api`, no CLI flag) and `--ring-url`; do not hang up on every voicemail (short-call surcharges); stay inside your CPS (default 2; India: the concurrency limit) | -| Before launch, rehearse | silence, barge-in, DTMF, bot timeout, socket refused and socket dropped mid-call, malformed `playAudio`, transfer to a busy human, recording on and off, and the stage 2 rollback | - -## When a call fails: debug with the CLI, in this order - -Run each layer once and keep the evidence (command output, redacted body, log line). Do not repeat a billable call until the failed layer passes. - -1. `plivo voice calls diagnose `. Plivo's AI debugger reads the call record, the SIP and media trace and your answer-URL responses. It usually takes about a minute; allow 30 to 120 s. The answer is AI-generated text, not a stable schema. It shares a small per-account rate limit with `plivo ask`, so do not loop it. Only calls on your own account can be diagnosed. -2. `plivo voice calls get -o json`. Read `hangup_cause_code`, `hangup_cause_name`, `hangup_source`, `answer_time`, `bill_duration`; for a handoff read the B-leg too. If a field is missing, `plivo api GET /Call//` has the full record. Map the code with the table below; the full list is in "Hangup causes and end-of-call patterns". -3. Reproduce the answer URL: `curl -s -i -X POST -d 'CallUUID=x&From=%2B...&To=%2B...&Direction=inbound&Event=StartApp'`, then apply the XML checklist to the body. The headers tell you 401, 405, 404 or 530; the checklist tells you what in the XML would break the call. A 7011 is an HTTP failure: the body alone cannot show it, so read status, method, proxy and signature logs. For 7013 or 8013 do the same against the transfer URL, where no `` is required. -4. Reproduce the socket: `plivo voice streams test --to wss://... --bidirectional --duration 5`. No frames back means the bot never sends `playAudio`. A connect failure means not public, not TLS, or wrong path. -5. Still unclear: console Voice, Logs, Calls, the call, Audio Streams, Debug logs (stream events, `DroppedStream` error text) and Call Insights (audio quality flags: one-way, broken, robotic, lag). Then `plivo ask --call-uuid "..."`, or Plivo support with the escalation packet in "Hangup causes and end-of-call patterns". - -| Code / name | Plain meaning | Fix | -|---|---|---| -| 7011 Error Reaching Answer URL | No usable HTTP response: 404, 401/403 (your auth), 405 (wrong method), 530/502 (dead tunnel), timeout, empty body. Invisible to static XML checks | Step 3; accept POST without Basic or bearer credentials; fallback URL set; answer under 15 s | -| 8011 Invalid Answer XML | Your URL answered but not with Plivo XML: JSON (often from an application that is not the one you wrote), HTML, malformed XML, an unsupported ``, a Twilio element such as `` | Confirm the number points at your own XML application (stage 2); run the checklist on the exact body | -| 4010 End Of XML Instructions | Normal end. With keepCallAlive, the XML ran out when the socket closed | Nothing. A 4010 a few seconds after answer is a reason to read the bot's connect handler and the stream status callbacks; the call record alone does not say who closed the socket | -| 3020 Rejected / 3010 Busy Line, source Answer XML, 0 s | Your XML returned `` or `` first. The routing page documents the audible effect but not the code; this pairing comes from call records, not a docs page | Intended? Fine. A bare `` has not been seen to produce these codes; it ends the call gracefully | -| 8012 / 7012 Action XML | The second document (a GetDigits, GetInput, Record or Dial action URL) was bad or unreachable | Apply the checklist to it, without the `` requirement. An action document follows the same contract: HTTP 200, `application/xml` or `text/xml`, one well-formed `` | -| 7013 / 8013 Transfer URL | The stage 7 transfer URL failed or returned bad XML | Step 3 against the transfer URL | -| 6020 Media Timeout | No media packets for 60 s (docs). This code alone does not say which side lost media | Check both media paths and the carrier; Call Insights | -| 6000 Scheduled Hangup | Max duration (default 4 h; `time_limit`) | Intentional? | -| 6010 Ring Timeout | Callee never answered (default 120 s) | Outbound: expected; tune `ring_timeout` | -| 1000 Cancelled, source API Request | Your backend hung up via the API | Nothing, if intended | -| 0 Unknown | Undetermined (docs: a known bug with Delete All Calls) | Debug logs; support if it recurs | -| 2070 Violates Media Anchoring / 5030 Concurrency Limit Breached | India: a leg or your server is outside India / over the concurrent-call limit, rejected instantly | India section, media anchoring and capacity | -| 3030 Unknown Caller ID | Caller ID is neither a number rented on this account nor an accepted verified caller ID for this route | Use a Plivo number you rent | -| 2030 Destination Country Barred | Geo permissions (Professional plan: US and India only) | Console, Voice, Geo Permissions | -| 9100 Machine Detected | Voicemail with `machine_detection=hangup` | Expected; mind short-call thresholds | -| 3000 / 3080 / 3070 / 3050 / 2000 | Carrier and destination failures on outbound campaigns | List hygiene, retries with backoff; hangup-causes section | -| 4240 / 4250 sip_auth_failed / timeout | SIP handoff credentials rejected or no response | Check `sipAuthUsername`, `sipAuthPassword`, realm, IP allow-list | -| `DroppedStream` (status callback, not a hangup code) | The socket failed to connect or died mid-call | Server or tunnel went away; `DegradedStream` first means too slow | - -## What not to do (each one a common failure in practice) - -- Do not let a greeting or a menu break the document. A `` or `` Plivo does not accept costs you the whole document, and with no fallback URL the caller gets nothing. The documented lists are short, so keep a copy: `` with the generic `WOMAN` and `MAN` voices documents `da-DK`, `nl-NL`, `en-AU`, `en-GB`, `en-US`, `fr-FR`, `fr-CA`, `de-DE`, `it-IT`, `pl-PL`, `pt-PT`, `pt-BR`, `ru-RU`, `es-ES`, `es-US`, `sv-SE`, and a `Polly.` voice covers many more, including `hi-IN` with `Polly.Aditi` and `en-IN` with `Polly.Raveena`. `` speech lists `en-US`, `en-GB`, `en-AU`, `es-US`, `es-ES`, `fr-FR`, `de-DE`, `it-IT`, `pt-BR`, `ja-JP`, `zh-CN` under the heading "common languages include", so that list is explicitly not exhaustive: test any other code before you rely on it (docs: , , ). The safe alternative is to let the bot speak the greeting over the stream. -- Do not put tokens or passwords in the WebSocket URL, `extraHeaders` or callback URLs. They appear in Plivo logs. Validate the Plivo signature instead (). -- Do not return `` as a placeholder. It ends the call gracefully the moment it runs, so the caller gets a call that answers and immediately stops, and in the call record it looks like a document that finished normally. Return `` instead while you build. Deliberate screening is `` or ``, which are the forms that give the caller a rejection or busy signal. -- Do not test outbound first. A failed outbound answer URL is a billed call that drops as soon as the callee answers. Prove inbound or `streams test` first. -- Do not rely on the codec default. Set `contentType` explicitly; the default differs between docs pages. -- Do not cold-call in India, and do not hang up on every voicemail in the US. Both are penalised (UCC, short-call surcharges). -- Do not copy Twilio XML. ``, ``, ``, `` and `` are not Plivo elements. Answer documents carrying a top-level `` have been observed to fail with 8011; a top-level `` and a nested `` have been seen to be ignored instead, and the docs do not say which happens, so fix them all and predict a code only for ``. The Plivo forms are ``, ``, `` and ``. -- Do not read a carrier code, a B-leg outcome, an out-of-credit cancel or a human hangup as proof of a WebSocket defect. Each has its own row in the hangup-causes section. +| Failed calls | `plivo voice calls list --limit 20 -o json` (20 is the API's per-page maximum: page with `--offset`; the API searches the last 7 days by default). Read `hangup_cause_code`, `hangup_cause_name`, `hangup_source` and the duration. Do not filter out 4010: a refused or crashed stream ends exactly that way, so look for 4010s that last seconds | +| One failure | "When a call fails" below | +| Stream drops | `DroppedStream` and `DegradedStream` on your `statusCallbackUrl`; log the raw `Event` (the docs use two naming schemes) | +| Callers hanging up in seconds | Common on streamed calls and not proof of a bot fault. Speak first and fast; keep any `` before `` short | +| A ceiling | Calls ending 4010 at the same second: `streamTimeout` or your own timer | +| Recordings | `plivo voice recordings list --call-uuid ` | +| Rehearse before launch | Silence, barge-in, DTMF, a bot timeout, a refused socket and one dropped mid-call, malformed `playAudio`, a busy human, recording on and off, the stage 2 rollback | -## The XML: compose it, check it +## When a call fails -This section is about the document that carries a ``: the documents to copy, the defaults to set, and the checks that decide whether a streamed call connects. It is complete for that job. For a document with no stream in it at all, and for the full attribute table of every other element, install `plivo-voice-xml` (`npx skills add https://www.plivo.com/docs --skill plivo-voice-xml`) or read . +Run each layer once, keep the evidence, and do not repeat a billed call until the failed layer passes. -One rule shapes half these documents, and it is a risk to raise, not a verdict. On ``, ``, ``, `` and `` the `redirect` attribute defaults to `true`, so when that element's `action` URL answers, Plivo runs the document it returns. What the docs do not say is that the elements below are dead. The input page states that after `retries` attempts with no input, execution continues to the next element, and the routing page shows a `` followed by a fallback that runs when the dial fails or times out. So the elements below are reachable on the nothing-happened path and skipped on the path where the caller does respond. Raise it as a risk: say the `` below may never open for a caller who presses a key, and offer `redirect="false"` or a repeat of the `` in the action document. Documents shaped this way run to a normal hangup every day, so never answer that the call breaks. The strongest case in this family is ``, and even that is not a documented failure: the routing page says it transfers call execution to a different URL and Plivo continues the call there, but no page states that siblings below it are skipped, so treat content under a `` as dead code to move rather than a broken call (docs: , , ). +1. `plivo voice calls diagnose `: Plivo's AI reads the call record, the SIP and media trace and your answer-URL responses. Allow 30 to 120 s; the answer is free text, not a schema; own account only. It shares a small per-account rate limit with `plivo ask`, so do not loop it. +2. `plivo voice calls get -o json`: `hangup_cause_code`, `hangup_cause_name`, `hangup_source`, `answer_time`, `bill_duration`; after a handoff read the human's leg too. `plivo api GET /Call//` has the full record. +3. Re-run the stage 4 `curl` against the URL that failed: the answer URL, or the action, transfer or redirect URL for 7012/8012, 7013/8013 or 7014/8014 (those documents need no ``). Headers show 401, 404, 405 or 530; the XML check finds body faults. +4. Re-run stage 3 against the stream URL. +5. Console: Voice, Logs, Calls, the call. Audio Streams has the stream's debug logs (events, the `DroppedStream` error text); Call Insights flags one-way, broken or robotic audio and lag. Then `plivo ask --call-uuid ""`, or Plivo support with every leg's call UUID, UTC times, the code, name and source per leg, the answer URL's host only, HTTP status and latency from your logs, the exact XML body (redacted), the stream callbacks received, the WebSocket close code, and whether stage 3 passes. Never send tokens or caller audio. -### Ask these questions (skip any already answered) - -1. Direction: inbound, outbound, or both? -2. Your WebSocket URL. Should start with `wss://`. `ws://localhost` needs a tunnel first (stage 5). -3. Audio format: `mulaw 8 kHz` (default, native telephony, the most common choice) or `L16 16 kHz` (when the speech model wants 16 k). If unsure, mulaw 8 kHz. -4. Record the call? If yes: where to post the recording URL, mono or stereo. -5. Say anything before the agent joins? A greeting or a recording notice. A keypad menu first? -6. Hand off to a human? Never, dial a number, dial a SIP address, redirect to a URL that decides, or put the caller in a room. Should the bot take the call back if the human does not answer? -7. Where should Plivo report stream problems? A status callback URL. Strongly recommended. +| Code or signal | Meaning on a streamed call | Next | +|---|---|---| +| 7011 Error Reaching Answer URL | No usable HTTP response: 404, 401 or 403 (your auth), 405 (method), 502 or 530 (dead tunnel), a timeout | Step 3; no credentials on the URL; a fallback URL | +| 8011 Invalid Answer XML | A 200 that is not Plivo XML: empty, JSON (often a console flow application, stage 2), HTML, malformed, an unsupported ``, Twilio `` | The number's application; the XML check on the exact body | +| 4010 End Of XML Instructions | The normal end of a keepCallAlive stream. Within seconds of answer, also how a refused or crashed stream ends | The stage 5 checklist | +| 4000 Normal Hangup | A person hung up. Source `Answer XML` means your `` ran | Nothing | +| 3020 / 3010, source Answer XML, 0 s | `` or `reason="busy"` ran first (seen in call records; no docs page maps it). A bare `` does not produce these | Intended? | +| 7012/8012, 7013/8013, 7014/8014 | The action, transfer or redirect URL was unreachable, or returned bad XML | Step 3 against that URL | +| 6020 Media Timeout | No media for 60 s; the code does not say which side | Both media paths, the carrier, Call Insights | +| 2070 / 5030 | India only: a leg or your server is outside India / any region: over the concurrency limit, rejected at once | "India" / raise the limit | +| 3030 / 2030 | The caller ID is neither rented on this account nor a verified caller ID / the destination is barred by geo permissions | Your Plivo number; Voice, Geo Permissions | +| 9100 Machine Detected | `machine_detection=hangup` met a voicemail | Expected | +| `DroppedStream` (stream callback) | The socket failed to connect or died mid-call; a `DegradedStream` before it means too slow | Server, tunnel, pacing | + +Every other code, including the carrier codes an outbound campaign sees daily: `plivo docs show voice/troubleshooting/hangup-causes` (). A carrier code, a busy human, an out-of-credit cancel or a person hanging up is not a WebSocket defect. -Do not ask about `keepCallAlive`, `streamTimeout`, `audioTrack`, `noiseCancellation` or `extraHeaders` unless the user raises them. Set the defaults below. +## The XML: compose it, check it -### The documents to copy, most common first +Ask only what is unknown: the direction; the `wss://` URL; the codec (`audio/x-mulaw;rate=8000` unless the speech model wants `audio/x-l16;rate=16000`); whether to record and where the recording URL goes; anything before the agent (a greeting, a recording notice, a keypad menu); the handoff (none, a number, a SIP address, a URL that decides, a room) and whether the bot takes the caller back; a stream status callback URL (strongly recommended). Do not ask about `keepCallAlive`, `streamTimeout`, `audioTrack`, `noiseCancellation` or `extraHeaders`: use the attribute table. -Each is one well-formed document. Replace the hosts, the numbers and the placeholders. `{{CallUUID}}`, `{{From}}` and `{{To}}` are placeholders for your server to fill in before it returns the XML: Plivo does not substitute them. Most deployments pass a short id as a query string or a path segment. +Copy the closest document. `{{CallUUID}}` and `{{To}}` are for your server to fill in before it returns the XML; Plivo substitutes nothing. A `` or `` placed after the `` runs when the socket closes. -A. Stream only. The shape most deployments run. +**A. Stream only**, the shape most deployments run. ```xml @@ -270,7 +187,7 @@ A. Stream only. The shape most deployments run. ``` -B. Record, then stream. `` must come first: with keepCallAlive an element after the stream runs only once the stream ends. +**B. Record, then stream.** `` must come first: with keepCallAlive, anything after `` runs only once the stream ends. ```xml @@ -279,42 +196,18 @@ B. Record, then stream. `` must come first: with keepCallAlive an elemen ``` -C. Greeting and a keypad menu, then stream. Everything before `` delays the bot's first word by that much. - -`redirect="false"` on the `` is what makes this reliable. Without it `redirect` defaults to `true`, so a caller who presses a key gets whatever `/plivo/menu` returns and the `` and `` below are skipped for that caller. A caller who presses nothing still falls through to them after `retries` attempts, which is why this shape is a design risk to raise rather than a broken document. With `redirect="false"` Plivo still posts the `Digits` to your menu URL, ignores whatever that URL returns, and carries on to the `` and the ``. Your menu URL then becomes a notification handler: it must still answer HTTP 200 quickly, and it is where you record the caller's choice so the bot can read it (pass the same `CallUUID` through, or key on it). +**C. Greeting and keypad menu, then stream.** Everything before `` delays the bot's first word. `redirect` defaults to `true` on ``, ``, ``, `` and ``: a caller who responds then gets whatever the `action` URL returns and skips the rest of this document, while a caller who does not falls through after `retries`. `redirect="false"` keeps everyone on this document; the menu URL still gets the `Digits`, must answer 200 fast, and is where you store the choice for the bot (key it on `CallUUID`). To route each key to a different bot instead, keep the default, put a `` and a `` after `` for the no-input path, and have the menu URL return document A or B. ```xml - Welcome to Acme. This call may be recorded for quality and training. - For sales, press 1. For support, press 2. + Welcome to Acme. This call may be recorded for quality and training. + For sales, press 1. For support, press 2. wss://voice.example.com/ws/{{CallUUID}} ``` -C2. The other way to do the same thing: leave `redirect` at its default and let the menu URL return the streaming document. Use this when each key must reach a different bot or a different WebSocket URL. The answer document is then just the greeting and the menu: - -```xml - - Welcome to Acme. This call may be recorded for quality and training. - For sales, press 1. For support, press 2. - Sorry, we did not get a choice. Goodbye. - - -``` - -and `/plivo/menu` returns document B (or A) with the WebSocket URL for the branch the caller chose. The `` and `` after the `` are the no-input path: without them the call ends in silence after `retries` attempts. - -D. Stream, then hand control back to a URL of your own when the bot closes the socket. - -```xml - - wss://voice.example.com/ws/{{CallUUID}} - https://voice.example.com/plivo/after-bot - -``` - -E. Stream plus a room. Note there is no `keepCallAlive` here: the MultiPartyCall must run. +**D. Stream plus a MultiPartyCall room.** No `keepCallAlive` here: the room must run after the stream starts. ```xml @@ -323,16 +216,7 @@ E. Stream plus a room. Note there is no `keepCallAlive` here: the MultiPartyCall ``` -F1. Answer document served before a transfer (stage 7, first of two documents). - -```xml - - - wss://voice.example.com/ws/{{CallUUID}} - -``` - -F2. The transfer document, served from the transfer URL. The `` after `` is what brings the caller back to the bot when the human does not answer. +**E. Transfer document** (stage 7), served from the transfer URL. The `` after `` brings the caller back to the bot when the human does not answer. ```xml @@ -342,718 +226,144 @@ F2. The transfer document, served from the transfer URL. The `` after `< ``` -H. Stream, then end the call cleanly when the bot closes the socket. - -```xml - - wss://voice.example.com/ws/{{CallUUID}} - - -``` - -India KYC application body, for `plivo numbers compliance create --data @app.json`. One `documents[]` entry per document type the requirements call returns. - -```json -{ - "country_iso": "IN", - "number_type": "local", - "alias": "", - "end_user": { - "type": "business", - "name": "", - "email": "", - "address_line1": "", - "city": "", - "state": "", - "postal_code": "", - "country": "IN", - "registration_number": "" - }, - "documents": [ - { - "document_type_id": "", - "data_fields": { "business_name": "" } - } - ] -} -``` - -### The defaults, and why - -| Setting | Default | Why | -|---|---|---| -| `bidirectional="true"` | always | the documented default is `false`, and then the caller hears nothing from the bot () | -| `keepCallAlive="true"` | always, except before a MultiPartyCall | documented: with it the stream runs exclusively and subsequent XML executes only after the stream disconnects; without it the following XML runs at once. Risky, not fatal: documents that leave it off run to a normal hangup, so recommend it and do not reject over it | -| `` before `` | when recording | with keepCallAlive, an element after the stream runs only once the stream ends, so a `` below would start after the conversation | -| `contentType` explicit | always | the XML reference says `audio/x-l16;rate=8000` is the default, the guide says mu-law; do not depend on either | -| `statusCallbackUrl` | always ask | the only push signal for `DroppedStream` and `DegradedStream` () | -| `streamTimeout` | omitted | platform default 86400 s; a short ceiling such as 300 s or 600 s cuts real conversations at that second | -| `extraHeaders` | `k=v,k2=v2`, no secrets, 512 bytes max | comma-separated in the XML reference, the API and the CLI; delivered in every event and logged. The Stream page calls the value custom key-value pairs and shows `userId=12345,sessionId=abc123`. The same page also prints a character constraint of `[A-Z]`, `[a-z]`, `[0-9]`, which cannot be read literally because its own example uses `=` and `,`. Streamed calls whose keys carry `_` or `-` end with a normal hangup, so treat unusual characters as worth testing rather than as a failure | -| After `` | nothing | most deployments end the document there; ``, `` or `` after it are the common continuations | -| Handoff | Transfer API, then `` | the transfer document is fetched when the socket closes; same-document `` or `` is the alternative | -| Credentials in URL or `extraHeaders` | never | validate `X-Plivo-Signature-V3` on the WebSocket upgrade instead () | - ### Check the XML before you serve it -Apply this checklist to the exact body your answer URL returns, not to the file you think it returns. It is static: it cannot see HTTP failures, so a document that passes still does not prove Plivo can fetch it (7011 leaves no trace in the body) or that a call connects. Every rule names its docs page or says it is a common observation. - -**Will break the call.** Every line here is backed by a documented rule or by a call with this shape that failed. Fix before dialling: - -- The body is empty. There is no usable answer document, so the call cannot proceed. If your server answered 200, the XML overview lists an empty response under invalid answer XML, so expect 8011; if it did not answer, or answered non-2xx, that is 7011. Read the HTTP status before naming a code. -- The body is JSON. Plivo reports 8011, because JSON is not Plivo XML. If the number is attached to a console flow application rather than an XML application, the flow answers instead of your answer URL and you get a JSON body you did not write: attach your own XML application to the number. Any other JSON is usually your framework's error or auth response, which normally also means a non-2xx and so 7011. -- The body is HTML (an error page or a login page): 8011. -- Not well formed XML: 8011. Two documents concatenated (anything after the first ``) and a comment containing `--` both fail the same way. -- The root element is not ``. -- `` at the top level. It is a Twilio element, and answer documents carrying it have been observed to fail with 8011. The Plivo form is ``. The documented Plivo elements are: Response, Record, Stream, Speak, Play, GetDigits, GetInput, Dial, Number, User, Conference, MultiPartyCall, Redirect, Wait, Hangup, PreAnswer, DTMF, Message. There is no `AgentHoldMusic` or `CustomerHoldMusic` element: hold music is set with the `agentHoldMusicUrl` and `customerHoldMusicUrl` **attributes** on `` (). SSML tags are allowed inside `` only. Any other unrecognised element belongs in the risky tier below: what Plivo does with one is not documented, and a top-level `` has been seen to be ignored rather than rejected. -- An answer document for a voice agent with no `` (and no `MultiPartyCall role="ai-agent"`). A plain `` or `` document plays a message and ends (4000 or 4010). Action, transfer and redirect documents do not need a ``. -- `` is empty: the call is answered and ends at once (4010). -- Only `` in an answer document: the call is answered and then ended gracefully at once, so the caller gets nothing and the call record looks like a document that finished normally. That is not a voice agent. Use `` as a placeholder. Deliberate screening is `` or ``; `rejected` and `busy` are the only two documented `reason` values, and the routing page documents the audible effect (a rejection tone, a busy signal). Call records for this shape carry 3020 or 3010 with hangup source Answer XML, which is an observation from real traffic and not a mapping any docs page publishes. -- `` with no WebSocket URL, a URL whose scheme is neither `wss://` nor `ws://`, a URL pointing at localhost (Plivo's servers cannot reach it), or a URL longer than the documented 2048 characters. Plain `ws://` is not in this tier: see the risky list. -- `audioTrack` that is not `inbound`, `outbound` or `both`; and `audioTrack="both"` or `"outbound"` together with `bidirectional="true"`, which the docs forbid. -- `contentType` outside `audio/x-mulaw;rate=8000`, `audio/x-l16;rate=8000`, `audio/x-l16;rate=16000`. Those three are the documented audio formats. `audio/x-mulaw;rate=16000` is not one of them: mu-law is documented at 8 kHz only. -- `statusCallbackMethod`, `method`, `callbackMethod` or `recordingCallbackMethod` that is not GET or POST. -- `extraHeaders` longer than 512 bytes. -- An action, callback, status-callback, hold-music or `` URL that is not an absolute `http(s)` URL, still holds a placeholder, or points at localhost. `` with no URL at all. -- An empty `` or `` inside ``: there is nothing to dial, and the routing page requires `` to contain at least one nested element. -- `` with no `action` URL: that page documents `action` as required, so there is nowhere for the input to go. On `` the input page gives `action` a default of `-` and does not mark it required, so a `` without one is legal: execution simply continues to the next element after `retries` attempts. Flag a missing `` only when the document clearly expects the digits to be posted somewhere. `retries` is an integer with a documented default of 1 and no published minimum, so do not reject `retries="0"` as invalid; note only that the docs give no behaviour for it (). -- `` outside the documented set for the voice you chose. A real call confirms this one: a `` document logged 8011. With the generic `WOMAN` and `MAN` voices the set is `da-DK`, `nl-NL`, `en-AU`, `en-GB`, `en-US`, `fr-FR`, `fr-CA`, `de-DE`, `it-IT`, `pl-PL`, `pt-PT`, `pt-BR`, `ru-RU`, `es-ES`, `es-US`, `sv-SE`; a `Polly.` voice covers many more, including `hi-IN` with `Polly.Aditi` and `en-IN` with `Polly.Raveena` (docs: , ). -- `` other than `WOMAN`, `MAN` or `Polly.`. Those are the documented values; SSML needs a `Polly.` voice. -- `` containing text instead of an audio file URL. - -**Risky.** None of these is a documented failure, and calls with each of these shapes run to a normal hangup. Raise them, say what could go wrong and what to check, and do not name a hangup code for any of them: - -- A one-way `` (no `bidirectional="true"`). The bot hears the caller and the caller hears nothing from the bot. Valid for transcription or monitoring, wrong for a talking agent. -- Text sitting directly inside `` outside any element. Only elements belong there; put text in ``. The docs do not say what Plivo does with stray text. -- An unrecognised element at the top level other than ``. What Plivo does with one is not documented, and a top-level `` has been seen to be ignored rather than rejected. Remove it. -- `bidirectional` set to anything that is not `true` or `false`. The docs give it as a boolean defaulting to `false` and say nothing about another value, so it is untested rather than known to fail. -- `noiseCancellation` that is not `true` or `false`, or a `noiseCancellationLevel` that is not an integer. Outside the documented values, with no documented behaviour. -- An empty `sendDigits=""` on a `` or ``: a template that rendered nothing. The docs give no behaviour for an empty one. -- `` with speech input and a `language` outside `en-US`, `en-GB`, `en-AU`, `es-US`, `es-ES`, `fr-FR`, `de-DE`, `it-IT`, `pt-BR`, `ja-JP`, `zh-CN`. The docs print that list under "common languages include", so it is explicitly not exhaustive and absence from it is not a documented failure. Test the code on a real call before shipping it. -- `ws://` instead of `wss://`. Production accounts stream over `ws://` and those calls end with a normal hangup, so this is never a reason to answer that a document breaks. Prefer `wss://` for the security reason: on `ws://` the caller's audio and anything in the URL or `extraHeaders` cross the internet in clear text, and no certificate proves the server is yours. Every example in the docs uses `wss://`. -- A temporary tunnel host in the stream URL or any callback URL: fine for testing, never for a live number, because a stopped tunnel means `DroppedStream` or 7011. -- Credentials in a URL (`user:password@`) or a token-like query value, in the stream URL or in `extraHeaders`. They appear in Plivo logs. Validate `X-Plivo-Signature-V3` instead. -- `http://` instead of `https://` on a callback URL. -- No `statusCallbackUrl`, or an empty one: you will not be told about `DroppedStream` or `DegradedStream`. -- No `contentType`: the XML reference documents `audio/x-l16;rate=8000` as the default and the guide says mu-law. Set it. -- `audio/x-l16;rate=24000`, which appears only in the Voice API reference and not in the Stream XML page's list of audio formats, and `audio/x-wav`, which is not documented anywhere. Prefer one of the three formats the Stream XML page lists. -- No `keepCallAlive="true"` on a document that has no MultiPartyCall. Documented behaviour: with it the stream runs exclusively and subsequent XML executes only after the stream disconnects, so without it the following XML runs at once. Documents that leave it off run to a normal hangup, so recommend adding it and say what the following element would do early; do not report the call as broken over it. -- A ``, ``, ``, `` or `` with an `action` URL and `redirect` left at its default `true`, with more than a terminal fallback written below it. A caller who responds gets the action document instead of the rest of yours; a caller who does not respond still falls through to it, which the input and routing pages document. Raise the risk, offer `redirect="false"` or a repeat of the content in the action document, and do not call it a broken call. A terminal fallback below the element (``, ``, ``, ``) is the documented correct shape and is not worth mentioning. -- More than one ``: only one stream runs per call at a time. -- Anything written after ``. The routing page says `` transfers call execution to a different URL and Plivo continues the call there, but it does not separately say that siblings below are skipped. Treat that content as dead code and move it into the document the redirect URL returns. -- `streamTimeout` under 120 s (it cuts real conversations) or a value that is not a positive integer. -- `noiseCancellationLevel` outside the documented range 60 to 100, or set without `noiseCancellation="true"`. What Plivo does with a value below 60 is not documented; stay in the range. -- `extraHeaders` using `;` as separator, or an item without `key=value`. -- `` after ``: with keepCallAlive it runs only after the stream ends, so it would start recording after the conversation rather than during it. `` without `recordSession="true"` waits for the caller to speak and then stops. -- A bare `&` in text or an attribute. It is invalid XML, so write `&`. Do not rely on any parser accepting it. -- An empty attribute value (`bidirectional=""`, `statusCallbackUrl=""`, `noiseCancellation=""`). Omit the attribute instead of emitting it empty; what Plivo does with an empty value is not documented. -- A voice the docs do not list for that language (no MAN voice for da-DK, fr-CA, ru-RU, sv-SE; no WOMAN voice for pt-PT), or an empty ``. -- `` with no `` or `` child: nothing is dialled. -- Any element nested inside a parent that does not document it as a child. The documented parents are `Response` (any element), `GetDigits` and `GetInput` (`Speak`, `Play`), `Dial` (`Number`, `User`) and `PreAnswer` (`Speak`, `Play`, `Wait`). Whether Plivo ignores or rejects other nesting is not documented; move the element out. - -**Style, or worth knowing.** No functional effect. Say so and move on: - -- ``, ``, ``, `` or `` before `` delays the socket, and so the bot's first word, by that much. -- `` creates a second, billed leg. Read `DialStatus` on its action URL. -- Recording is on: disclosure, consent, retention and deletion rules are yours to check. -- `MultiPartyCall role="ai-agent"` over XML: the attributes are documented, but whether this form opens the socket the way the REST form does is not verified either way. -- Everything in this checklist that is not about `` is a short form of the general XML rules, chosen for what breaks a streamed call. For the full element by element attribute tables and the complete list of shapes that produce 8011 and 8012, install `plivo-voice-xml` (`npx skills add https://www.plivo.com/docs --skill plivo-voice-xml`) or read . - -## India: what a voice agent needs before its first call - -Facts from these docs pages: , , , , , , , , , , , (India Compliance Errors). Where the CLI has no command, the console or API path is given. - -### 1. Account - -- Indian numbers are only available to India data-region organisations. The data region cannot be changed after creation. If you are in the US region, create a new organisation with the India data region from the organisation switcher in the console. No new signup is needed. -- Only India-registered businesses can rent Indian numbers and use domestic routes. A business outside India must use international routes: international rates, and the caller ID shown is a US or international number. -- If you need both India and international calling, the docs say to run two separate Plivo accounts. -- Geo permissions: INR accounts can only call within India. Professional (pay-as-you-go) accounts can call only US and India. -- Verified Caller ID is not supported for India. The caller ID must be a Plivo-rented Indian number. -- CLI: `plivo auth whoami -o json` shows which account you are on. There is no CLI command for data region or organisation switching. -- Which Indian number series support `` and whether streaming to a server in India is permitted is not stated in the docs. Media anchoring (section 5) requires your server to be in India. - -### 2. KYC (compliance application): an agent can run this end to end from the CLI - -One rule: run `plivo numbers compliance requirements` and supply exactly the document types it returns. The console guide and the API reference have disagreed on how many documents are needed (one vs two). The live requirements response is the source of truth. Do not hard-code a count. - -The agent cannot produce the inputs. The certificate files and the exact legal details come from the user. Never fill them in yourself. Never submit without the user saying "go". - -Ask the user for: - -| Input | Why | Rule | -|---|---|---| -| Certificate files (PDF, JPEG or PNG, 5 MB or less each, filename 99 characters or less) | uploaded as `documents[i].file` | one file per document type returned by `requirements`. Two further rules are commonly repeated but appear on no docs page: that the same file cannot fill two slots, and that a PAN on its own is not enough. Supply a distinct file per type and whatever `requirements` returns, and do not tell a user an application will be rejected on either ground. | -| Legal business name, exactly as printed on the certificates | `end_user.name` and `documents[].data_fields.business_name` | must match across documents and fields, character for character | -| Registration number (CIN or Udyam number), GSTIN | `end_user.registration_number`; the GST certificate carries the GSTIN | copy from the documents; do not guess | -| Contact email, registered address (line, city, state, postal code) | `end_user.*` | | -| Direct brand or reseller? | reseller: one application per customer, named in `alias`; the customer's application id is passed at rent time | | -| Is this the first application on the account? | the documents must be sealed and signed by an authorised signatory (Plivo takes billing address and GST details from it) | tell the user before they upload | - -Commands, in order. Every step that writes appears twice: once with `--dry-run`, which prints the request and sends nothing, and once with `--yes`. Show the user the previewed request and wait for them to say go before you run the second form. A compliance application is a regulatory filing; renting a number spends money. - -```bash -# 1. What is required right now (read-only; source of truth; do not hard-code document ids or counts) -plivo numbers compliance requirements --country IN --number-type local --user-type business -o json - -# 2. Fill the payload from the user's answers (one documents[] entry per returned type), then create and submit -plivo numbers compliance create --data @app.json \ - --file documents[0].file=@first_document.pdf \ - --file documents[1].file=@second_document.pdf --dry-run # preview the filing; nothing is sent -plivo numbers compliance create --data @app.json \ - --file documents[0].file=@first_document.pdf \ - --file documents[1].file=@second_document.pdf --yes -o json # only after the user says go; returns compliance_id, status "submitted" - -# 3. Poll until it leaves "submitted" (read-only; 080 and 022 numbers: automated review, typically about 5 minutes) -plivo numbers compliance get --expand documents -o json # status: accepted | rejected - -# 4a. rejected: read rejection_reason, fix the document or a field, resubmit. update REPLACES all documents: re-attach every file -plivo numbers compliance update --data @app.json \ - --file documents[0].file=@first_document.pdf --file documents[1].file=@second_document.pdf --dry-run # preview -plivo numbers compliance update --data @app.json \ - --file documents[0].file=@first_document.pdf --file documents[1].file=@second_document.pdf --yes # after approval - -# 4b. accepted: rent. Direct brands: Plivo attaches the accepted application automatically. -plivo numbers search --country IN --type local --limit 10 # read-only -plivo numbers buy --dry-run # preview; this one spends money -plivo numbers buy --yes # after approval -# Reseller, or "compliance_application_id is required": the CLI buy has no flag for it, so use the API: -plivo api POST /PhoneNumber// --body '{"compliance_application_id":""}' --dry-run -plivo api POST /PhoneNumber// --body '{"compliance_application_id":""}' --yes - -# 5. Numbers you already had: link them. This changes how an existing number is treated, so preview it too. -plivo numbers compliance link --link +9180XXXXXXXX= --dry-run -plivo numbers compliance link --link +9180XXXXXXXX= --yes -plivo numbers get -o json # confirm the link -``` - -If a command rejects `--dry-run` or `--yes`, read its `--help` rather than dropping the preview: run the read-only `get` or `list` for the same resource first, show the user the current state, and get their agreement before you write. - -Optional: put `"callback_url": "https://..."` (HTTPS only) in the payload. Plivo POSTs a V3-signed callback when the status changes, instead of polling. - -Status: `draft`, then `submitted`, then `accepted` (rent and link) or `rejected` (update, which auto-resubmits). Also `suspended` (unresolved UCC complaints; see section 6) and `expired` (create a new one). - -Errors you will see, verbatim: `compliance_application_id is required` (no accepted application to attach at rent). `Compliance application must be in 'accepted' status. Current status: 'submitted'.` (too early; keep polling). `Compliance application must be in 'rejected' status` (update only works on rejected). `Number not found on your account. Only rented numbers can be linked.` - -Common rejection reasons (docs): details do not match government records (download a fresh copy from the GST, MCA or Udyam portal), expired document, unaccepted document type, unreadable upload, same file in both slots. - -Not covered by this API: 140-series (promotional) and 160-series (BFSI) numbers. Those are a separate provisioning process (Tata DLT registration, declaration forms, NOC, voice header and template approval, 5 to 14 business days) that runs through your Account Manager or a support ticket. See section 4. - -### 3. Renting the number - -```bash -plivo numbers search --country IN --type local --limit 10 -plivo numbers buy --dry-run [--app-id ] # preview first: this spends money -plivo numbers buy --yes [--app-id ] # only after the user approves -``` - -- Direct brands: the accepted application links automatically at purchase. Resellers must choose the customer's approved application. -- The CLI `buy` has no `compliance_application_id` flag. If purchase fails with `compliance_application_id is required`, pass it through the Buy a Phone Number API, previewing first: `plivo api POST /PhoneNumber// --body '{"compliance_application_id":""}' --dry-run`, then the same command with `--yes`. - -### 4. Number series: pick the right one or every complaint counts as UCC - -| Series | Permitted use | Who | Time to provision | -|---|---|---|---| -| Landline (022, 080, ...) | Service and transactional calls only. Promotional content strictly prohibited | Non-BFSI businesses | about 5 min automated KYC | -| 140-series | Promotional voice calls only. Not for transactional or service calls | Any business making promotional calls | about 5 to 10 business days | -| 160-series | Service and transactional calls, BFSI only (RBI, SEBI, IRDAI, PFRDA-regulated). Promotional use leads to disconnection and penalties | BFSI entities | about 7 to 14 business days | - -Using the wrong series is itself a violation. Complaints from such calls are treated as UCC regardless of consent. - -140 and 160 provisioning is offline. There is no CLI or console flow: - -1. Register on the Tata Teleservices DLT portal as Principal Entity (PE) and Telemarketer (TM). Same legal entity for both means Self-Managed; a vendor placing calls for you means Partner-Managed. Only Tata DLT is supported (not Airtel, Vi or BSNL). The TM must be registered in Mumbai or Karnataka. -2. Email your Account Manager or raise a Plivo support ticket with the signed, sealed Aggregator Telemarketer Declaration (140 and 160) plus the BFSI Customer Application Form (160 only). -3. Plivo allocates the number and issues a NOC per number. -4. TM (140) or PE (160) registers the Voice Header on Tata DLT with the NOC. PE uploads the GST certificate (140) or regulator certificate (160). Tata approves in 1 to 2 (140) or 3 to 5 (160) business days. -5. PE registers Voice Templates (the transcript of what the agent says, with placeholders). Approval about 1 business day (140) or 1 to 2 (160). Then the number can go live. - -### 5. Media anchoring - -Both legs of every call must originate and terminate in India. Inbound: India to India. Outbound: Indian number to Indian destination. Conferences: all participants in India. Violations fail with hangup cause 2070 `Violates Media Anchoring`. The hangup-causes page adds: for India calls the server must be in India; do not mix PSTN and WebRTC in conferences. A US-hosted platform cannot terminate India calls (403 `domestic_anchored_terms_not_met`). - -### 6. Consent and UCC (outbound agents) - -- Cold calling is prohibited. You need explicit digital consent before any commercial call (TRAI TCCCPR 2025). Calls without it are Unsolicited Commercial Communication (UCC). Applies to landline and 160-series numbers. -- Complaints appear on the console UCC dashboard (Phone Numbers, UCC), in a daily email, and via the UCC API: `plivo api GET /Ucc/` (list; filter `?status=rejected`), `plivo api GET /Ucc//`, and `plivo api POST /Ucc// --dry-run` then `--yes` to submit proof once the user has approved what is being filed. There is no typed CLI command. -- Within 5 business days of a complaint, upload opt-in proof containing all three: business logo, complainant's phone number, opt-in date within the last 6 months. Rejected proof must be re-uploaded inside the same 5-day window. -- Remove the complainant from your list immediately. Calling them again is itself a violation. -- Escalation (tied to your compliance ID): no proof in 5 days blocks the compliance ID for 15 days (proof lifts it). 5 or more unique complaints in any rolling 10 days means immediate suspension, first violation if proofs fail. A second such instance means TRAI blacklisting for 1 year across all Indian operators. A number with no compliance ID mapped puts the whole billing entity at risk. -- You may file a representation with Plivo (decided within 7 business days) or appeal to TRAI. - -### 7. Capacity - -- India accounts have a concurrency limit, default 50 concurrent calls (inbound plus outbound, Voice API plus SIP trunking). CPS equals concurrency divided by 25 (default 2). Calls over the limit are rejected instantly with 5030 `Concurrency Limit Breached` (in the API response and the hangup callback). No queueing. Hard enforcement since 20 Apr 2026. -- Check usage: console Voice, Call Logs, Export, Export Concurrency Data. Increase by raising a support ticket (minimum step 25 slots, which is +1 CPS). If the 30-day peak is over 80% of the limit, raise it first. -- Abandoned and short-call surcharges exclude calls to India. -- Carrier failover in India needs High Availability (HA) numbers (a second number from another carrier, billed as an extra rental). Hangup callbacks carry `CarrierFailoverTriggered=true` when it fired. Outbound only. +Check the exact body the URL returns. A pass proves the document only, not that Plivo can fetch it or that a call connects. -### 8. Exact API error strings (400 Bad Request) and what they mean +**Will break.** Fix before dialling: -| Error text | Meaning | Fix | -|---|---|---| -| `from parameter is invalid. +91... is not associated with your account. Use a phone number rented on your account.` | Caller ID is not a number rented on this account | Rent an India number and call from it | -| `from number +91... cannot place calls as its compliance application is not in 'accepted' status. Refer to https://www.plivo.com/docs/numbers/rent-india-numbers for next steps.` | Application pending, submitted, rejected, expired, or none attached | `plivo numbers compliance list --status accepted`; attach with `compliance link`; or submit or fix the application | -| `from number +91... cannot place calls as its compliance application is 'suspended'. Refer to https://www.plivo.com/docs/voice/concepts/ucc-management for next steps.` | Unresolved UCC complaints | Upload opt-in proof for every open complaint on the UCC dashboard | -| `compliance_application_id is required to rent this number. Refer to https://www.plivo.com/docs/numbers/rent-india-numbers for details.` (on rent) | No accepted application to auto-attach | Submit and wait for approval, or pass an accepted application's UUID in the Buy request | -| `Compliance application must be in 'accepted' status. Current status: 'submitted'.` (on rent) | The application you passed is not approved yet | Follow the status table above, then retry | - -### 9. Evidence checklist before the first call +- An empty body: 8011 if the server answered 200, 7011 if it answered non-2xx or not at all, so read the status before you name a code. +- JSON, HTML or XML that is not well formed (including two concatenated documents, or a comment containing `--`): 8011. JSON you did not write usually means a console flow application owns the number (stage 2). +- A root other than ``, or an empty `` (answered, then ended at once with 4010). +- A top-level Twilio ``: observed to fail with 8011; the Plivo form is ``. Plivo's elements are Response, Record, Stream, Speak, Play, GetDigits, GetInput, Dial, Number, User, Conference, MultiPartyCall, Redirect, Wait, Hangup, PreAnswer, DTMF and Message. Hold music is the `agentHoldMusicUrl` and `customerHoldMusicUrl` attributes of ``, not an element. +- An answer document that can never reach a `` (or a `MultiPartyCall role="ai-agent"`), directly or through an action URL that returns one. Action, transfer and redirect documents need no ``. +- Only ``: the call answers and ends gracefully at once, and the record looks like a normal finish. Use a `` placeholder while building; deliberate screening is `` or ``. +- A talking bot's `` without `keepCallAlive="true"`, except before a MultiPartyCall (document D). Without it the next element runs at once (documented), and with nothing after the stream the call ends within seconds with 4010 (observed). +- A `` with no URL, a scheme other than `wss://` or `ws://`, a localhost host, or a URL over 2048 characters. +- `audioTrack` other than `inbound`, `outbound` or `both`, or `outbound` or `both` together with `bidirectional="true"` (the docs forbid it). +- A method attribute other than GET or POST; `extraHeaders` over 512 bytes. +- An action, callback, hold-music or `` URL that is not absolute http(s), still holds a placeholder, or points at localhost; an empty ``; an empty `` or ``, or a `` with neither; a `` without `action` (on `` it is optional). +- A `` the chosen voice does not support: a `` logged 8011. `WOMAN` and `MAN` cover da-DK, nl-NL, en-AU, en-GB, en-US, fr-FR, fr-CA, de-DE, it-IT, pl-PL, pt-PT, pt-BR, ru-RU, es-ES, es-US and sv-SE; other languages need a `Polly.` voice (`hi-IN` with `Polly.Aditi`, `en-IN` with `Polly.Raveena`; the full list: `plivo docs show voice/concepts/ssml`, ). Also a `voice` other than `WOMAN`, `MAN` or `Polly.`, and a `` holding text instead of an audio URL. -Write each item down with its source (a CLI command's output or a console page). Any item you cannot show is "not known", and the number is not ready. +**Risky.** Say what to check; name no hangup code: -1. Organisation id and displayed data region (console; no CLI). -2. Legal entity that owns the business eligibility (India-registered, or reseller acting for one). -3. Caller ID number and proof it is rented on this account (`plivo numbers get -o json`). -4. Attached compliance application id and its `accepted` status (`plivo numbers compliance get -o json`). `submitted` is not `accepted`. -5. Call purpose (service, transactional, promotional, BFSI) and the matching number series. -6. Consent record reference and a check against your suppression list (complainants, opt-outs). -7. A drawing of both legs and your media server with each region named (section 5). -8. Approved 140 or 160 header and template references where they apply. -9. A rollback owner and the UCC escalation contact. +- No `bidirectional="true"`: the caller hears nothing from the bot (right only for transcription or monitoring). +- `contentType` missing, or not one of the documented values in the attribute table (for example `audio/x-wav` or `audio/x-mulaw;rate=16000`): untested. Set it explicitly. +- `ws://` instead of `wss://`: the guide requires `wss://`; `ws://` streams are seen to connect, but audio and headers cross the internet in clear text. Also `http://` callback URLs, and a temporary tunnel host anywhere (a stopped tunnel means `DroppedStream` or 7011). +- No `statusCallbackUrl`: you will not hear about `DroppedStream`. +- Credentials or tokens in the stream URL, `extraHeaders` or a callback URL: Plivo logs them. Use signatures. +- An element with an `action` URL and `redirect` left `true`, with more than a terminal fallback below it (document C). A terminal ``, ``, `` or `` below it is the correct shape. +- Anything after ``: dead code; move it into the redirect target's document. +- `` after `` (it starts after the conversation), or without `recordSession="true"` (it waits for speech, then stops). +- More than one `` (one runs per call); `streamTimeout` under 120 s or not a positive integer; `noiseCancellationLevel` outside 60 to 100 or without `noiseCancellation="true"`; `extraHeaders` separated by `;` or holding an item that is not `key=value`. +- Other Twilio or unknown elements (``, ``, ``, ``; a top-level `` was seen ignored): use ``, `` and ``. Also stray text in ``, nesting a parent does not document, empty attribute values, a bare `&` (write `&`), an empty `sendDigits`. +- A `` speech `language` outside en-US, en-GB, en-AU, es-US, es-ES, fr-FR, de-DE, it-IT, pt-BR, ja-JP and zh-CN: the docs call that list common, not complete, so test the code on a call. -## Outbound voice agents: answering-machine detection, US call quality, caller ID +**Style.** `` creates a second, billed leg. Recording leaves disclosure, consent and retention to you. -Facts from these docs pages: , , , , , , , , . +Other Voice XML elements: `plivo skill install voice-xml`, which covers the common ones and points to the docs for full attribute tables, or . -### Answering-machine detection (AMD) +## `` attributes and the WebSocket protocol -| Parameter | Values / default | Effect | +| XML attribute (API parameter) | Use | Why | |---|---|---| -| `machine_detection` | `true` or `hangup` | `true`: Plivo notifies `machine_detection_url` and the call continues. `hangup`: Plivo hangs up on detection (hangup cause 9100 Machine Detected) | -| `machine_detection_time` | ms, default `5000` | How long to analyse | -| `machine_detection_url` / `machine_detection_method` | URL, `POST` default | Receives `Machine=true`, `Event=MachineDetection`, `CallUUID`, `From`, `To`, `CallStatus` | -| `machine_detection_maximum_speech_length` | 1000 to 6000 ms | advanced tuning | -| `machine_detection_initial_silence` | 2000 to 10000 ms | advanced tuning | - -- Detection is asynchronous. It never blocks the call flow. Your answer URL has already returned `` by the time `Machine=true` arrives. -- On `Machine=true` you can hang up (`plivo voice calls hangup --yes`), transfer the call to a voicemail-message URL (`plivo voice calls transfer --legs aleg --aleg-url https://.../voicemail`), or tell your bot over its own channel to switch to voicemail mode. -- The docs warn: do not hang up on every voicemail. At scale that produces short-duration calls that count against the quality thresholds below. Leave a brief message or schedule a retry. -- CLI: `plivo voice calls make --from ... --to ... --answer-url ... --answer-method POST --machine-detection true|hangup --ring-url ... --hangup-url ... --dry-run`, then the same command with `--yes` once the user approves; every call is billed. Note `--answer-method` defaults to `GET`, so set `POST` explicitly if your endpoint only accepts POST. The CLI has no `--machine-detection-url` or `-time` flags. To set them use `plivo api POST /Call/ --body '{"from":...,"to":...,"answer_url":...,"answer_method":"POST","machine_detection":"true","machine_detection_url":"https://...","machine_detection_time":5000}' --dry-run`, then `--yes`. -- WhatsApp voice calls (`call_type=whatsapp_voice`) do not support machine detection. - -### Expectations for an outbound campaign - -Measure your own answer rate and connect rate by destination, list source and time window. This skill does not carry a benchmark number. Codes you will see daily are in the carrier and destination table below: 6010 ring timeout, 3000 no answer, 3080 carrier error, 2000 invalid destination, 3050 unallocated, 9100 machine detected. - -### US: staying deliverable (applies to US destinations only) - -US carriers judge traffic on answer rate and call duration. Plivo enforces monthly thresholds on non-India destinations: - -| Call type | Definition | Threshold | Surcharge on excess | -|---|---|---|---| -| Abandoned | 0 s duration (never answered or dropped before connect) | under 20% of volume | USD 0.005 per call | -| Short-duration | answered, 6 s or less | account-limits page: 10%; US deploy page: 20%. Plan for 10% | USD 0.015 per call | - -Substantially higher abandonment can trigger an account review. Keep metrics healthy: call opted-in recipients only; pace inside your CPS; do not hang up on every voicemail; make the agent identify itself and the reason in the first sentence (people who hang up in the first seconds create short calls); use a Plivo number you own as caller ID; register Caller Reputation; spread retries and prune dead numbers. - -CPS (calls per second): every account starts at 2 CPS outbound (inbound 10). Calls above the limit are queued, not rejected (audio-streaming path), and dial later. That is bad for appointment-window calls, so pace requests yourself. 2 CPS is about 7,200 attempts per hour. Size it as peak concurrent calls divided by average call seconds (100 concurrent 3-minute conversations need under 1 CPS). Higher CPS needs an Enterprise plan, requested from Plivo support in the console. India instead has a hard concurrency limit (see the India section). API rate limit: 300 requests per 5 s (429 when exceeded). Default max call duration 4 h (`time_limit` up to 86400 s). - -### Caller ID in the US - -- STIR/SHAKEN is automatic. Plivo signs an outbound US call as Verified (attestation A) only when the caller ID is a Plivo number rented by the same account. Anything else is B or C ("Not Verified"). Status appears as `STIRVerification` in answer, fallback and hangup callbacks and in call records (`X-Plivo-Stir-Verification` SIP header). Plivo may stop signing if calls breach fair use, look like robocalls, get traceback requests, or use invalid caller IDs. -- Verified Caller ID (only if you must show a number you own outside Plivo): OTP by SMS or call via console (Voice, Verified Caller ID) or `POST /VerifiedCallerId/` then `POST /VerifiedCallerId/Verification//` with the OTP. No CLI command; use `plivo api`. Primarily US; not applicable in India. Verification lets you use the number as caller ID; it does not give attestation A, guarantee how the number displays, or prevent spam labels. -- Caller Reputation (early-access beta, US local and toll-free): register the business on your 10DLC Business Profile with `enable_caller_reputation=true`, wait up to 2 business days, then set `caller_reputation=enabled` on each number. AT&T USD 12 per business per month, T-Mobile free, Verizon not supported yet. Does not guarantee no spam flag. No CLI command. -- 10DLC registration is for SMS. It is not required for voice calls. -- Geo permissions: Professional (pay-as-you-go) accounts can call only the US and India; other countries need an Enterprise plan. Barred destinations fail with 2030, 2040 or 2050. -- US-region free-trial organisations may see "Voice capability is currently disabled for this account" on `calls make`. Request outbound access from the console. This gating is not in the docs. - -### Testing outbound safely - -A failed outbound answer URL is a billed call that drops when the callee answers (7011 or 8011). Prove the WebSocket with `plivo voice streams test` and the XML with the checklist first. Then place one call to your own phone: `plivo voice calls make --from --to --answer-url https://HOST/plivo/answer --answer-method POST --dry-run` to preview, then `--yes`. - -## Callbacks, signature validation, timeouts - -Facts from these docs pages: , , , , , , , , , , . - -### Which URL fires when - -This table lists the URLs an agent deployment uses. The general contract, in three lines: an `action` URL expects one Plivo XML document back and Plivo runs it; a `callbackUrl` expects nothing back, so answer HTTP 200 with an empty body or ``; and `redirect` decides which one owns the rest of the call, defaulting to `true` on `GetDigits`, `GetInput`, `Record`, `Dial` and `Conference` so the action document replaces yours (). - -| URL | Set where | When | Must return | -|---|---|---|---| -| `answer_url` (Primary Answer URL) | Call API (mandatory for outbound) or application (mandatory for the number) | Call answered (outbound) or arrives (inbound). `Event=StartApp`, `CallStatus=in-progress` | Plivo XML | -| `fallback_url` (Fallback Answer URL) | Call API or application | Answer URL unreachable | Plivo XML | -| `ring_url` | Call API only | Destination starts ringing. `Event=Ring` | 200 | -| `hangup_url` | Call API or application | Call ends. `Event=Hangup`, `CallStatus=completed`, plus `HangupCause`, `Duration`, `BillDuration`, `TotalCost`, `StartTime`, `AnswerTime`, `EndTime`, `CarrierFailoverTriggered=true` only when it happened | 200 | -| `machine_detection_url` | Call API | Answering machine detected. `Machine=true`, `Event=MachineDetection` | 200 | -| `action` (Dial, GetDigits, GetInput, Record, Conference) | XML attribute | Element finished | Plivo XML to continue the call | -| `callbackUrl` (Dial, Record, Conference), `statusCallbackUrl` (Stream, MPC) | XML attribute | Events during the element | 200; no XML expected. JSON is fine here | -| `aleg_url` / `bleg_url` | Transfer API | Transfer requested. Fired when the current element yields; with `` that appears to be when the bot closes the socket, which the docs do not state, so verify it once on your own account | Plivo XML | - -Common request parameters on answer, fallback and hangup: `CallUUID`, `From`, `To`, `Direction` (`inbound` or `outbound`), `CallStatus` (`ringing`, `in-progress`, `completed`; outbound also `busy`, `failed`, `timeout`, `no-answer`), `Event`, `RequestUUID` (outbound), `ALegUUID`, `ALegRequestUUID`, `ForwardedFrom` (only when the carrier sends it), `CallerName` (SIP), `STIRVerification` (US), `SessionStart`. Custom SIP headers arrive as `X-PH-`. Their names and values are restricted to `[A-Z]`, `[a-z]` and `[0-9]` so they survive URL encoding (docs: ). The `` page prints the same character set as a constraint on `extraHeaders`, but it cannot be read literally there: that page's own example uses `=` and `,`. See the `extraHeaders` row in the attribute table. SIP-authenticated inbound legs add `SIPAuthType`, `SIPAuthUser`, `SIPSourceIP`. - -The parameter lists an agent deployment actually reads: Dial `action` sends `DialStatus` (`completed`, `busy`, `failed`, `cancel`, `timeout`, `no-answer`), `DialRingStatus`, `DialHangupCause`, `DialALegUUID` and `DialBLegUUID`; `GetDigits` `action` sends `Digits`; `Record` `action` and `callbackUrl` send `RecordUrl`, `RecordingID` and the durations. For the complete per-element parameter tables, install `plivo-voice-xml` or read . - -### Response rules for the answer URL - -- Return HTTP 200 with `Content-Type: application/xml` or `text/xml` and one well-formed `` document. -- Anything else gives 7011 (non-2xx, unreachable, empty) or 8011 (not Plivo XML). -- Accept the method configured on the application (`answer_method`; applications default to POST, the CLI's `calls make --answer-method` defaults to GET). -- Do not require Basic or bearer credentials on the URL. Plivo cannot log in. Require a valid `X-Plivo-Signature-V3` instead. -- Answer fast. The XML overview says Plivo waits 15 seconds for XML. The configurable read timeout below defaults to 40 s. Plan for well under 15 s; static XML needs no database call. -- Plivo may deliver a callback more than once (retries). Make handlers idempotent. Keys: `CallUUID` (hangup, ring), `CallUUID` plus element (action), `RecordingID` (recording), `StreamID` plus `Event` (stream status). -- Keep the answer URL short. The console rejects very long URLs; the limit is not published. Carry context through `CallUUID`, custom SIP headers (`X-PH-*`) or a short opaque id, not through long query strings. - -### Timeouts, retries and edge region (URL fragments) - -Append a fragment to tune one callback URL: `https://host/answer#ct=2000&rt=5000&rc=2&rp=ct,rt&er=mumbai` (). +| `bidirectional` (`bidirectional`) | `true` | Default `false`: the caller hears nothing from the bot | +| `keepCallAlive` (none) | `true`, except before a MultiPartyCall | Default `false`. With it the stream runs alone and later XML runs only when it ends | +| `contentType` (`content_type`) | `audio/x-mulaw;rate=8000` | Always set it: the reference gives the default as `audio/x-l16;rate=8000`, the guide says mu-law. Also documented: `audio/x-l16;rate=8000`, `audio/x-l16;rate=16000`, `audio/x-l16;rate=24000`; mu-law is 8 kHz only | +| `statusCallbackUrl`, `statusCallbackMethod` (`status_callback_url`, `status_callback_method`) | Your URL, `POST` | The only push signal for drops | +| `streamTimeout` (`stream_timeout`) | Omit (86400 s) | When it fires the stream stops and, with nothing after it, the call ends 4010. 300 or 600 s cuts real conversations | +| `audioTrack` (`audio_track`) | Omit (`inbound`) | `outbound` and `both` are not allowed with `bidirectional="true"` | +| `extraHeaders` (`extra_headers`) | `k1=v1,k2=v2`, no secrets | Max 512 bytes; arrives in `start` as `extra_headers` and is logged. The page also prints a `[A-Za-z0-9]` constraint that its own example (`userId=12345,sessionId=abc123`) breaks; keys with `_` or `-` are seen to work, so test unusual characters rather than reject them | +| `noiseCancellation`, `noiseCancellationLevel` (`noise_cancellation`, `noise_cancellation_level`) | Omit, or `"true"` with 60 to 100 (default 85) | Filters the caller's audio | -| Key | Meaning | Allowed | Default | -|---|---|---|---| -| `ct` | connection timeout, milliseconds | 100 to 10000 | 2000 | -| `rt` | read timeout, milliseconds | 100 to 40000 | 40000 | -| `tt` | total timeout across retries, milliseconds | 100 to 55000 | 55000 | -| `rc` | retry count | 0 to 5 | 1 | -| `rp` | retry policy | `4xx`, `5xx`, `ct`, `rt`, `all`, comma separated | `ct,rt` | -| `er` | edge region | `nearest`, `local`, `n_california`, `n_virginia`, `frankfurt`, `singapore`, `mumbai` | `nearest` | +Docs: `plivo docs show voice-agents/audio-streaming/xml/stream` (); the 24 kHz value is on `plivo docs show voice/xml/audio-streaming` (). -This budget is separate from the XML response deadline: the XML overview gives Plivo 15 seconds for an XML response, so design the handler for that even though the read timeout defaults higher. The fragments apply to the console application's Primary Answer, Fallback Answer and Hangup URLs, to the Call API's `answer_url`, `ring_url`, `hangup_url`, `fallback_url` and `machine_detection_url`, to the Transfer API's `aleg_url` and `bleg_url`, to recording and transcription URLs, and to XML `action` and `callback` URLs. They do not apply to audio URLs used by `` or ``, which use fixed values. Three more things that matter for an agent: +Limits, from (cut off in the CLI) and the best-practices page: a stream URL of 2048 characters; one stream per call; WebSocket messages up to 64 KB, audio chunks of 16 KB base64 or less; about 20 ms of audio per `media` frame; a playback buffer of 40 s on the best-practices page (`DegradedStream` at 30, 60 and 90% full) and about 60 s in the guide. If the first connection fails, Plivo tries twice more, then drops the stream. Plivo closes the socket when the call ends. -- The applicable list on the callback configuration page is exhaustive and a `` `statusCallbackUrl` is not on it, so do not expect the fragments to change how that callback is retried. -- The fragment is not part of the signed URL, so adding one does not break signature validation. -- Plivo's Voice Alerts email you when callback failures exceed 5% or calls queue for more than 2 minutes (no setup). For a bot whose answer URL flaps under load, that alert is the earliest signal. +Plivo sends JSON text frames, never binary: -### Signature validation (V3) - -Every HTTP request from Plivo to your server, and the WebSocket upgrade request to your `` URL, carries three headers: `X-Plivo-Signature-V3`, `X-Plivo-Signature-Ma-V3`, `X-Plivo-Signature-V3-Nonce`. - -Use your SDK's helper. Every Plivo server SDK ships one, and the manual form is easy to get subtly wrong. Only implement it by hand if your language has no SDK, and then follow the worked example on the signature page exactly rather than this prose: - -1. Take the final request URL: scheme, host, port, path and query string. -2. POST only: append a `.`, then every POST parameter as `name` then `value`, sorted alphabetically by name with Unix-style case-sensitive sorting, and no separator between them. On a GET the parameters are already in the query string and this step adds nothing. -3. Append a `.`, then the nonce from `X-Plivo-Signature-V3-Nonce`. -4. HMAC-SHA256 with the Auth Token as the key. Base64-encode. -5. Compare in constant time with `X-Plivo-Signature-V3`. - -The separators matter and are visible in the documented worked example. For URL `https://example.com/abcd?foo=bar` with POST parameters `CallUUID`, `Digits`, `From` and `To` and nonce `kjsdhfsd87sd7yisud2`, the assembled string in the docs is `https://example.com/abcd?foo=bar.CallUuid4vbcpem8-0u46-x1ha-9af1-438vc92bf374Digits1234From+15551111111To+15555555555.kjsdhfsd87sd7yisud2`: a `.` between the URL and the sorted parameters, and a `.` before the nonce. If your manual implementation omits those two dots it will compute a different string and reject every genuine request. Read the current page before you ship: . - -Notes from the docs: - -- `X-Plivo-Signature-V3` is signed with the token of the account or subaccount that owns the number. `X-Plivo-Signature-Ma-V3` is always signed with the main account's token. -- If the account has more than one active Auth Token, the header holds a comma-separated list of signatures. Accept if any matches. -- V2 signatures are deprecated. -- SDK helpers: Python `plivo.utils.validate_v3_signature(method, url, nonce, auth_token, signature[, params])`; Node `plivo.validateV3Signature(method, uri, nonce, authToken, signature[, params])`; Ruby `Plivo::Utils.valid_signatureV3?`; Java `Utils.validateSignatureV3`; Go `plivo.ValidateSignatureV3`; .NET `XPlivoSignatureV3.VerifySignature`. -- For the WebSocket upgrade the method is `GET` and the URI is the full `wss://` URL Plivo dialled. Plivo documents a Node stream package that validates the upgrade signature for you (`PlivoWebSocketServer({ validateSignature: true, authToken })`). Before recommending any stream package to a user, check that it is actually published for their language on the current docs page and on that language's package index; do not assume from the docs alone. -- If validation fails and you return no XML, the call ends with 7011 or 8011. Return 400 or 401 only when you are sure the request is not Plivo's. -- Behind a load balancer or reverse proxy, sign-check the URL Plivo dialled: external scheme, host, port, path and query. A rewritten private URL (`http://`, a different port, a stripped prefix) is the usual cause of a false rejection. Read the forwarded-host headers or configure the public URL explicitly. -- The Audio Streaming protocol page carries a manual JavaScript signing example whose base-string description differs from the signature page. Where the two disagree, follow the signature page and the SDK helper. -- Never print the token to debug a mismatch. Log the method, the URL shape with the host only, which signature header you compared, the SDK version and the CallUUID. - -Tests worth running before go-live: a genuine GET and POST pass; one changed form value fails; a changed host, scheme, port or path fails; a missing signature or nonce fails; both signatures pass during a token rotation; a duplicate callback runs no side effect twice; logs show neither the token nor the caller's number in clear text where your policy forbids it. - -### Network - -- Plivo callbacks come from region-specific edge IPs (San Jose, Ashburn, Frankfurt, Sao Paulo, Sydney, Singapore, Mumbai; list on the firewall page). If your answer URL sits behind an IP allow-list, allow those. Otherwise leave it open and rely on signatures. -- Handing a call to a SIP contact centre: allow Plivo's outbound media IPs for the region there, or the transfer fails before the contact centre sees it. SIP signalling ports 5060, 5061, 5080; RTP 16384 to 32768 UDP. -- Bringing a number from another carrier into the agent: forward it to a Plivo number, or have the carrier send SIP INVITEs to `sip:{app_id}@app.plivo.com` protected by SIP authentication (IP ACL or digest credential on the application). Auth is resolved from the Request-URI, not the To header. 10 failed attempts in 60 s lock the source out for 60 s. - -### Multi-tenant notes (docs-backed parts only) - -- One application can serve every tenant. Route on `To` (the dialled number) and put the call's `CallUUID` or `To` into the WebSocket URL from your server. -- Use a subaccount per client when the client needs its own numbers, its own callback signature (`X-Plivo-Signature-V3` is signed with the subaccount token) or separate call-record exports. Subaccounts share the parent balance and geo permissions. -- India resellers submit one compliance application per end customer. -- The architecture guidance itself is not in the docs. - -## The WebSocket protocol: `` attributes, limits, events - -Facts from these docs pages: , , , , , , , , . Where pages disagree, both values are given and the one this skill follows is marked. Anything marked as an observation has no docs page behind it. - -### `` attributes (XML) and Start Stream API parameters - -| XML attribute | API parameter | Documented default | Notes | -|---|---|---|---| -| `bidirectional` | `bidirectional` | `false` | Required for an agent that talks back. | -| `keepCallAlive` | (none) | `false` | The stream runs exclusively; following XML runs only after the stream ends. Set it, except when a MultiPartyCall follows. | -| `contentType` | `content_type` | `audio/x-l16;rate=8000` (4 reference pages). The Getting Started guide says `audio/x-mulaw;rate=8000`. Always set it. | Values: `audio/x-mulaw;rate=8000` (native telephony, no transcoding), `audio/x-l16;rate=8000`, `audio/x-l16;rate=16000`, `audio/x-l16;rate=24000` (Voice API reference only). mu-law 8 kHz is the most common choice. | -| `audioTrack` | `audio_track` | `inbound` | `inbound`, `outbound`, `both`. With `bidirectional="true"` it cannot be `outbound` or `both` (docs). | -| `streamTimeout` | `stream_timeout` | `86400` s | Max stream duration; the stream ends with reason "Stream timeout" and, with nothing after ``, the call ends 4010. Usually left unset; short ceilings such as 300 s or 600 s cut real conversations at that second. | -| `statusCallbackUrl` | `status_callback_url` | (none) | Where stream lifecycle events go. Often left unset, which leaves you blind to `DroppedStream`. | -| `statusCallbackMethod` | `status_callback_method` | `POST` | `GET` or `POST` | -| `extraHeaders` | `extra_headers` | (none) | `k1=v1,k2=v2` in the XML reference, the API and the CLI; the guide and protocol reference show `;`. Max 512 bytes. The Stream page describes the value only as custom key-value pairs and its own example is `userId=12345,sessionId=abc123`. It does also print a constraint of `[A-Z]`, `[a-z]`, `[0-9]`, which cannot be read literally: taken at face value it forbids the `=` and `,` that the same page's example uses. That character set is the documented rule for SIP custom headers (`X-PH-` prefixed), where it exists so the header survives URL encoding, and it does not transfer to this attribute. Streamed calls whose keys carry `_`, `-` or mixed case end with a normal hangup, so treat unusual characters as worth testing rather than as a failure and never reject a document over the character set. Delivered in every event as the string `extra_headers`. Never put secrets here. | -| `noiseCancellation` | `noise_cancellation` | `"false"` | Real-time noise suppression on the inbound audio. | -| `noiseCancellationLevel` | `noise_cancellation_level` | `85` | Documented range 60 to 100. What Plivo does with a value outside that range is not documented; stay inside it. | - -Plivo does not substitute `{{CallUUID}}` or similar placeholders in the WebSocket URL. Your server renders the URL before it returns the XML. - -REST: `POST/GET/DELETE https://api.plivo.com/v1/Account/{auth_id}/Call/{call_uuid}/Stream/[{stream_id}/]`. Stream object fields: `stream_id`, `call_uuid`, `service_url`, `bidirectional`, `audio_track`, `content_type`, `start_time`, `end_time`, `bill_duration`, `rounded_bill_duration`, `billed_amount`. - -CLI: `plivo voice calls streams start --url wss://... --bidirectional --content-type audio/x-mulaw;rate=8000 --stream-status-callback https://... [--extra-headers k=v,k2=v2]`. The CLI default content type is `audio/x-l16;rate=16000`; pass it explicitly. Also `streams list `, `streams get `, `streams stop [stream_id]`. - -Stopping the stream with `DELETE .../Stream/` on a document whose only element is `` is expected to end the call, because the stream was the last thing in the document and a document that runs out ends the call. That is an inference from the documented `keepCallAlive` behaviour, not a rule Plivo publishes, and no page names a hangup code for it. Treat it as a risk: anything you then try to do to that call, including a transfer, may find no live call to act on. Order a handoff the other way round, transfer first and then close the socket, and verify it once on your own account. - -### Limits - -| Limit | Value | +| `event` | Key fields | |---|---| -| WebSocket URL length | 2048 characters | -| Concurrent streams per call | 1 | -| Max stream duration | same as the call | -| Playback queue buffer | 40 s (best-practices; `DegradedStream` fires at 30, 60, 90% full). The guide says about 60 s. | -| Max WebSocket message | 64 KB; recommended audio chunk 16 KB base64 or less | -| Connect retries | if the first WebSocket connection fails Plivo attempts twice more, then drops the stream (best-practices, "WSS Socket Connection Failures") | -| Chunk cadence | about 20 ms per `media` event; about 160 bytes at mu-law 8 kHz | -| Disconnect | Plivo closes the stream and socket when the call ends. You do not need to. | - -### Events Plivo sends to your server (JSON text frames) - -| `event` | When | Key fields | -|---|---|---| -| `start` | once, on connect | `sequenceNumber` (starts at 1), `start.callId`, `start.streamId`, `start.accountId`, `start.tracks` (for example `["inbound"]`), `start.mediaFormat.encoding` (for example `audio/x-mulaw`), `start.mediaFormat.sampleRate`, `extra_headers` | -| `media` | continuously | `streamId`, `media.track` (`inbound`), `media.timestamp` (ms epoch, string), `media.chunk` (per-track counter), `media.payload` (base64 raw audio; decode it, there is no WAV header) | -| `dtmf` | caller presses a key | `dtmf.digit` (`0-9`, `*`, `#`, `A-D`), `dtmf.track`, `dtmf.timestamp` | -| `playedStream` | playback reached a checkpoint you set | `name` (your checkpoint name) | -| `clearedAudio` | the queue was cleared after your `clearAudio` | `streamId` | - -`ForwardedFrom` is not in the `start` event. It arrives as an answer-URL parameter, and only when the carrier sends it. - -Handling rules from the protocol reference, plus two observations: - -- Everything is a JSON text frame; the audio is base64 inside `media.payload`. Plivo does not send binary WebSocket frames. -- Read the negotiated format from `start.mediaFormat`, not from the XML you think you returned. The two can differ when a proxy or a template rewrote the document. -- `extra_headers` is metadata, not authentication. Validate `X-Plivo-Signature-V3` on the upgrade request instead. -- The protocol reference documents no JSON `stop` input event, although the Stream XML page lists a "Stop" event in its WebSocket table. Handle the WebSocket close as the end of the stream, and accept a `stop` event if one arrives. `plivo voice streams test` does send a JSON `{"event":"stop"}` text frame before it closes, so the pre-flight exercises the `stop` path; a dropped call will not, so do not require `stop` to run your cleanup. -- One state machine per `streamId`; bound both queues; when the bot falls behind, drop or fail explicitly rather than let latency grow. -- A completed handshake proves nothing about decoded audio, return audio or a phone call. +| `start` | Once: `sequenceNumber` (from 1), `start.callId`, `start.streamId`, `start.accountId`, `start.tracks`, `start.mediaFormat.encoding`, `start.mediaFormat.sampleRate`, `extra_headers` | +| `media` | `streamId`, `media.track`, `media.timestamp`, `media.chunk`, `media.payload` (base64 raw audio, no WAV header) | +| `dtmf` | `dtmf.digit` (`0-9`, `*`, `#`, `A-D`), `dtmf.track`, `dtmf.timestamp` | +| `playedStream` | `name` of a checkpoint that has played | +| `clearedAudio` | `streamId`, after your `clearAudio` | -### Events your server sends to Plivo +The bot sends: -| `event` | Purpose | Body | -|---|---|---| -| `playAudio` | play audio to the caller (bidirectional only) | `media.contentType` (`audio/x-mulaw` or `audio/x-l16`), `media.sampleRate` (must match the stream's `contentType`), `media.payload` (base64 raw audio) | -| `checkpoint` | mark a point in the play queue; you get `playedStream` with the same `name` when it plays | `streamId`, `name` | -| `clearAudio` | drop everything queued. This is how barge-in works | `streamId` | -| `sendDTMF` | send tones into the call (drive an external IVR, enter a PIN) | `dtmf` (`0-9*#A-D` string) | - -`playAudio.media.contentType` has no `;rate=` part; the rate goes in `media.sampleRate`. mu-law is `audio/x-mulaw`, not `audio/pcmu`. The Stream XML page's example quotes `sampleRate` as a string and the protocol reference shows a number; test the one your SDK sends. Send raw audio, never a WAV or MP3 container. - -Pacing (docs): send audio at real-time cadence, about 20 ms per frame. Sending too fast gives the `buffer_overflow` error and fills the 40 s buffer, so `clearAudio` feels late. Use `checkpoint` to learn when a sentence finished. - -Recommended by the docs: support interruption with `clearAudio`; treat `*` as interrupt and `#` as repeat; combine STT end-of-speech with a 300 to 500 ms timeout for turn-taking; aim for under 1 s total response (STT under 200 ms, LLM under 500 ms, TTS under 200 ms, network under 100 ms); host near the callers (US East or West, Frankfurt or London, Singapore or Mumbai). Plivo connects from the edge nearest the caller. - -Time to first audio (not in the docs): call answer, then Plivo fetches the answer URL, parses the XML, opens the WebSocket, sends `start`, then your first `playAudio`. Read the first three timestamps from the console call debug log or `plivo voice calls diagnose`; measure the last from your own logs. There is no early-media or pre-answer stream in the docs, and no documented target for Plivo's own setup time. - -Echo: the docs describe noise cancellation only. There is no documented echo cancellation on the audio Plivo sends you. If your bot's own speech shows up in the inbound audio, gate STT while the bot speaks or use your STT's echo suppression. That is a bot-side technique, not a documented Plivo feature. - -### Stream status callbacks (HTTP to `statusCallbackUrl`) - -Two naming schemes appear in the docs. The best-practices and troubleshooting pages, and Plivo's console debug logs use: - -| `Event` | Meaning | +| `event` | Body | |---|---| -| `StartStream` | audio streaming began | -| `StopStream` | streaming stopped (call ended, API stop, or `streamTimeout`) | -| `DroppedStream` | WebSocket connect failed, was terminated mid-call, or was cut for being too slow. Sample debug-log `Error`: `connection disconnected with the remote service` | -| `DegradedStream` | slow connection; buffer 30, 60, 90% full | - -The Getting Started guide and protocol reference instead describe `Event` values `started`, `stopped`, `failed` with `StatusReason` and `Duration`. This skill follows the first scheme. Log the raw `Event` string you receive rather than assuming either. Fields common to both: `CallUUID`, `StreamID`, `Timestamp`, `From`, `To`, `Direction`. The XML reference adds the stream's `bidirectional`, `audioTrack`, `streamTimeout`, `contentType`, `extraHeaders`, `keepCallAlive`. +| `playAudio` | `media.contentType` (`audio/x-mulaw` or `audio/x-l16`, no `;rate=`), `media.sampleRate` matching the stream, `media.payload` (base64 raw audio, never a WAV or MP3 container) | +| `checkpoint` | `streamId`, `name`; `playedStream` comes back when playback reaches it | +| `clearAudio` | `streamId`; drops the queued audio (barge-in) | +| `sendDTMF` | `dtmf`, a `0-9*#A-D` string | -A 200 with any body (JSON is fine) acknowledges a status callback. Console: Voice, Logs, Calls, the call, Audio Streams shows stream UUID, start and end, duration, billed amount, hangup reason (`API request`, `Call hangup`, `Connection error`, `Stream timeout`) and a Debug logs link with the event list. Debug logs are the fallback when no `statusCallbackUrl` is set; they are manual. +- Read the codec from `start.mediaFormat`, not from the XML you think you returned. +- The protocol reference documents no JSON `stop` event, although the Stream page lists "Stop": end on the socket close, and accept a `stop` if one arrives. +- Send audio at real-time pace. Bursting fills the buffer (`buffer_overflow`) and makes `clearAudio` late; use `checkpoint` to learn when a sentence has played. +- The Stream page's example quotes `sampleRate` as a string and the reference shows a number: test what your SDK sends. +- `extra_headers` is metadata, not authentication. Only noise cancellation is documented, not echo cancellation: if the bot hears itself, gate STT while it speaks. +- Full schemas: `plivo docs show voice-agents/audio-streaming/concepts/audio-streaming-reference` (). -### Troubleshooting map (from the troubleshooting page) +Stream status callbacks: the troubleshooting and best-practices pages and the console debug logs use the `Event` values `StartStream`, `StopStream`, `DroppedStream` and `DegradedStream`; the guide and the reference describe `started`, `stopped` and `failed`. Log the raw value. Common fields: `CallUUID`, `StreamID`, `Timestamp`, `From`, `To`, `Direction`. Acknowledge with a 200 (any body). Symptoms and error strings (`connection_failed`, `authentication_failed`, `invalid_content_type`, `buffer_overflow`): `plivo docs show voice-agents/audio-streaming/troubleshooting/troubleshooting` (). -| Symptom | Check | -|---|---| -| Connection never establishes | URL is `wss://` (every documented example is; do not depend on plain `ws://`), public, valid non-expired certificate, firewall allows inbound, tunnel still running | -| Drops mid-call | `DegradedStream` callbacks (slow link), server crash (add reconnect and graceful handling), send periodic pings | -| No `media` events | `audioTrack` is `inbound` or `both`; handler registered before start; `start` arrived first | -| Caller hears nothing | `bidirectional="true"`; `playAudio` `contentType` and `sampleRate` match the XML; raw audio, no file headers | -| Garbled audio | sample-rate mismatch; wrong codec; headers left in the payload | -| Slow responses | mu-law 8 kHz, deploy near callers, stream TTS as generated, pool AI connections | -| Error strings seen | `connection_failed` (URL or SSL), `authentication_failed` (signature), `invalid_content_type` (format mismatch), `buffer_overflow` (sending audio too fast) | - -### What Plivo does inside ``, billing, data kept +To start a stream over REST instead of XML: `plivo voice calls streams start --url wss://... --bidirectional --content-type "audio/x-mulaw;rate=8000"`. Quote the content type (a bare `;` ends the shell command) and always pass it (the CLI default is `audio/x-l16;rate=16000`). Its `--stream-status-callback` sends `stream_status_callback_url`, a field the API does not document, so for status callbacks use `plivo api POST /Call//Stream/ --body '{"service_url":"wss://...","bidirectional":true,"content_type":"audio/x-mulaw;rate=8000","status_callback_url":"https://..."}' --dry-run`, then `--yes`. -Plivo moves raw audio only. Speech recognition, the model and the voice are yours. Plivo TTS exists only as `` before or after the stream; it cannot be injected mid-stream. If you want Plivo to run the AI and pick the voice in a console, that is the hosted AI Agents product, not ``. +Plivo moves raw audio only: STT, the model and the voice are yours, and Plivo TTS exists only as `` before or after the stream. A streamed call is billed as the underlying call; quote no prices. -A `` call is billed as the underlying Voice API call: from answer, per leg, 60 s minimum increment. The Stream object exposes `bill_duration`, `rounded_bill_duration` and `billed_amount`. Whether the stream itself carries a charge on your plan is not in the docs; the API example shows a non-zero `billed_amount`. Read the live pricing page for your currency. This skill does not quote prices. +## Callbacks, signature validation, timeouts -Plivo keeps call records, the Stream object (URL, times, billing), stream debug-log events, and recordings only if you add ``. Anything in the WebSocket URL or `extraHeaders` is logged. Retention periods, HIPAA and BAA are not in the voice docs; ask your account manager. +- Answer, fallback and `action` URLs must return Plivo XML; `hangup_url`, `ring_url`, `callbackUrl` and `statusCallbackUrl` need only a 200. Plivo retries, so make handlers idempotent: the keys are listed in `plivo docs show voice/concepts/callbacks` (). +- Per-URL timeouts, retries and edge region go in a URL fragment such as `#ct=2000&rt=5000&rc=2&er=mumbai`: `plivo docs show voice/concepts/callback-configurations` (). A `` `statusCallbackUrl` is not among the URLs it applies to. Still answer with XML well inside 15 s. +- Every Plivo request carries `X-Plivo-Signature-V3`, `X-Plivo-Signature-Ma-V3` and `X-Plivo-Signature-V3-Nonce`. Validate with your SDK's helper (Python `plivo.utils.validate_v3_signature`, Node `plivo.validateV3Signature`): `plivo docs show voice/concepts/signature-validation` (). Where the page's prose or worked example disagrees with the SDK, follow the SDK. With several active Auth Tokens a header holds a comma-separated list: accept any match. V3 is signed with the token of the account or subaccount that owns the number, Ma-V3 with the main account's. +- The WebSocket upgrade carries the same headers. Validate it as a `GET` over your stream URL with `wss://` replaced by `http://`, keeping the port and query as dialled: on a live call only that form matched, not `wss://` or `https://`. +- Behind a proxy, validate against the URL Plivo dialled (its public host, port, path and query, and `https://` for HTTP callbacks), not a rewritten private one. If validation fails and you return no XML, the call ends 7011 or 8011, so reject only what is surely not Plivo's. Never log the token. +- Firewalls: Plivo calls your URLs from regional edge IPs, listed on the firewall page (stage 7). Without an allow-list, rely on signatures. -### AI agent as a MultiPartyCall participant +## India: what a voice agent needs before its first call -XML: `room`; default content type `audio/x-l16;rate=8000`. -REST Add Participant: required `role` (`agent`, `supervisor`, `customer` or `ai-agent`), `from` and `to`; then for `role=ai-agent` the optional `ai_agent_stream_service_url`, `ai_agent_stream_content_type`, `ai_agent_stream_status_callback_url`, `ai_agent_stream_status_callback_method` and `ai_agent_stream_extra_headers` (`key=value`). The CLI's `plivo voice multiparty participant add --role` accepts only `agent|supervisor|customer`, so add an AI participant with `plivo api POST /MultiPartyCall/name_/Participant/`. +Never infer country rules from a phone prefix; check the account. Report each item as observed, not known or failed: -Add a Participant documents `role`, `from` and `to` as required arguments, and the `ai-agent` role is one of the documented `role` values, so send all three. The `ai_agent_stream_*` parameters are listed as the optional set to use when `role` is `ai-agent`. The skeleton below has every documented field in place, but it is **not runnable as written**: `to` is required and the docs do not say what value it should carry for a participant reached over a WebSocket, so fill that in only once Plivo has confirmed it. Preview it, do not send it blind: +1. **Organisation.** An India data-region organisation; the region is fixed at creation. The docs disagree on how: `rent-india-numbers` says the console's organisation switcher (no new signup), `india-calling` a second account with another email. Only India-registered businesses can rent Indian numbers and call on domestic routes; INR accounts can call only within India. +2. **KYC.** An `accepted` compliance application: `plivo numbers compliance list --country IN --status accepted -o json`. `submitted` is not `accepted`. A number's `compliance_status` in `numbers get` is not in the published schema, so treat a missing field as not known. +3. **Series.** Landline (022, 080) for service and transactional calls; 140-series for promotional calls only; 160-series for BFSI only. The wrong series makes every complaint count as UCC, even with consent. 140 and 160 numbers are provisioned offline (Tata DLT registration, a NOC, header and template approval, days to weeks, no CLI): `plivo docs show voice/concepts/140-series-provisioning` () and `plivo docs show voice/concepts/160-series-provisioning` (). Which series allow `` is not documented. +4. **Media anchoring.** Both legs and your bot's server stay in India, or the call fails with 2070. +5. **Consent.** Cold calling is prohibited. TRAI counts a pre-recorded or AI-generated voice call as A2P. Service calls (about what the customer already has) and transactional ones (within 30 minutes of their transaction) need no explicit consent; one that supports an ongoing purchase or use needs it, valid 7 days (renewable) or until revoked. A UCC complaint needs opt-in proof within 5 business days or the compliance ID is blocked; repeated complaints suspend it, and numbers on a suspended application cannot place calls. Remove complainants at once. Rules: `plivo docs show voice/concepts/ucc-management` (). Complaints are readable with `plivo api GET /Ucc/`; proof is a multipart upload the CLI cannot send, so upload it on the console UCC dashboard. +6. **Capacity.** Professional accounts start at 50 concurrent calls. Over the limit a call is rejected at once with 5030 and Make Call returns HTTP 403. Raise the limit with Request Enterprise under Organization settings > Account limits before a campaign: `plivo docs show voice/concepts/account-limits` (). +7. **Caller ID.** A Plivo-rented Indian number; Verified Caller ID does not apply in India. -```bash -plivo api POST /MultiPartyCall/name_consult-/Participant/ --dry-run --body '{ - "role": "ai-agent", - "from": "", - "to": "", - "ai_agent_stream_service_url": "wss://voice.example.com/ws/consult-", - "ai_agent_stream_content_type": "audio/x-mulaw;rate=8000", - "ai_agent_stream_status_callback_url": "https://voice.example.com/plivo/mpc-stream-status", - "start_mpc_on_enter": true }' -``` +Eligibility and calling rules: `plivo docs show voice/concepts/india-calling` (). -then the same command with `--yes` once the user approves. Read the previewed body back to them first: this adds a participant to a live room. +### KYC from the CLI -Still not documented, so do not state any of it as fact: what `to` should be for an AI participant that is reached over a WebSocket rather than dialled, and whether Plivo dials or bills that leg; what the AI hears, the full room mix or one party; whether hold and mute must be set together to park a party, and the ordering when switching parties; whether the XML form opens the socket the same way the REST form does. Ask Plivo before you build on any of those. Use the documented-attributes-only answer document below, add the AI over REST, then dial the human with `plivo voice multiparty participant add consult- --from --to --role agent --dry-run` followed by `--yes`, and park a party with `plivo api POST /MultiPartyCall/name_consult-/Member// --body '{"mute": true, "hold": true}' --dry-run` then `--yes`. Test each step on a real room before relying on it, and write down what you observe. +You run the commands; the user supplies the certificate files and the exact legal details, and says "go" before anything is filed. Never fill in a value yourself. -```xml - - consult-{{CallUUID}} - +```bash +plivo numbers compliance requirements --country IN --number-type local --user-type business -o json # what to supply, now +plivo numbers compliance create --data @app.json --file 'documents[0].file=@cert.pdf' --dry-run # then --yes -o json after "go" +plivo numbers compliance get --expand documents -o json # poll: 080/022 review is automated, typically about 5 minutes +plivo numbers buy --dry-run # accepted: direct brands get it attached at purchase +plivo numbers compliance link --link +91XXXXXXXXXX= --dry-run # numbers you already had ``` -## Hangup causes and end-of-call patterns - -Docs pages: , , , , , , , . A line marked as an observation describes what streaming deployments run into in practice and has no docs page behind it; a line marked "not verified" is neither documented nor confirmed. Do not repeat either as a documented Plivo rule. - -Where to read them: `plivo voice calls get -o json` gives `hangup_cause_code`, `hangup_cause_name`, `hangup_source`, `answer_time`, `end_time`, `bill_duration`; `plivo api GET /Call//` returns the full record. The `hangup_url` callback carries the raw telephony `HangupCause` (`NORMAL_CLEARING`, `USER_BUSY`, `NO_ANSWER`, `CALL_REJECTED`, `UNALLOCATED_NUMBER`, `NETWORK_OUT_OF_ORDER`). The Dial `action` URL carries `DialHangupCause` and `DialBLegHangupCauseCode` for the human leg. Console: Voice, Logs, Calls; each call has Audio Streams and Debug logs tabs. Hangup sources (docs): `Caller`, `Call recipient`, `Plivo`, `Carrier`, `API Request`, `Answer XML`, `Error`, `Unknown`. - -Triage order: read the call record for every leg; inspect the exact answer, action or transfer HTTP exchange; check the exact body against the XML checklist; read the stream status callbacks and the bot's close or error log; read Call Insights per leg; retest the WebSocket on its own. Do not repeat a billable call until the failed layer passes. - -### Normal ends of an agent call - -| Code | Name | What it means on a `` call | Check | -|---|---|---|---| -| 4000 | Normal Hangup | Caller or callee hung up. The most common end. | Nothing. | -| 4010 | End Of XML Instructions | The XML ran out. With `keepCallAlive="true"` and nothing after ``, this is the normal end when the bot closes the socket. | A call that lasted only a few seconds is a reason to look at the bot's connect handler, the stream status callbacks and the console Audio Streams log. The call record does not say who closed the socket, so do not report the bot as the cause until one of those three confirms it. | -| 1000 | Cancelled, source `API Request` | Your backend hung up with the Hangup API or `plivo voice calls hangup`. A normal way for a bot to end a call. | Nothing, if your code did it. | -| 4020 / 4030 | Multiparty Call Ended / Kicked Out | The MPC room ended or a participant was removed. Expected in the room patterns. | Was the room end intentional? | -| Source `Answer XML`, code 4000 | Your XML ended the call | `` after `` runs when the socket closes and ends the call cleanly. | Nothing. | +- `requirements` is the source of truth: one document, and one `--file`, per returned type. The pages disagree on the count, so never hard-code it. A business PAN alone is not accepted, the same file in two slots is rejected, and the first application must be sealed and signed by an authorised signatory. +- `app.json`: copy the India example under "Create" in `plivo docs show numbers/compliance` (). The legal name goes in `end_user.name` and `data_fields.business_name` exactly as printed on the certificate; the CIN, Udyam number or GSTIN comes from the documents. Resellers file one application per customer, named in `alias`. +- `rejected`: read `rejection_reason`, fix it, and run `plivo numbers compliance update` (valid only on `rejected`; it replaces every document, so re-attach every file). +- `compliance_application_id is required` on `buy` (a reseller, or nothing to attach): `numbers buy` has no flag for it, so `plivo api POST /PhoneNumber// --body '{"compliance_application_id":""}' --dry-run`, then `--yes`. +- `calls make` from a number whose application is not `accepted` fails with `cannot place calls as its compliance application is not in 'accepted' status`; `suspended` means unresolved UCC complaints. -Two patterns that look like failures but are usually not: +Documents, statuses and rejection reasons: `plivo docs show numbers/rent-india-numbers` (). -- The human hangs up within a few seconds of answer (source `Caller` inbound, `Callee` outbound). Read the hangup source before you blame the socket: a caller-sourced hangup is a person, not a defect. Causes the docs point at: silence before the first word, a slow greeting, voicemail. Fix on your side: speak first and fast, keep `` before `` short or drop it, use answering-machine detection outbound. -- The bot closes the socket a few seconds after answer (code 4010, source `Plivo`). Check the bot's connect handler and `plivo voice streams test --bidirectional`. +## Outbound -### Failures Plivo attributes to your URLs and XML - -| Code | Name | Meaning | First check | -|---|---|---|---| -| 7011 | Error Reaching Answer URL | Non-2xx, unreachable, timeout or empty body from the answer URL. Not only a day-one problem: mature deployments whose answer URL fails under load produce it for months. | `curl -s -i -X POST -d 'CallUUID=x&From=%2B1&To=%2B1&Direction=inbound&Event=StartApp'`, then the XML checklist. Set a fallback URL. Alert on the 7011 rate, not just on the first call. | -| 8011 | Invalid Answer XML | The answer URL replied, but not with Plivo XML: JSON (a console flow application with no flow, or an API endpoint), HTML, malformed XML, a `` Plivo does not support, a Twilio element such as ``. | Run the checklist on the body. Console debug logs show the body Plivo saw. | -| 8012 / 7012 | Invalid / Error Reaching Action XML | The second document (GetDigits, GetInput, Record or Dial `action`) was bad or unreachable, typically well into the call. Only fatal when `redirect="true"` (docs). | Check the action handler's response, without the `` requirement. | -| 8013 / 7013 | Invalid / Error Reaching Transfer URL | The transfer URL (stage 7) failed or returned bad XML. | `curl` the transfer URL and check it the same way. | -| 8014 / 7014 | Invalid / Error Reaching Redirect XML | A `` target failed. | Same. | -| 7022 to 7034 | Invalid URL / Invalid Method | An action, transfer or redirect URL is not `http(s)://`, or the method is not GET or POST. | Fix the attribute. | -| 3020 / 3010, source `Answer XML`, 0 s | Rejected / Busy Line | Your XML returned `` or `` as the first element. Observed pairing, not a documented one: the routing page gives the audible effect (rejection tone, busy signal) and the hangup-causes table describes 3020 and 3010 from the called party, so this mapping comes from call records. Deliberate call screening for some deployments. | Intended? Then fine. If not, find who added the `reason`. A bare `` has not been seen to land here: it ends the call gracefully and shows as a normal end from source `Answer XML`. | -| Source `Answer XML`, 4000, no stream | Message-only IVR | `` or `` then ``: out-of-hours or deflection messages. | Intended? Fine for a non-agent document. | - -### Timeouts and ceilings - -| Code | Name | Meaning | Check | -|---|---|---|---| -| 4010 at exactly N seconds, source `Plivo` | streamTimeout or another fixed ceiling | `streamTimeout` caps the stream (docs: default 86400 s). When it fires the stream stops and, with nothing after ``, the call ends 4010. Calls that all end at the same second (300, 600, 1200 s) point at a configured ceiling. Per call, only a `StopStream` callback with reason "Stream timeout" or the console Audio Streams log proves it. | A 300 s or 600 s ceiling cuts real conversations. Set it to your longest acceptable call or leave the default. Rule out the bot's own timer too. | -| 6000 | Scheduled Hangup | Max call duration (`time_limit` on the API, `timeLimit` on Dial; default 4 h, max 86400 s). | Intentional? | -| 6010 | Ring Timeout Reached | Not answered within `ring_timeout` (default 120 s). Expected on outbound campaigns. | Tune `ring_timeout`. | -| 6020 | Media Timeout | No media packets for 60 seconds (docs). The docs say: check network connectivity. This code alone does not say which side lost media. | Check both media paths and the carrier. Call Insights shows packet counts per leg. | -| 0 | Unknown | Hangup reason undetermined. Docs note a known bug: the Delete All Calls API sets it. | Check the debug logs; contact Plivo support with the UUID if it recurs. | - -### Handoff (``) outcomes - -The human leg is a separate B-leg with its own call record. A meaningful share of human legs never answer (busy, caller cancelled, SIP endpoint not registered, lost race, no answer). Read `DialStatus` on the Dial action URL: `completed`, `busy`, `failed`, `cancel`, `timeout`, `no-answer`. - -| Code | Name | Meaning | Check | -|---|---|---|---| -| 4010 + raw `USER_BUSY` | End of XML, busy target | The human was busy and nothing followed ``. | Put `` or `` after `` so the caller is not dropped. | -| 2020 | Endpoint Not Registered | The SIP softphone or endpoint is offline. | Check the agent's registration before dialling; fall back to a number. | -| 9000 | Lost Race | Another parallel Dial leg answered first. Normal for simultaneous dial. | Nothing. | -| 4240 / 4250 | sip_auth_failed / sip_auth_timeout | The SIP contact centre rejected the credentials or did not answer the digest challenge. | Check `sipAuthUsername`, `sipAuthPassword`, realm, IP allow-list. | -| 4210 | sip_auth_failed (inbound) | An external carrier sending SIP to your application failed the IP ACL or credential check. | See . | -| 9110 | Confirm Key Challenge Failed | `` was not pressed by the human leg. | Check the confirm prompt and key. | -| A-leg continues after the B-leg ends | Caller returned to the bot or to the next element | This is what a `` after `` produces. | Make sure the next element is what you want the caller to hear. | - -### Carrier and destination codes an outbound agent sees daily - -These never start a stream; they come from the carrier or the destination. - -| Code | Name | Meaning (docs) | Action | -|---|---|---|---| -| 3000 | No Answer | Destination unavailable or unreachable. | Retry later. | -| 3080 | Carrier-side error | The carrier returned an error. | Retry; if persistent, Plivo support. | -| 2000 | Invalid Destination Address | Not E.164 or invalid. | Fix list hygiene. | -| 3070 | Request Timeout | Carrier did not respond in time. | Retry. | -| 3050 | Unallocated Number | Destination invalid or out of service. | Prune the list. | -| 3110 | Declined | Destination cannot or will not participate. | Verify the destination accepts calls. | -| 5020 | Routing Error | Could not route. | Plivo support with the call UUID. | -| 3040 | Forbidden | Destination rejected or blocked the call. | Recipient may block your number. | -| 3090 | Network Congestion | Carrier overloaded. | Retry with backoff. | -| 5000 | Network Error | Fatal network condition. | Plivo support with the call UUID. | -| 5010 | Platform-side error | Plivo-side error (docs). | Plivo support with the call UUID. | -| 2060 | Loop Detected | The B-leg would redial the A-leg's number. | Fix routing logic. | -| 3130 | Spam block | Carrier rejected on spam reputation. | STIR/SHAKEN A with your own Plivo number, Caller Reputation, list hygiene. | -| 3140 | DNO Caller ID | Caller ID is on a Do-Not-Originate list. | Use another number. | -| 3030 | Unknown Caller ID | Caller ID is neither a number rented on this account nor an accepted verified caller ID for this route. | Use a Plivo number you rent. | -| 2030 / 2040 / 2050 | Destination Country / Number / Prefix Barred | Geo permissions (Professional plan: US and India only). | Console, Voice, Geo Permissions. | -| 2010 / 3100 / 3120 | Destination Out Of Service / Busy Everywhere / User Does Not Exist Anywhere | Destination-side conditions (docs). | Verify the number; retry later or prune. | -| 2070 | Violates Media Anchoring | India: a leg or your server is outside India. | India section, media anchoring. | -| 5030 | Concurrency Limit Breached | India: over the account's concurrent-call limit. Rejected instantly. | Stagger; ask Plivo support to raise it. | -| 1010 | Cancelled (Out Of Credits) | Balance hit zero. | `plivo account get`; auto-recharge. | -| 1020 | Cancelled (Simultaneous dial limit) | Too many concurrent dials to one destination. | Pace the campaign. | -| 9100 | Machine Detected | Voicemail with `machine_detection=hangup`. | Expected; watch short-call thresholds. | - -### Audio quality flags (Call Insights) - -The console shows "Suspected Issues" per leg. Docs mapping: One-Way Audio: packet count and audio level. Broken Audio: packet loss. Robotic Audio: jitter. High Connect Time: post-dial delay. Audio Lag: round-trip time. Also `LOW_AUDIO_LEVEL`, `MEDIA_IP_NOT_WHITELISTED` (fix your firewall allow-list; never disable the firewall) and `NO_AUDIO`. The docs note that quality statistics are not available for all PSTN calls. What the flags mean for your bot's WebSocket leg is not documented (not verified). Use them to decide whether to look at the carrier leg or at your server. - -### Stream-level reasons (not hangup codes) - -Stream status callback `Event=DroppedStream` (socket failed to connect, was terminated, or was too slow) and `DegradedStream` (buffer 30, 60, 90% full) tell you the bot side failed while the call itself may have ended 4010. Console Audio Streams log "Hangup reason": `API request`, `Call hangup`, `Connection error`, `Stream timeout`. - -Codes not listed in the docs still appear in `hangup_cause_name`; contact Plivo support with the `call_uuid`. SIP trunking and MPC `termination_cause_code` use separate code sets. - -### Escalation packet for Plivo support - -Send: the call UUID and any parent or B-leg UUIDs; UTC timestamps; hangup code, name and source per leg; region; the answer URL host (never the full URL with query or credentials); HTTP status and response time from your logs; the exact XML body Plivo fetched, redacted; stream status callback events received; the WebSocket close code and reason from your server; Call Insights flags; and whether `plivo voice streams test --bidirectional` passes against the same host. Never include tokens or caller audio. - -## Questions customers ask that the docs do not answer - -Say so plainly, then point at the source. Do not fill the gap from memory. The recurring gaps: the per-minute price of a streamed call and whether the stream itself is billed (read the live pricing page or `plivo ask`; the Stream object exposes `bill_duration` and `billed_amount`); Plivo's answer-to-`start` setup time and any pre-answer stream (measure from the console debug log); `` on SIP-trunk legs, which Indian series allow it, and the answer-URL length limit (ask Plivo support); enablement (no switch exists; what blocks people is US-trial outbound gating and India KYC); data retention, HIPAA and BAA (ask your account manager); echo cancellation (only noise cancellation is documented); which stream packages are actually published for a given language (check the current integration guide and that language's package index before recommending one, and fall back to the raw protocol, which is fully documented). Third-party agent platforms that connect over SIP do not use `` at all: that is , and the skill for it is a separate install (`npx skills add https://www.plivo.com/docs --skill plivo-sip-trunking`). Plivo's hosted AI Agents product is a different product again. - -| Theme | What the skill can say | Source or boundary | -|---|---|---| -| A. Connect my bot | Return `` from the answer URL and speak the WebSocket protocol. Pass a short opaque id in the URL for correlation. Plivo documents a Node stream package that validates the WebSocket signature. Check the current integration guide and the language's package index before recommending a package; the raw protocol is documented and always available. | `voice-agents/audio-streaming/concepts/audio-streaming-guide`, `.../integration-guides/plivo-stream-sdk` | -| B. Answer or XML errors, URL length | 7011 is an HTTP problem; 8011 is a body problem. Capture the exact body and run the XML checklist. For the general XML rules beyond a streamed call, install `plivo-voice-xml` or read the XML overview page. The Stream URL limit is 2048 characters; the answer-URL limit is not published (the console rejects very long ones). Carry context through `CallUUID`, `X-PH-*` SIP headers or a short id. | `voice/troubleshooting/hangup-causes`, `voice-agents/audio-streaming/xml/stream` | -| C. Latency and pacing | Time to first audio = answer, answer-URL fetch, XML parse, WebSocket open, `start`, your first `playAudio`. Read the first steps from the console call debug log; measure the last from your logs. Send 20 ms frames at real-time cadence; bursting fills the 40 s buffer and delays barge-in. Plivo's own setup time and any pre-answer stream are not documented. | `.../concepts/audio-streaming-reference`, `.../concepts/best-practices`, `.../troubleshooting/troubleshooting` | -| D. Human handoff | Transfer API then `` in a later document, or `` or `` after `` in the same document. Order it API first, then close the socket. Never `DELETE .../Stream/` first: on a document that ends at the stream, stopping the stream ends the call. | stage 7; `voice-agents/audio-streaming/xml/stream` (keepCallAlive); `voice/api/calls` (Transfer) | -| E. Audio problems, echo | Match `contentType` in both directions; `bidirectional="true"`; raw audio, no headers. `audio/x-mulaw;rate=16000` is not one of the three documented audio formats; mu-law is documented at 8 kHz only. Only noise cancellation is documented; there is no documented echo cancellation on the audio Plivo sends you. | `voice-agents/audio-streaming/xml/stream`, `voice/call-insights` | -| F. Pricing and billing | Billed as the underlying call, per leg, from answer; the Stream object exposes `bill_duration` and `billed_amount`. Whether the stream itself is charged on a given plan, and the price, are not in the docs: read the live pricing page or `plivo ask`. Quote no number. | `voice/api/calls` (When Billing Starts), `voice-agents/audio-streaming/api/audio-streams` | -| G. Enablement | `` has no switch. What blocks people: outbound voice disabled on US-region trial organisations ("Voice capability is currently disabled"; not in the docs), India KYC, and separately enabled products. | `voice-agents/audio-streaming/overview`; India section | -| H. Recording | `` before ``. Disclosure, consent and retention are the customer's to check. | `voice/xml/record`, `voice/api/recordings` | -| I. DTMF and barge-in | `dtmf` events over the socket; `clearAudio` for interruption; `checkpoint` and `playedStream` to learn what played. Keypad menus before the stream use ``. | `.../concepts/audio-streaming-reference`, `voice/xml/input` | -| J. Callbacks, signatures, data kept | Validate V3 signatures; handlers idempotent on `CallUUID` or `StreamID` plus event. Plivo keeps call records, the Stream object, debug-log events and recordings you asked for; anything in the URL or `extraHeaders` is logged. Retention periods, HIPAA and BAA: not in the voice docs; ask the account manager. | `voice/concepts/signature-validation` | -| K. Multi-tenant | One application can serve every tenant (route on `To`). A subaccount per client only for separate numbers, a separate signature token or separate call records. Architecture guidance itself is not in the docs. | multi-tenant notes above | -| L. Speech and AI inside the stream | Plivo moves raw audio only. STT, the model and the voice are the customer's. Plivo TTS exists only as `` before or after the stream. | `.../concepts/audio-streaming-guide` (AI Service Credentials) | -| M. Calls not connecting, same number for humans and AI | The debug loop above (`diagnose`, `get`, the XML checklist, `streams test`). The application decides per call whether to return `` or ``. `ForwardedFrom` arrives only when the carrier sends it and is not in the `start` event. | hangup causes; `voice/concepts/callbacks` | -| N. India | Data region, KYC, series, media anchoring, consent, UCC, concurrency: the India section. Which series allow `` is not documented. | `voice/concepts/india-calling`, `numbers/rent-india-numbers` | -| O. Answering-machine detection | Asynchronous; `Machine=true` reaches `machine_detection_url` after your XML already ran. Not a speech classifier inside the socket. | `voice/concepts/machine-detection` | -| P. SIP-based agent platforms | Agent platforms that connect over SIP trunking do not use ``. Do not translate SIP settings into Stream attributes. That is a separate skill and a separate install: `npx skills add https://www.plivo.com/docs --skill plivo-sip-trunking`. | `voice-agents/sip-trunking` | -| Q. Hosted AI agents | A different product surface. This skill cannot answer its access, pricing or model questions. | `voice-agents` overview | -| R. Browser, WebRTC and SIP endpoints | Their own SDKs and docs; out of scope here. | `sdk/client`, `voice/api/endpoints` | -| S. Generic call debugging | Hangup causes and Call Insights; stream-specific checks only if a stream started. | hangup causes above | -| T. Sales, trial, demo | Do not invent eligibility, credits, provisioning times, prices or approval outcomes. Point at the commercial pages or the account manager. | public commercial pages | - -Two more items with no docs answer: whether `` works on SIP-trunk legs, and the maximum answer-URL length. Route both to Plivo support. +- Prove inbound, or at least stage 3, first. +- Answering-machine detection is asynchronous: `calls make --machine-detection true` (or `hangup`) delivers `Machine=true` to `machine_detection_url` after your `` has started. The CLI has no flag for that URL or the timing parameters, so use `plivo api POST /Call/` (preview first). On a machine, hang up, transfer to a voicemail URL, or tell the bot. Parameters: `plivo docs show voice/concepts/machine-detection` (). +- US: use a Plivo number rented on this account as the caller ID; it is the only way to get STIR/SHAKEN attestation A. Do not hang up on every voicemail: short calls count against the quality thresholds and draw surcharges. Calls above your CPS are queued, so pace your own requests. Thresholds and CPS: `plivo docs show voice-agents/audio-streaming/deploy/us-call-quality-and-cps` (); concurrency: the account-limits page above. +- Professional plans can call only the US and India; other countries need Enterprise, and barred destinations fail with 2030: `plivo docs show voice/concepts/geo-permissions` (). +- A US-region trial organisation may see "Voice capability is currently disabled for this account" on outbound calls: request access in the console (not in the docs). +- Measure your own answer and connect rates by destination, list and time window. ## When this skill does not have the answer -Do not guess, and do not fill the gap from general knowledge of other platforms. In order: - -1. **Read the current documentation.** Every page on is available as Markdown by adding `.md` to its URL, and lists every page. Start at , and . From a terminal `plivo docs search ` searches the full text of every page, `plivo docs list` prints the index and `plivo docs show ` prints one page; those three need no credentials and are not rate limited, so reach for them before the assistant. -2. **Ask Plivo's assistant from the terminal**: `plivo ask ""`. It reads the documentation and can see the account, so it answers things this file cannot: what a specific call did, whether a compliance application is accepted, what a destination costs. It is limited to five requests per ten minutes per account, so save it for the question you cannot answer another way. `plivo voice calls diagnose ` is the same assistant pointed at one call, and it shares that limit, so do not loop either. -3. **If you have no CLI access**, tell the person you are working with to ask the same question to the assistant in the Plivo console. - -Treat the answer as evidence, not as final. If it contradicts the documentation, say that it does and prefer the documentation for published behaviour. If it gives a number the documentation does not publish, repeat it as something the assistant said, not as a documented fact. - -For CLI behaviour, `plivo --help` outranks this file: if the two disagree, the CLI is right and this file needs updating, and you should say so. Never invent flag names, XML attributes or hangup codes. Where Plivo's own pages disagree (codec default, `extraHeaders` separator, stream-callback event names, playback buffer size, short-call threshold), this file names both values and which one it follows. - -## CANNOT - -- Cannot treat an accepted API request as success. `calls make` returning a `request_uuid` means Plivo accepted the request, not that the bot spoke. A checklist pass, an HTTP 2xx or a WebSocket handshake is not success either. A call worked only when the call record, the callbacks, the bot's log and a human who heard the audio agree. -- Cannot spend or reconfigure without a preview. Before `calls make --yes`, `numbers buy --yes` or re-attaching a number, show the `--dry-run` output or the current number-to-application binding, and offer the rollback command (`plivo numbers update --app-id `). Never bulk-call from this skill. -- Cannot infer account state. Look numbers, applications, compliance applications and calls up with the CLI. Never assume a number is voice-enabled, compliance-approved, in the right data region, or attached to the right app. -- Cannot fill in or submit KYC on its own. Business name, CIN or Udyam number, GSTIN and address are copied from the documents the user supplies, never inferred. A compliance application is a regulatory filing and is only submitted after the user explicitly says so. KYC, series choice, 140 and 160 provisioning and UCC proof have fixed timelines that cannot be shortcut. Verified Caller ID does not exist for India. -- Cannot decide legal compliance. Consent, disclosure, recording, retention and calling-hours rules need the user's own legal review; this skill states the platform rules only. -- Cannot overstate platform rules. Plivo accepts `application/xml` or `text/xml`. Use HTTPS and `wss://`: every documented example does. Whether Plivo would accept plain `ws://` or `http://` is not documented, so do not claim either way. -- Cannot name a `contentType` default or a stream-callback event name as certain. The docs disagree; set the codec explicitly and log the raw `Event` you receive. -- Cannot invent CLI flags for what the CLI lacks: no `applications update --fallback-answer-url`, no `calls make --machine-detection-url`, no `numbers buy --compliance-application-id`, no `participant add --role ai-agent`, no verified-caller-ID or UCC commands. Use `plivo api ` or the console and say so. -- Cannot diagnose calls on another account. `diagnose` and `ask` share a rate limit. -- Cannot declare a number ready. Static and synthetic checks end at "the Plivo side looks right". Only a real call observed in this session closes gate 5. - -This skill does not cover: general Plivo XML in full (every element other than ``: separate install, `npx skills add https://www.plivo.com/docs --skill plivo-voice-xml`), SIP-trunking agents (separate install, `--skill plivo-sip-trunking`), SMS or WhatsApp, the Browser SDK, WebRTC and SIP endpoints, conferences beyond the room patterns, number masking, SSML depth, pricing, or the inner workings of any bot framework. It cannot see your server's logs or your Plivo account's data; use the CLI commands it names for that. - -Sources: the Voice API and Audio Streaming sections of the Plivo docs (pages cited above), and the CLI's own `--help` output. Where a rule is marked as an observation rather than documented, it has no docs page behind it: re-verify it before you rely on it, and re-verify every rule in this file if the platform changes. +Do not guess from other platforms. Search the docs first, without credentials: `plivo docs search `, then `plivo docs show `. Next, `plivo ask ""`, which can also see the account; it shares the small rate limit with `diagnose` (on `RATE_LIMITED`, wait as told). Treat its answer as evidence and prefer the docs where they conflict. Without the CLI, ask the assistant in the Plivo console. The docs do not answer the price of a streamed call, Plivo's setup latency, data retention, HIPAA or BAA, which India series allow ``, `` on SIP-trunk legs, or the longest accepted answer URL: say so and route to Plivo support or the account manager. Out of scope: SIP-connected agent platforms (`plivo skill install sip-trunking`), Plivo's hosted AI Agents, SMS, browser and SIP endpoints, and the inside of the bot. diff --git a/cli-skill/SKILL.md b/cli-skill/SKILL.md index 793c06e..6ae4af3 100644 --- a/cli-skill/SKILL.md +++ b/cli-skill/SKILL.md @@ -1,126 +1,29 @@ --- name: plivo-cli -description: Use the `plivo` CLI binary instead of raw curl for any Plivo task — sending SMS/MMS/WhatsApp, making calls, managing numbers/applications, verify OTP, logging in. Trigger whenever the user mentions Plivo, plivo auth_id/auth_token, or asks for a Plivo HTTP call. +description: Runs Plivo tasks with the plivo CLI instead of raw curl, covering SMS/MMS/WhatsApp, calls and call diagnosis, numbers, applications, Verify OTP, login and profiles. Use for any Plivo API request or plivo command, or when a Plivo auth_id or auth_token comes up. Not for answer-URL XML, WebSocket bots or SIP design (see plivo-voice-xml, plivo-audio-streaming, plivo-sip-trunking). --- -# plivo-cli skill +# plivo-cli -Single Go binary on PATH (installed as `plivo`). Prefer the CLI over curl — the JSON output is ~10x cheaper to consume than raw REST and the error envelope is stable across commands. For any endpoint the CLI doesn't wrap, use the generic `plivo api` escape hatch (below) rather than curl. +`plivo` is a single binary. Prefer it to curl: it authenticates from a saved login, previews with `--dry-run`, and returns a stable JSON envelope with typed errors. For an endpoint it does not wrap, use `plivo api `. -> Ships inside the plivo-cli binary. `plivo skill list` reports whether this -> copy still matches your binary; `plivo skill install cli` refreshes it. +`plivo --help` is the source of truth for commands and flags: read it before running a command you have not used, and never guess a flag. `plivo docs search ` and `plivo docs show ` read the Plivo docs (API parameters, XML, error codes) from the terminal, with no login. -## If you are an AI agent +## Installation -- `export PLIVO_FEEDBACK_PROMPT=0` and `CI=1` before any command (suppresses the feedback prompt and any TTY-only interactives). -- Auth: the machine must already have a profile from `plivo login`. There is no headless credential path — see Authentication below. -- Always pass `-o json`. Success: `{"data": }` on stdout, exit 0 — for lists the rows are at `data.objects`, with paging at `data.meta`. Error: `{"error": {"code", "message", "hint", "retryable", "status_code", ...}}` on stderr, non-zero exit. -- Never invoke interactive commands: `plivo login` (browser flow), bare `plivo feedback` (prompts). -- Multiple message recipients use `<` as the separator, **quoted**: `--dst "+14155551111<+14155552222"`. (This is Plivo's native delimiter — the CLI passes `dst` through verbatim. Commas do NOT work.) -- Preview any spend command with `--dry-run` first; add `--yes` to actually execute. +Not on PATH: `brew install plivo/tap/plivo`, or `curl -fsSL https://raw.githubusercontent.com/plivo/plivo-cli/main/install.sh | bash`, then `plivo --version`. This skill ships inside the binary: `plivo skill list` shows whether the installed copy is stale, and `plivo skill install` refreshes it. -## 60-second quickstart +## Rules -1. Install: `curl -fsSL https://raw.githubusercontent.com/plivo/plivo-cli/main/install.sh | bash` -2. Log in: `plivo login` (opens browser). -3. Verify: `plivo auth whoami`. -4. First command: `plivo voice calls list --limit 5`. -5. Anything that spends money: add `--dry-run` first, then `--yes` to confirm. - -## Authentication - -- Browser OAuth (PKCE) via `plivo login` is the only credential source. There is no env var and no flag that accepts a raw `auth_id` / `auth_token`. -- Resolution precedence: `--profile ` flag → active profile in `~/.plivo/config.toml`. -- Headless (agents/CI): `plivo login` needs a browser, so DO NOT call it. The machine must already hold a profile. If `plivo auth whoami` fails, stop and ask a human to run `plivo login` there — you cannot supply credentials yourself. -- The CLI never prompts or reads stdin for credentials. - -## When to invoke - -- Any Plivo REST op (numbers, messages, calls, applications, verify, lookup). -- Endpoints the CLI doesn't wrap yet → `plivo api ` (typed errors, profile resolution, dry-run — strictly better than curl). -- Voice streaming developer-loop (`voice streams test`, `voice streams forward`). -- Login + credential management. -- About to write a curl against `api.plivo.com` → stop, check `plivo --help` / `plivo api` first. - -## Other Plivo skills - -This file covers the CLI itself. Three product skills cover the work you do with it, each a separate install: - -- `plivo-audio-streaming` — connect a WebSocket voice bot to phone calls with ``, and debug one that fails. -- `plivo-sip-trunking` — connect LiveKit, ElevenLabs, Retell or Vapi to phone calls over SIP trunking. -- `plivo-voice-xml` — write the XML your answer URL returns: IVRs, call routing, recording, conferences. - -Install any of them with `npx skills add https://www.plivo.com/docs --skill `. - -## Installation — if `plivo` is not on PATH - -First check: -```bash -command -v plivo || echo "not installed" -``` - -If missing, pick whichever option fits the environment: - -```bash -# (1) install.sh — one-line installer (preferred). Verifies SHA256SUMS, then -# drops the binary in the first user-owned dir on PATH; no sudo. -# Override the target dir with PLIVO_INSTALL_DIR. Windows: install.ps1. -curl -fsSL https://raw.githubusercontent.com/plivo/plivo-cli/main/install.sh | bash - -# (2) Build from source -git clone https://github.com/plivo/plivo-cli.git ~/plivo/plivo-cli -cd ~/plivo/plivo-cli && go install . -# Binary lands at ~/go/bin/plivo-cli; symlink the canonical name: -ln -sf ~/go/bin/plivo-cli ~/go/bin/plivo - -# (3) GitHub release — direct download (assets are plivo__[.exe]) -PLATFORM="$(uname -s | tr '[:upper:]' '[:lower:]')_$(uname -m | sed 's/x86_64/amd64/;s/aarch64/arm64/')" -curl -fL -o /tmp/plivo "https://github.com/plivo/plivo-cli/releases/latest/download/plivo_${PLATFORM}" -chmod +x /tmp/plivo && mv /tmp/plivo ~/.local/bin/plivo -``` - -Verify: `plivo --version`. Then run `plivo login` to bootstrap credentials (see Authentication above). - -## Keeping the CLI up to date - -```bash -plivo upgrade --check # report only — is a newer release available? -plivo upgrade # install latest -plivo upgrade --version v0.2.0 # pin a specific release tag -plivo upgrade --force # reinstall even if already on latest -``` - -If installed via Homebrew, `plivo upgrade` refuses — use `brew upgrade plivo` instead. The CLI also auto-checks GitHub for newer releases once a day on success and prints a one-line nudge. **Set `PLIVO_NO_UPDATE_CHECK=1`** to suppress in CI / scripted use. The server may also return HTTP 426 to flag the build as below the supported minimum — surfaced as `code: CLI_TOO_OLD` (exit 6) with a recommendation to upgrade. - -This skill ships with each plivo-cli release; reinstall the CLI to update. - -## Notation in this file - -- `` = positional argument the user MUST provide. -- `[--flag]` = optional flag. -- `--flag ` = flag that takes a value. -- `(spend)` = costs money; refuses without `--yes` (exit 5, `code: DESTRUCTIVE_REFUSED`). - -## Universal flags (work on every command — persistent/global) - -| Flag | Type | Default | When to use | -|---|---|---|---| -| `--profile ` | string | active profile | invoke against a non-active profile for this call only | -| `-o, --output ` | `table\|json` | `table` on TTY, `json` when piped | force JSON for scripts | -| `--dry-run` | bool | false | (API-backed commands) print the HTTP request and exit 0 — preview without spending | -| `-y, --yes` | bool | false | confirm spend / destructive verbs (refused otherwise) | -| `-q, --quiet` | bool | false | suppress non-data output (banners, hints) | -| `--no-color` | bool | false | strip ANSI from output | -| `--log-level ` | `debug\|info\|warn\|error\|none` | `warn` | `debug` prints outbound URLs to stderr | -| `--timeout ` | int | 30 | per-request timeout | - -`--dry-run` applies to API-backed commands — it's a no-op for `login`, `ask`, `upgrade`, `voice streams test`, and similar non-REST flows. - -### `--explain` — narrate before executing (not universal) - -Unlike the flags above, `--explain` is a **local** flag registered on only these commands; anywhere else it's rejected with `unknown flag: --explain`: - -`plivo api`, `plivo account applications create`, `plivo auth whoami`, `plivo voice calls make`, `plivo messaging {sms,mms,whatsapp} send`, `plivo numbers buy`, `plivo numbers release`, `plivo verify sessions create`. +- **Auth.** Browser login (`plivo login`) is the only credential source: the CLI reads no `PLIVO_AUTH_ID` or `PLIVO_AUTH_TOKEN` and has no flag for them. Do not run `plivo login` yourself; if `plivo auth whoami` exits 2, ask the human to run it in their own terminal. Never print, echo, export or store an auth token, including one the user pastes. `--profile ` selects another saved profile. +- **Preview, then confirm.** Run any change with `--dry-run` first: it prints the request on stderr and exits 0 without sending a write. Spend and destructive commands then need `--yes`: run that as a separate command, only once the human has approved that spend or deletion. Other writes (creates, updates, live-call verbs such as `play`, `speak` and `transfer`) have no `--yes` gate and act as soon as they run. + - Spend commands (`calls make`, `messaging * send`, `numbers buy|cnam`, `masking sessions create`, 10DLC `brands|campaigns create`, `verify sessions create`, `multiparty participant add`, mutating `plivo api` verbs) preview with `--dry-run` alone. + - Destructive commands (`numbers release`, `calls hangup`, `conferences hangup`, `calls streams stop`, `powerpacks numbers remove`, and every `delete`, `kick` and `end`) refuse `--dry-run` alone with exit 5, although the hint suggests it. Preview them with `--yes --dry-run` (`--dry-run` wins). For `sip * delete`, run it without `--yes`: it names what it would detach, then refuses. + - `plivo lookup` is billed per lookup and has no `--yes` gate: ask first. + - `--dry-run` only holds back Plivo API writes. Commands that change local state or talk to something else ignore it and act for real: `login`, `logout`, `auth use`, `auth remove`, `config set`, `config telemetry`, `feedback` (submits), `upgrade` (installs; use `upgrade --check`) and `voice streams test` (connects to the WebSocket; no call). `skill install --dry-run` is the exception: it writes nothing. +- **Output.** Pass `-o json`. Reads and creates print `{"data": }` on stdout; list rows are at `data.objects`, paging at `data.meta`. Many writes (`numbers update|release`, `calls hangup|transfer`, `account applications update`, `multiparty participant add`) print nothing on stdout: check the exit code, read the result back with a `get`, and never retry a spend command because stdout was empty. +- **Errors** go to stderr as `{"error": {"code", "message", "hint", "retryable", "status_code"}}` with a non-zero exit. Switch on `code` (exit codes below), never on message text. +- **No prompts.** Never run bare `plivo feedback` or `plivo ask -i`, and pass `-y` to `voice streams forward`. `export PLIVO_FEEDBACK_PROMPT=0 CI=1 PLIVO_NO_UPDATE_CHECK=1` silences the rating prompt and the update hint. ## Top-level command map @@ -128,576 +31,77 @@ Unlike the flags above, `--explain` is a **local** flag registered on only these account applications | get | subaccounts | update agents list | get | create | update | publish | pause | resume | delete | nodes | runs api generic REST escape hatch (any api.plivo.com path) -config get | set | telemetry (CLI settings, incl. telemetry opt-out) -ask one-shot question to Plivo's AI assistant (SSE stream) +ask one-shot question to Plivo's AI assistant auth list | use | remove | whoami -docs list | search | show (read Plivo's docs in the shell) -feedback rate the CLI (interactive or one-shot) -login browser PKCE OAuth login -logout remove a profile + its keychain token -lookup carrier/format lookup for an E.164 number -messaging get | sms | mms | whatsapp (aliases: message, msg, sms) -numbers buy | cnam | compliance | get | list | masking | release | search | update (alias: number) -sip calls | trunks | acl (SIP Trunking; alias: sip-trunking) -skill install | list (manage the bundled agent skills) -support list past support escalations (filed via `plivo ask`) -upgrade self-update the binary +config get | set | telemetry +docs list | search | show +feedback rate the CLI +login browser login (PKCE) +logout remove a profile and its keychain token +lookup carrier lookup for an E.164 number +messaging get | sms | mms | whatsapp (aliases: message, msg, sms) +numbers buy | cnam | compliance | get | list | masking | release | search | update +sip calls | credentials | ip-acl | trunks | uris +skill install | list +support past support escalations +upgrade self-update verify sessions (create | get | list | validate) voice calls | conferences | endpoints | multiparty | recordings | streams ``` -Many groups have short aliases (e.g. `account application`/`app`, `voice call`, `voice conf`, `voice mpc`, `messaging sms powerpacks`/`pp`). `plivo --help` is always the source of truth. - -## Authentication - -### `plivo login` - -Browser PKCE OAuth — opens default browser, captures the callback over a local loopback listener, persists creds. This is the **only** way to authenticate: there is no flag and no environment variable that accepts credentials inline, so an agent/CI host needs a profile logged in beforehand (see Authentication above). - -If the browser can't reach that listener (container, WSL, VM, SSH), the human pastes the full URL from the browser's address bar into the terminal after approving; this needs an interactive terminal. - -One profile is saved per organization: with no `-n`, the profile name is derived from the org (slug of its name, e.g. `acme-inc`), falling back to `default` when the org has no name. Logging into a second org saves a second profile instead of overwriting the first; re-authorizing the *same* org updates its profile in place. - -| Flag | Type | Default | When | -|---|---|---|---| -| `-n, --name ` | string | derived from the org, or `default` | save under an explicit profile name instead of the org-derived one | -| `--no-verify` | bool | false | skip the post-login `GET /Account/` validation (offline / mock use) | - -Examples: -```bash -plivo login # profile named after the org (or "default") -plivo login --name staging # explicit profile name -``` - -After login: auth_id + email in `~/.plivo/config.toml`; auth_token in OS keychain (macOS Keychain / Windows Credential Manager / Linux Secret Service), with an inline `~/.plivo/config.toml` (chmod 0600) fallback when no keychain is available. - -### `plivo auth whoami` - -Verify creds + show the active account. - -### `plivo auth list` - -List all configured profiles (name, org when known, auth_id) + show which is active. - -### `plivo auth use ` - -Switch the active profile. Used after `plivo login --name X` to flip default. - -### `plivo auth remove ` - -Remove a non-active profile. (For the active profile use `plivo logout`.) - -### `plivo logout [name]` - -Delete a profile + best-effort remove its token from the keychain. With no arg → active profile. - -## Core invariants (read once) - -- **Output**: TTY → table, pipe → JSON. Force JSON anywhere with `-o json`. -- **Spend verbs require `--yes`** or refuse with exit 5 + `code: DESTRUCTIVE_REFUSED`. Verified list (commands that gate on `--yes`): `messaging {sms,mms,whatsapp} send`, `voice calls make`, `voice calls hangup`, `numbers buy`, `numbers cnam`, `numbers release`, `numbers masking sessions create`/`delete`, `messaging sms 10dlc brands create`, `messaging sms 10dlc campaigns create`, `messaging sms 10dlc links delete`, `messaging sms powerpacks delete`, `voice multiparty end`, `voice multiparty participant add`/`kick`, `voice conferences hangup`, `voice conferences member kick`, `verify sessions create`, `account applications delete`, `account subaccounts delete`, `voice endpoints delete`, `voice recordings delete`, `numbers compliance delete`, and mutating verbs of `plivo api` (POST/PUT/PATCH/DELETE). - - NOTE: live-call control verbs `voice calls play`, `speak`, `record`, `dtmf`, `transfer`, `stop-*` do **NOT** require `--yes` — they act on an already-established call. -- **Stable error envelope** on stderr: `{"error":{"code", "message", "hint", "retryable", "status_code", ...}}`. Switch on `code`, never message text. -- **Verify before inventing**: `plivo --help` is the source of truth. The CLI evolves; don't assume from memory. -- **`--dry-run`** previews the exact HTTP request without sending. Works on every API-backed command. -- **`--explain`** narrates the action in plain English before running — only on the commands listed under "Universal flags" above; everywhere else it's `unknown flag: --explain`. - -## JSON output envelopes - -All commands with `-o json` emit `{"data": }` on stdout and exit 0. Nothing is dropped or reshaped, so `data` matches the API docs exactly. - -For list commands that means the rows are nested, not at the top level: - -``` -{"data": {"api_id": "...", "meta": {"limit": 20, "offset": 0, ...}, "objects": [ {...} ]}} -``` - -So read `data.objects[]` for rows and `data.meta` for paging. Single-resource commands put the object straight at `data`. - -**Changed in v0.3.0:** `data` used to be the rows array itself with paging in a sibling `"meta"`, and it only carried the subset of fields the CLI had typed. If you were written against a v0.2.x CLI, `data[0]` is now `data.objects[0]`. - -Failures emit `{"error": {"code", "message", "hint", "retryable", "status_code", "request_id", "docs_url", "context"}}` on stderr with a non-zero exit — see the [error-envelope cheatsheet](#error-envelope-cheatsheet) below. - -## Scripted / non-interactive use - -```bash -export PLIVO_FEEDBACK_PROMPT=0 # silence the post-success "rate the CLI?" prompt -export CI=1 # gates TTY-only nudges -plivo voice calls list -o json | jq ... -``` - -The post-success auto-prompt is already TTY-gated so most scripted runs are fine, but `PLIVO_FEEDBACK_PROMPT=0` is the bulletproof escape hatch when the wrapper detects a pseudo-TTY. - -## Feedback - -- **Manual:** `plivo feedback` → interactive rating (1-5) + optional comment. -- **One-shot:** `plivo feedback --rating 4 --message "..."` (either field alone is fine). -- **Auto-prompt:** after a successful command on an interactive TTY, the CLI may ask once to rate it. TTY-gated; snoozed on decline. - -| Flag | Type | When | -|---|---|---| -| `--rating <1-5>` | int | non-interactive submit; combine with `--message` to skip prompts | -| `--message "..."` | string | comment (PII-scrubbed client-side and server-side) | -| `--no-context` | bool | don't attach CLI version / OS / arch metadata | -| `--yes` | bool | skip the pre-submit preview | +## Behaviour `--help` does not show -Env vars: -- `PLIVO_FEEDBACK_PROMPT=0` — silence the auto-prompt; manual `plivo feedback` still works. -- `PLIVO_FEEDBACK_TELEMETRY=0` — disable all submission (manual becomes a no-op). -- `PLIVO_FEEDBACK_ENDPOINT` — override the collector endpoint (when unset, the command surfaces a clear "not wired" message rather than dropping silently). +- `messaging` aliases replace the group name only: `plivo sms sms send` works, `plivo sms send` fails with `unknown flag`. +- Recipients: separate several with `<`, quoted (a bare `<` is a shell redirect): `--dst "+14155551111<+14155552222"`. Commas do not work. `numbers` commands take the number as digits without `+`. +- `messaging whatsapp send` sends free text, which WhatsApp allows only within 24 hours of the user's last message. To open a conversation, send a template with `plivo api POST /Message/` and a `template` body. +- `voice calls make`: omit `--machine-detection` to turn detection off; the `none` that help lists is rejected by the API. +- `voice calls streams start`: quote `--content-type "audio/x-mulaw;rate=8000"` (`;` ends a shell command). `--stream-status-callback` sends `stream_status_callback_url`, while the API documents `status_callback_url`: when the callback matters, start the stream with `plivo api POST /Call//Stream/`. +- `voice multiparty create` is retired and hidden: an MPC starts when its first participant is added, with `voice multiparty participant add --from --to `. +- `voice streams forward` points the app's answer URL, and so every number on that app, at a tunnel while it runs, then restores it on exit. Use a dedicated test app. Preview with `--dry-run` (it shows the URL it would replace) and get approval. Then run it in the background with `-y -o table` and wait for its `Ready` line; in JSON mode it prints nothing, not even the tunnel URL, until it exits. Stop it with SIGINT or SIGTERM: SIGKILL skips the restore. It does not notice a dropped tunnel, so restart it. +- `account applications update` has no `--fallback-answer-url`: set it with `plivo api POST /Application//`. `account applications delete` also deletes the app's endpoints, with or without `--cascade`: the API cascades by default. +- `sip`: quote `--uri "host;transport=tcp"` (the help example does not). Passwords go only through `--password-stdin`; to preview a password change, pass `--username` too. Deleting a URI deletes the trunks that use it. Route a number to an inbound trunk with `numbers update --trunk-id `. +- `plivo api` reports every HTTP error as `UPSTREAM_ERROR` (exit 3) with the real status in `status_code`: switch on that. Its `--dry-run`, `--explain` and `--log-level debug` print the request body unredacted, so never send a secret through it. `/Message/` expands to `/v1/Account//Message/`; a `/v1/...` path is used as-is. +- `plivo ask` and the `diagnose` commands share a small per-account rate limit: on `RATE_LIMITED`, wait as long as the message says. +- `docs show` prints a JSON envelope when piped: add `-o table`, or use `jq -r .data.body`. `docs search` rows are at `data`, not `data.objects`. A page that ends inside a code block was cut off: read `https://www.plivo.com/docs/.md`, which returns clean Markdown (bare `plivo.com` only redirects there). -## Generic REST escape hatch — `plivo api` - -For any endpoint the CLI doesn't yet wrap. Profile resolution, `--dry-run`, structured error envelopes, and the same exit codes as the rest of the CLI. - -```bash -plivo api GET /Account/ # account-scoped path → /v1/Account//... -plivo api GET /Message/ --query "limit=10" -plivo api POST /Message/ --body @msg.json --yes # mutating verbs require --yes -cat msg.json | plivo api --method POST /Message/ --body @- --yes -plivo api GET /Application/ --header "X-Debug: 1" -``` +## Known issues -| Flag | When | -|---|---| -| `--method ` | HTTP method (alternative to the positional arg; useful when piping) | -| `--body ` | request body: literal JSON, `@path`, or `@-` for stdin | -| `--query ` | query param (repeatable) | -| `--header "K: V"` | extra header (repeatable; overrides defaults) | +- In v1.1.0 to v1.1.2, `voice streams forward` cannot carry a live call: it rejects Plivo's stream (it checks the signature against the `wss://` URL while Plivo signs the `http://` one), and its XML has no `keepCallAlive`, so the call ends at once with hangup cause 4010 (End Of XML Instructions). Fixed on main for the next release; check `plivo --version`. -Paths: absolute (`/v1/Account/MA…/Message/`) used as-is; account-scoped (`/Message/`) expanded to `/v1/Account//...`. GET/HEAD pass through; POST/PUT/PATCH/DELETE require `--yes`. +## Workflows -## Common workflows +Provision a number and attach an app: -**Provision a phone number end-to-end** ```bash plivo numbers search --country US --type local --limit 5 -o json -plivo numbers buy +1415... --dry-run # preview spend -plivo numbers buy +1415... --yes # rent it -plivo account applications create --app-name "my-app" --answer-url https://my.app/answer -o json -plivo numbers update +1415... --app-id # attach the app +plivo numbers buy 14155550100 --dry-run # show the human, then rerun with --yes once approved +plivo account applications create --app-name my-app --answer-url https://example.com/answer -o json +plivo numbers update 14155550100 --app-id +plivo numbers get 14155550100 -o json | jq '.data.application' # read it back ``` -**Test an inbound webhook locally** -```bash -plivo voice streams test --to ws://localhost:7860/ws --duration 5 --bidirectional -# If the bot replies correctly, bridge a real call: -plivo voice streams forward --number +1415... --app --to ws://localhost:7860/ws -``` +Debug a failed call: -**Send your first SMS** ```bash -plivo numbers list --type local -o json | jq '.data.objects[].number' # pick a src -plivo messaging sms send --src +1415... --dst +1415... --text "hi" --dry-run -plivo messaging sms send --src +1415... --dst +1415... --text "hi" --yes +plivo voice calls get -o json | jq '.data | {hangup_cause_name, hangup_cause_code, hangup_source}' +plivo voice calls diagnose # AI walk-through of the call +plivo docs show voice/troubleshooting/hangup-causes -o table # what a hangup code means +plivo docs show sip-trunking/troubleshooting/zentrunk-hangup-codes -o table # the same, for SIP trunk calls ``` -**Debug a failed call** -```bash -plivo voice calls get -o json | jq '.data | {state, hangup_cause, end_time}' -plivo voice calls diagnose # AI lifecycle walk-through -plivo ask "why did call fail?" --call-uuid -``` - -**Switch between accounts/profiles** -```bash -plivo auth list # see all profiles, marked active -plivo auth use staging # flip default -plivo numbers list --profile prod # one-shot override -``` - -## Numbers - -### `plivo numbers list` - -List rented numbers on the account. - -| Flag | Type | When | -|---|---|---| -| `--type ` | string | filter by number type | -| `--starts-with ` | string | filter by E.164 prefix (e.g. `+1`) | -| `--alias ` | string | filter by alias | -| `--services ` | string | filter by enabled services (comma-combine) | -| `--subaccount ` | string | filter by subaccount | -| `--limit ` | int | page size (default 20, max 20) | -| `--offset ` | int | pagination offset | - -### `plivo numbers get ` - -Get one rented number. - -### `plivo numbers search` - -Search marketplace for buyable numbers. - -| Flag | Type | When | -|---|---|---| -| `--country ` | string | **required**; e.g. `US`, `IN`, `GB` | -| `--type ` | string | filter | -| `--pattern ` | string | digit pattern in the number | -| `--region ` | string | region filter | -| `--limit ` | int | default 20 | -| `--offset ` | int | pagination offset | - -### `plivo numbers buy ` (spend) - -Rent a number from the marketplace. **Requires `--yes`**. - -| Flag | Type | When | -|---|---|---| -| `--app-id ` | string | auto-attach to this application after purchase | - -(`--yes` / `--dry-run` are the universal spend flags.) - -### `plivo numbers update ` - -Update metadata on a rented number. - -| Flag | When | -|---|---| -| `--alias "..."` | set alias | -| `--app-id ` | associate an Application | -| `--subaccount ` | move under a subaccount | - -### `plivo numbers release ` (spend) - -Release a rented number (stops monthly billing). **Requires `--yes`**. - -### `plivo numbers cnam ` (spend) - -Caller-ID Name (CNAM) lookup for a US/CA number. **Requires `--yes`** (it costs money). - -### `plivo numbers compliance ...` - -Phone-number regulatory compliance. Sub-verbs: `requirements`, `create` (multipart, auto-submits), `get`, `list`, `update` (multipart, auto-resubmits a rejected app), `delete` (`--yes`), `link` (bulk-link numbers to accepted applications). Use `plivo numbers compliance --help` for the full surface. - -### `plivo numbers masking sessions ...` - -Phone-number masking session lifecycle: `create` (spend, `--yes`), `get`, `list`, `delete` (`--yes`). (Alias: `numbers mask`.) - -## Messaging - -Channel-split CLI: SMS / WhatsApp / MMS each have their own subgroup. Universal `plivo messaging get ` works across channels. The `messaging` group aliases to `message`, `msg`, and `sms`, so `plivo sms sms send ...` == `plivo messaging sms send ...`. The alias replaces only the group name; `plivo sms send` is not a command. +Use the table for the call's product: the two code spaces reuse numbers, so 4010 is End Of XML Instructions on Voice but `unauthorized_by_carrier` on Zentrunk. -### `plivo messaging sms send` (spend) +Pick a sending number: `plivo numbers list --services sms -o json | jq -r '.data.objects[].number'`. Numbers are listed without a leading `+`. -Send an SMS. **Requires `--yes`**. +## Exit codes -| Flag | Type | When | -|---|---|---| -| `--src ` | string | **required**; sender (E.164, shortcode, or sender ID) | -| `--dst ` | string | **required**; recipient. Multiple: separate with `<`, **quoted**: `--dst "+14155551111<+14155552222"` | -| `--text "..."` | string | **required**; message body | -| `--url ` | string | delivery-status callback URL | -| `--method ` | string | callback method (default POST) | +`1` user, flag, validation, not-found, conflict or account-policy error (`USER_ERROR`, `BAD_FLAG`, `BAD_INPUT`, `VALIDATION_ERROR`, `RESOURCE_NOT_FOUND`, `RESOURCE_CONFLICT`, `GEO_PERMISSION_DENIED`, `OUTBOUND_DISABLED`, `INSUFFICIENT_FUNDS`); typed commands also report a network failure as `USER_ERROR`, with a message starting `http:`. `2` auth (`AUTH_*`). `3` network or upstream, retryable (`NETWORK_ERROR`, `UPSTREAM_*`, `INTERNAL_ERROR`). `4` `RATE_LIMITED`: back off. `5` `DESTRUCTIVE_REFUSED`: needs `--yes`. `6` `CLI_TOO_OLD`: the human should upgrade (`plivo upgrade`, or `brew upgrade plivo`). -(`messaging mms send` and `messaging whatsapp send` take the **same five flags** — there are no extra `--type`, `--powerpack`, `--trackable`, `--log`, `--urls`, `--template` flags on these commands in this version.) - -### `plivo messaging sms list` - -List sent/received SMS. - -| Flag | When | -|---|---| -| `--state ` | filter by delivery state | -| `--direction ` | filter by direction | -| `--from ` / `--to ` | filter by leg (from_number / to_number) | -| `--limit ` / `--offset ` | pagination | - -### `plivo messaging get ` - -Get one message (any channel). Universal across SMS/WhatsApp/MMS. - -### `plivo messaging sms diagnose ` - -AI-powered: walks a message lifecycle and explains failures in plain English. (`messaging mms diagnose` / `messaging whatsapp diagnose` exist too.) - -### `plivo messaging sms 10dlc ...` - -US A2P 10DLC registration. Subgroups: `brands` (`create` spend, `get`, `list`, `update`), `campaigns` (`create` spend, `get`, `list`, `update`), `links` (`create`, `list`, `delete` `--yes`). - -### `plivo messaging sms powerpacks ...` - -Powerpack (number-pool) CRUD: `create`, `get`, `list`, `update`, `delete` (`--yes`), `numbers` (manage numbers inside a powerpack). (Alias: `pp`.) - -### `plivo messaging sms tollfree ...` - -Toll-free verification (US TFN compliance): `list`, `get`, `submit`. (Alias: `tfv`.) - -### `plivo messaging whatsapp send` / `plivo messaging mms send` (spend) - -Same five flags as `messaging sms send` (`--src`, `--dst`, `--text`, `--url`, `--method`), including the `--dst "+1...<+1..."` multi-recipient form. Each also has `list` and `diagnose`. - -## Voice — calls - -### `plivo voice calls make` (spend) - -Place an outbound call. **Requires `--yes`**. - -| Flag | Type | When | -|---|---|---| -| `--from ` | string | **required**; caller (must be on your account) | -| `--to ` | string | **required**; recipient | -| `--answer-url ` | string | PlivoXML URL on answer (defaults to Plivo's hello demo) | -| `--answer-method ` | string | default **GET** | -| `--hangup-url ` | string | webhook on hangup | -| `--ring-url ` | string | webhook on ring | -| `--machine-detection ` | string | answering-machine handling | - -### `plivo voice calls list` / `get ` - -List/get calls. List filter flags: `--direction `, `--from` (from_number), `--to` (to_number), `--limit`, `--offset`. - -### `plivo voice calls hangup ` - -End an in-progress call. **Requires `--yes`**. - -### `plivo voice calls transfer ` - -Transfer one or both legs of a live call to new PlivoXML URLs. - -| Flag | When | -|---|---| -| `--legs ` | which leg(s) to transfer (default aleg) | -| `--aleg-url ` / `--bleg-url ` | new PlivoXML URL per leg | -| `--aleg-method` / `--bleg-method` | GET\|POST (default POST) | - -### `plivo voice calls play ` / `stop-play ` - -Stream audio into a live call. (No `--yes` required.) - -| Flag | When | -|---|---| -| `--urls ` | **required**; comma-separated audio URL(s) | -| `--length ` | stop after N seconds (0 = full file) | -| `--legs ` | which leg (default aleg) | -| `--loop` | bool; replay until hangup | -| `--mix` | bool; mix with call audio vs replace (default true) | - -### `plivo voice calls speak ` / `stop-speak ` - -TTS into a live call. (No `--yes` required.) - -| Flag | When | -|---|---| -| `--text "..."` | **required**; text to speak | -| `--voice ` | TTS voice (default WOMAN) | -| `--language ` | e.g. `en-US`, `en-GB`, `hi-IN` (default en-US) | -| `--legs ` | which leg (default aleg) | -| `--mix` | mix vs replace (default true) | - -### `plivo voice calls dtmf ` - -Send DTMF digits into a live call. - -| Flag | When | -|---|---| -| `--digits <0-9*#>` | **required**; e.g. `1234#` | -| `--leg ` | which leg (default aleg) | - -### `plivo voice calls record ` / `stop-record ` - -Record a live call. (No `--yes` required.) - -| Flag | When | -|---|---| -| `--time-limit ` | max recording length (default 60) | -| `--file-format ` | audio container (default mp3) | -| `--both-legs` | record both legs (default: A-leg only) | -| `--transcribe` | request transcription | -| `--callback-url ` | URL hit when recording finishes | -| `--callback-method ` | default POST | - -### `plivo voice calls diagnose ` - -AI-powered lifecycle walkthrough + plain-English failure explanation. - -### `plivo voice calls streams ` — per-call AudioStream CRUD - -Distinct from `voice streams` (the dev-loop group). Sub-verbs: `list `, `get `, `start `, `stop []`. - -`start` flags: `--url ` (**required**), `--audio-track ` (default inbound), `--bidirectional`, `--content-type` (default `audio/x-l16;rate=16000`), `--stream-status-callback ` (alias `--callback-url`), `--extra-headers "k1=v1,k2=v2"`, `--service-type`. - -## Voice — streaming dev loop - -Use these for local development of WebSocket-based audio streaming **without** a real call. Distinct from `voice calls streams` (REST CRUD on an existing call's stream). - -### `plivo voice streams test` - -Open a WebSocket to a URL, send Plivo-format start/media/stop frames with synthetic audio, report results. No call placed, no spend. - -| Flag | Type | Default | When | -|---|---|---|---| -| `--to ` | string | **required** | the WebSocket endpoint to test (ws:// or wss://) | -| `--duration ` | int | 3 | seconds of synthetic audio (max 30) | -| `--codec ` | string | `mulaw` | audio codec advertised | -| `--rate ` | int | 8000 | sample rate (8000 for mulaw, 16000 typical for l16) | -| `--bidirectional` | bool | false | also read frames back (test bot→caller path) | -| `--insecure` | bool | false | skip TLS verification (self-signed dev certs) | - -```bash -plivo voice streams test --to wss://my-bot.example.com/ws -plivo voice streams test --to ws://localhost:7860/ws --duration 5 --bidirectional -``` - -### `plivo voice streams forward` - -Temporarily redirect an app's `answer_url` to a local tunnel so a real call's audio bridges into your local WebSocket handler. Restores the original `answer_url` on Ctrl+C. - -| Flag | Type | Default | When | -|---|---|---|---| -| `--number ` | string | **required** | E.164 number attached to the app | -| `--app ` | string | **required** | Application UUID whose answer_url gets temporarily redirected | -| `--to ` | string | **required** | your local WebSocket to forward audio to | -| `-y, --yes` | bool | false | skip the confirmation prompt | -| `--keep` | bool | false | DON'T restore `answer_url` on exit (advanced) | -| `--codec ` | string | `mulaw` | codec advertised to Plivo | -| `--rate ` | int | 8000 | sample rate | -| `--bidirectional` | bool | true | allow bot to write audio back to caller | -| `--print-payload` | bool | false | dump full webhook bodies (verbose) | - -Requires ngrok in PATH or at `~/.plivo/bin/ngrok`. Saves the app's current `answer_url`, starts an ngrok tunnel + local HTTP/WS server, points the app at the tunnel, bridges incoming call audio. Restores `answer_url` on SIGINT unless `--keep`. - -## Voice — conferences / multiparty / endpoints / recordings - -```bash -plivo voice conferences list | get | hangup | record | stop-record | member ... -plivo voice multiparty list | get | end | participant ... -plivo voice endpoints list | get | create | update | delete -plivo voice recordings list | get | delete -``` - -- `voice conferences member`: `mute`/`unmute`, `deaf`/`undeaf`, `kick` (`--yes`), `play`/`stop-play` (`--urls` required), `speak`/`stop-speak` (`--text` required). -- `voice multiparty participant add ` requires `--from` + `--to` (spend, `--yes`); optional `--role `. Given a name, it starts the MPC if none by that name is ongoing; there is no `voice multiparty create`. Also: `list`, `mute`/`unmute`, `hold`/`unhold`, `kick` (`--yes`). -- `voice multiparty end` and `voice conferences hangup` require `--yes`. -- `voice endpoints` / `voice recordings` `delete` require `--yes`. - -Run `plivo voice --help` for the rest. - -## Account + applications - -### `plivo account get` / `plivo account update` - -Get/update account info. - -### `plivo account subaccounts` - -Subaccount CRUD: `list`, `get`, `create`, `update`, `delete` (`--yes`). - -### `plivo account applications` - -| Verb | Required flags | Notes | -|---|---|---| -| `list` | — | `--limit`, `--offset` for pagination | -| `get ` | — | | -| `create` | `--app-name`, `--answer-url` | optional: `--answer-method`, `--hangup-url`, `--message-url`, `--fallback-answer-url`, `--default-number-app`, `--log-incoming-messages` (default true) | -| `update ` | — | same flags as create; only supplied ones get patched | -| `delete ` | `--yes` | spend/destructive verb; refuses without confirmation | - -(Aliases: `account application`, `account app`.) - -## Verify - -```bash -plivo verify sessions create --app-uuid --recipient +1... --channel sms # spend, --yes -plivo verify sessions get -plivo verify sessions list -plivo verify sessions validate --otp 123456 -``` - -`create` flags: `--app-uuid` (**required**), `--recipient` (**required**), `--channel ` (default sms), plus optional `--locale`, `--alpha-sender`, `--url`, `--method`. `validate` requires `--otp`. - -## Lookup - -```bash -plivo lookup # carrier + line-type (lookup.plivo.com); --type defaults to carrier -``` - -## Conversational / debug - -### `plivo ask ""` - -Ask Plivo's AI assistant — streams the answer via SSE. **One-shot only**: each invocation is a single message with no prior conversation history; there is no interactive mode and no history flag in this version. Long flows (voice-debug can run 2-5 minutes) have no overall HTTP timeout; Ctrl-C cancels (exit 130, no auto-retry). - -| Flag | When | -|---|---| -| `--call-uuid ` | include a call's context so the assistant can debug it specifically | -| `--verbose` | show the assistant's tool_call / tool_output events on stderr | -| `--debug-stream` | dump raw SSE frames to stderr (debugging this CLI) | - -With `-o json`, each SSE event is emitted as one JSONL line (handy for scripts/agents). - -```bash -plivo ask "What does Plivo SMS error code 30007 mean?" -plivo ask --call-uuid 21e68d29-... "Debug what happened on this call" -plivo ask -o json "What's the rate for outbound voice to Brazil?" -``` - -### `plivo support` - -List your past support escalations (the ones filed via `plivo ask`). Read-only; `-o json` supported. (This is NOT an interactive chat — use `plivo ask` for that.) - -### `plivo upgrade` - -Self-update the CLI binary (see "Keeping the CLI up to date"). - -### `plivo agents` - -AI agent flows: node-graph voice/chat/message agents. Aliases to `agent`. - -| Command | What it does | -| --- | --- | -| `agents list` / `get ` | list flows; fetch one flow's full definition (fields, nodes, connections) | -| `agents create` / `update` | create or edit a flow | -| `agents publish` / `pause` / `resume` | move a flow between DRAFT and ACTIVE, or stop it handling traffic | -| `agents delete` | delete a flow (**requires `--yes`**) | -| `agents nodes list` / `get ` | browse the node catalogue available to a graph | -| `agents runs list` / `get ` | inspect executions of a flow | - -```bash -plivo agents list -o json | jq '.data.objects[] | {agent_id, name, status}' -plivo agents nodes list -plivo agents runs list -``` - -## Error-envelope cheatsheet - -Switch on `code` (string), never message text. `code` → exit-code mapping (stable): - -| Code | Exit | Likely cause | -|---|---|---| -| `AUTH_MISSING` | 2 | no creds — run `plivo login` | -| `AUTH_INVALID` | 2 | wrong auth_id/token — re-login | -| `AUTH_FORBIDDEN` | 2 | authenticated but not permitted | -| `AUTH_EXPIRED` | 2 | session/token expired — re-login | -| `AUTH_2FA_REQUIRED` / `AUTH_RECAPTCHA_REQUIRED` | 2 | interactive auth challenge required | -| `DESTRUCTIVE_REFUSED` | 5 | spend/destructive verb without `--yes` | -| `RATE_LIMITED` | 4 | back off + retry (`retryable: true`) | -| `CLI_TOO_OLD` | 6 | server returned 426 — run `plivo upgrade` | -| `NETWORK_ERROR` | 3 | DNS / connection / TLS (`retryable: true`) | -| `UPSTREAM_TIMEOUT` / `UPSTREAM_UNAVAILABLE` / `UPSTREAM_ERROR` / `INTERNAL_ERROR` | 3 | transient upstream failure | -| `BAD_FLAG` / `BAD_INPUT` / `VALIDATION_ERROR` / `USER_ERROR` | 1 | client-side flag / shape / validation problem | -| `RESOURCE_NOT_FOUND` | 1 | 404 from upstream | -| `RESOURCE_CONFLICT` | 1 | 409 / state conflict | -| `GEO_PERMISSION_DENIED` / `OUTBOUND_DISABLED` / `INSUFFICIENT_FUNDS` | 1 | account capability / policy gate | - -All envelopes carry `hint` + `retryable`. Unknown/unmapped codes exit 1. - -## JSON consumption patterns - -```bash -# One field -plivo voice calls get -o json | jq '.data.duration' - -# Filter -plivo numbers list -o json | jq '.data.objects[] | select(.type=="local")' - -# Pipe across calls -APP_ID=$(plivo account applications list -o json | jq -r '.data.objects[0].app_id') -plivo numbers update +1... --app-id "$APP_ID" -o json -``` +## Other Plivo skills -## Sanity check +Each is bundled in the binary and installed separately, with no network: `plivo skill install first-agent | audio-streaming | sip-trunking | voice-xml`. -```bash -plivo --help && plivo auth whoami -``` +- `plivo-first-agent`: take a new user to a first AI voice agent on a real call. +- `plivo-audio-streaming`: connect a WebSocket voice bot to calls with ``. +- `plivo-sip-trunking`: connect LiveKit, ElevenLabs, Retell, Vapi or another SIP platform over SIP trunking. +- `plivo-voice-xml`: write and fix the XML an answer URL returns. diff --git a/cmd/skill.go b/cmd/skill.go index d42e750..5a2433e 100644 --- a/cmd/skill.go +++ b/cmd/skill.go @@ -9,6 +9,7 @@ import ( audiostreamingskill "github.com/plivo/plivo-cli/audio-streaming-skill" cliskill "github.com/plivo/plivo-cli/cli-skill" + firstagentskill "github.com/plivo/plivo-cli/first-agent-skill" "github.com/plivo/plivo-cli/internal/clierr" "github.com/plivo/plivo-cli/internal/output" siptrunkingskill "github.com/plivo/plivo-cli/sip-trunking-skill" @@ -35,6 +36,7 @@ var bundledSkills = []bundledSkill{ content: cliskill.SkillMD, summary: "the CLI reference — use `plivo` instead of raw curl", }, + {selector: "first-agent", dirName: "plivo-first-agent", content: firstagentskill.SkillMD, summary: "take a new user to a first AI voice agent on a real call"}, {selector: "audio-streaming", dirName: "plivo-audio-streaming", content: audiostreamingskill.SkillMD, summary: "connect a WebSocket voice bot to calls with "}, {selector: "sip-trunking", dirName: "plivo-sip-trunking", content: siptrunkingskill.SkillMD, summary: "connect an AI voice platform over SIP trunking"}, {selector: "voice-xml", dirName: "plivo-voice-xml", content: voicexmlskill.SkillMD, summary: "write and fix Plivo Voice XML"}, @@ -85,7 +87,7 @@ var skillCmd = &cobra.Command{ // skillInstallCmd writes the embedded SKILL.md into the agent skills directory // (default ~/.claude/skills/plivo-cli; override with --dir, or --print to stdout). var skillInstallCmd = &cobra.Command{ - Use: "install [cli|audio-streaming|sip-trunking|voice-xml|all]", + Use: "install [cli|first-agent|audio-streaming|sip-trunking|voice-xml|all]", Short: "Install an agent skill so coding agents auto-load the reference", Long: `Install a Plivo agent skill. @@ -93,6 +95,7 @@ A skill is a single-file reference (SKILL.md) written for LLM coding agents. They are bundled in the binary, so this writes them out without a network call. cli the CLI reference — use ` + "`plivo`" + ` instead of raw curl + first-agent take a new user to a first AI voice agent on a real call audio-streaming connect a WebSocket voice bot to calls with sip-trunking connect an AI voice platform over SIP trunking voice-xml write and fix Plivo Voice XML @@ -103,11 +106,12 @@ Each skill lands at ~/.claude/skills//SKILL.md by default. Use --dir to target another agent's skills directory, or --print to write the content to stdout so any other tool can capture it; both act on a single skill.`, Example: ` plivo skill install # CLI skill -> ~/.claude/skills/plivo-cli/ + plivo skill install first-agent # -> ~/.claude/skills/plivo-first-agent/ plivo skill install voice-xml # -> ~/.claude/skills/plivo-voice-xml/ plivo skill install all # every listed skill plivo skill install all --dry-run # show destinations, write nothing`, Args: cobra.MaximumNArgs(1), - ValidArgs: []string{"cli", "audio-streaming", "sip-trunking", "voice-xml", "all"}, + ValidArgs: []string{"cli", "first-agent", "audio-streaming", "sip-trunking", "voice-xml", "all"}, RunE: runSkillInstall, } diff --git a/cmd/skill_frontmatter_test.go b/cmd/skill_frontmatter_test.go new file mode 100644 index 0000000..f1f495d --- /dev/null +++ b/cmd/skill_frontmatter_test.go @@ -0,0 +1,58 @@ +package cmd + +import ( + "regexp" + "strings" + "testing" + "unicode/utf8" + + "gopkg.in/yaml.v3" +) + +var ( + skillNameRe = regexp.MustCompile(`^[a-z0-9-]{1,64}$`) + xmlTagRe = regexp.MustCompile(`<[A-Za-z/][^>]*>`) +) + +// Loaders with a strict YAML parser, such as the `skills` npm CLI, skip a skill +// whose frontmatter does not parse. An unquoted ": " inside the description is +// enough to do that, and Claude Code's tolerant loader hides it. +func TestBundledSkills_frontmatterFollowsAgentSkillsSpec(t *testing.T) { + for _, s := range bundledSkills { + t.Run(s.selector, func(t *testing.T) { + content := strings.ReplaceAll(s.content, "\r\n", "\n") + rest, ok := strings.CutPrefix(content, "---\n") + if !ok { + t.Fatal("SKILL.md does not open with a frontmatter block") + } + block, _, ok := strings.Cut(rest, "\n---\n") + if !ok { + t.Fatal("frontmatter block is not closed") + } + + var fm struct { + Name string `yaml:"name"` + Description string `yaml:"description"` + } + if err := yaml.Unmarshal([]byte(block), &fm); err != nil { + t.Fatalf("frontmatter is not valid YAML: %v", err) + } + + if fm.Name != s.dirName { + t.Errorf("name %q does not match the install directory %q", fm.Name, s.dirName) + } + if !skillNameRe.MatchString(fm.Name) { + t.Errorf("name %q must be 1-64 lowercase letters, digits or hyphens", fm.Name) + } + if fm.Description == "" { + t.Error("description is empty") + } + if n := utf8.RuneCountInString(fm.Description); n > 1024 { + t.Errorf("description is %d characters; the limit is 1024", n) + } + if tag := xmlTagRe.FindString(fm.Description); tag != "" { + t.Errorf("description contains the XML tag %q", tag) + } + }) + } +} diff --git a/cmd/testdata/help/plivo_skill_install.txt b/cmd/testdata/help/plivo_skill_install.txt index 7cb61b3..777c7b8 100644 --- a/cmd/testdata/help/plivo_skill_install.txt +++ b/cmd/testdata/help/plivo_skill_install.txt @@ -4,6 +4,7 @@ A skill is a single-file reference (SKILL.md) written for LLM coding agents. They are bundled in the binary, so this writes them out without a network call. cli the CLI reference — use `plivo` instead of raw curl + first-agent take a new user to a first AI voice agent on a real call audio-streaming connect a WebSocket voice bot to calls with sip-trunking connect an AI voice platform over SIP trunking voice-xml write and fix Plivo Voice XML @@ -15,10 +16,11 @@ target another agent's skills directory, or --print to write the content to stdout so any other tool can capture it; both act on a single skill. Usage: - plivo skill install [cli|audio-streaming|sip-trunking|voice-xml|all] [flags] + plivo skill install [cli|first-agent|audio-streaming|sip-trunking|voice-xml|all] [flags] Examples: plivo skill install # CLI skill -> ~/.claude/skills/plivo-cli/ + plivo skill install first-agent # -> ~/.claude/skills/plivo-first-agent/ plivo skill install voice-xml # -> ~/.claude/skills/plivo-voice-xml/ plivo skill install all # every listed skill plivo skill install all --dry-run # show destinations, write nothing diff --git a/docs/COMMANDS.md b/docs/COMMANDS.md index 75aa654..7cba78d 100644 --- a/docs/COMMANDS.md +++ b/docs/COMMANDS.md @@ -3085,6 +3085,7 @@ A skill is a single-file reference (SKILL.md) written for LLM coding agents. They are bundled in the binary, so this writes them out without a network call. cli the CLI reference — use `plivo` instead of raw curl + first-agent take a new user to a first AI voice agent on a real call audio-streaming connect a WebSocket voice bot to calls with sip-trunking connect an AI voice platform over SIP trunking voice-xml write and fix Plivo Voice XML @@ -3096,13 +3097,14 @@ target another agent's skills directory, or --print to write the content to stdout so any other tool can capture it; both act on a single skill. ``` -plivo skill install [cli|audio-streaming|sip-trunking|voice-xml|all] [flags] +plivo skill install [cli|first-agent|audio-streaming|sip-trunking|voice-xml|all] [flags] ``` Examples: ``` plivo skill install # CLI skill -> ~/.claude/skills/plivo-cli/ + plivo skill install first-agent # -> ~/.claude/skills/plivo-first-agent/ plivo skill install voice-xml # -> ~/.claude/skills/plivo-voice-xml/ plivo skill install all # every listed skill plivo skill install all --dry-run # show destinations, write nothing diff --git a/first-agent-skill/SKILL.md b/first-agent-skill/SKILL.md new file mode 100644 index 0000000..beaac36 --- /dev/null +++ b/first-agent-skill/SKILL.md @@ -0,0 +1,386 @@ +--- +name: plivo-first-agent +description: "Guides a new user from nothing to a first Plivo AI voice agent on a real call, covering CLI login, a number, an application, an echo bot, then an OpenAI bot over a tunnel. Use when someone wants to build or try a first Plivo voice agent or bot, or set up Plivo end to end. Not for an existing bot or production (plivo-audio-streaming) or SIP platforms (plivo-sip-trunking)." +license: Apache-2.0 +--- + +# Plivo: your first voice agent + +You take the user from nothing to a live call with an AI voice agent. The test call reaches a Plivo number (the user calls it, or Plivo calls the user), and Plivo streams the audio to a bot on the user's machine through a tunnel. The run has two bot stages: + +1. **Echo bot.** It needs no API key. The caller hears their own voice. This proves the number, the application, the tunnel and the audio in both directions. +2. **OpenAI bot,** from the Pipecat example that the Plivo docs use. It needs the user's own AI keys. + +Use the `plivo` CLI for every Plivo step and read values with `-o json`. `plivo --help` decides which commands and flags exist. Where this file describes behavior that differs from the help text, it describes tested behavior: follow this file. + +## Done means all of this, with evidence + +1. Application `my-first-agent` exists, and the number is attached to it (a number the user already had: only if they call in). +2. **Echo call:** a real call reached the echo bot, and the user confirms they heard their own voice. +3. **OpenAI call:** a real call reached the OpenAI bot, and the user confirms they had a conversation. Skip this item only if the user has no AI key, and say so. +4. **Call records:** for each call, `plivo voice calls get -o json` shows `call_duration` of 10 seconds or more and a `hangup_cause_name` of `Normal Hangup` or `End Of XML Instructions`. +5. **Resting state:** at the end, the application's answer URL and method are the resting values: the values recorded in step 5, or the deployed URL from step 8. The number is where the user chose to leave it (step 9). +6. **Report to the user:** + - the number and the application id; + - what runs on their machine, or where it is deployed; + - how to start it again; + - what each paid step cost; + - how to deploy it, if they did not. + +Do not report "done" for an item that you did not see in a command output or hear confirmed by the user. + +## Track the run in your task list + +At the start, create these nine tasks in your agent's own task or todo tool, and mark each one done only when its check passes. Each agent names the tool differently: for example TaskCreate and TaskUpdate in Claude Code (TodoWrite in older versions), and the plan tool in Codex CLI. Use the one your agent has. + +1. Check tools and login +2. Ask the setup questions +3. Test the echo bot (no phone) +4. Get a number +5. Create or reuse my-first-agent +6. Echo call +7. OpenAI bot and call +8. Offer a deploy +9. Leave a safe resting state + +If your agent has no task tool, print this list, and print it again with ticks after each task. In Claude Code the task tools can be off by default; if they are missing, tell the user that starting Claude Code with `CLAUDE_CODE_ENABLE_TODO_TOOLS=1` turns them on, and do not change their settings yourself. + +## Ask with your question tool + +Ask every question with your agent's own structured question tool, if it has one: the setup questions in one call, and one yes/no question for each step that costs money or changes the account, after you show its preview. Each agent names the tool differently. In Claude Code it is AskUserQuestion: one call takes 1 to 4 questions, each with 2 to 4 options, and it adds its own free-text "Other" option, so do not add one. If your agent has no such tool, ask the same questions as a numbered list with an "Other" choice, and wait for the answer. + +Setup questions (task 2). Before you ask, look for a previous run: do the step 5 lookup and list the numbers on `my-first-agent` as step 5 does. If a number is on it, add "Use + from the last run" as the first Number option and mark it recommended in place of "Rent a new number", so a second run does not rent a second number. + +| Header | Question | Options | +|---|---|---| +| Country | Which country should the number be in? | US (recommended) · Canada · India | +| Number | Do you want to rent a new number? | Rent a new number (recommended) · Use a number I have | +| AI keys | Which AI keys do you have for the second bot? | None yet (echo bot only) · OpenAI only · OpenAI, Deepgram and Cartesia | +| Test call | How do you want to make the test calls? | I call the number (recommended) · Plivo calls my phone | + +- **India:** Plivo expects KYC at signup, so assume an India data-region organization with an accepted KYC application, and check it: `plivo numbers compliance list --country IN --number-type local --status accepted -o json`. If that lists no application, stop and explain that KYC comes first (`plivo skill install audio-streaming`, India section). Otherwise continue, with the India notes in steps 4 and 6. +- **Another country (from "Other"):** the Compliance API covers India only, so it cannot tell you what another country needs. Use the search result in step 4 instead: rent only a number whose `restriction` is null. If every result has a `restriction`, stop and explain its `restriction_text` (for example, an address proof). +- **Money:** ask about each paid step separately with your question tool, after you show its preview. There are two: renting a number, and each call. Never pass `--yes` unless the user said yes to that exact step. + +## Rules for every change to the account + +- **Preview first, in its own command.** Run the preview with `--dry-run`, show the output to the user, and ask. Then run the real command. Never run the preview and the write in one shell command. +- **Some writes have no `--yes` gate.** `account applications create`, `account applications update` and `numbers update` write as soon as they run without `--dry-run`. The preview is your only safety check. +- **Record every value before you change it.** For example, read the number's current `application` first; that is the rollback value. +- **Never leave the number on a URL that does not answer.** The resting answer URL for a new application is Plivo's demo document. It returns valid XML on GET, so a caller hears a short demo message, not an error. +- **Check the answer URL before every `forward` start.** Before you start `streams forward`, check that the application's `answer_url` and `answer_method` equal the recorded resting values. `forward` saves whatever it finds and restores that on exit. So a dead tunnel URL left behind by a killed run would be saved and restored again. +- **Run a background `forward` with `-o table`.** When stdout is not a terminal, `forward` defaults to JSON output. In JSON mode it prints nothing until it exits, not even its `✓ Ready.` line. +- **Keys:** + - Never read, print or ask for API keys in the chat. The user writes them into `.env` with an editor. + - Never run `plivo login` yourself; it needs a browser. Ask the user to run it; in Claude Code they type `! plivo login`. +- **Writes can print nothing on stdout,** even with `-o json`. Check the exit code, then read the result back with a `get` command. +- **Number formats:** + - Pass numbers to `numbers` commands as digits, exactly as `numbers search` or `numbers list` returns them (for example `14155551234`). + - `forward --number` only prints the number, so write it in E.164 form (`+14155551234`). + +## Step 1: check tools and login + +```bash +plivo --version # if missing: brew install plivo/tap/plivo, or the install.sh one-liner from the Plivo docs +plivo auth whoami -o json # exit 2 with AUTH_MISSING: ask the user to run `plivo login`, then run this again +uv --version # the bots run with uv; if missing, ask the user to install it (https://docs.astral.sh/uv/) +ssh -V # `forward` uses ngrok when it is installed, otherwise localhost.run over ssh +``` + +Check: `plivo --version` is v1.1.3 or later, `whoami` shows the account the user expects, and `cash_credits` is above 0. + +- **Version gate:** rent no number and place no call until this check passes. Earlier releases cannot carry the call (v1.1.0 to v1.1.2 end it at once with 4010). `plivo upgrade --check` reports whether a newer release exists; ask, then run `plivo upgrade` (Homebrew installs: `brew upgrade plivo`). A dev build (`-dev`, as in `0.1.0-dev`, or `vX.Y.Z-N-g`) is unknown: ask the user, or treat it as unsupported. +- **ngrok without an authtoken:** if ngrok is installed but has no authtoken, `forward` fails. Run it again with `--tunnel localhost.run`. +- **Trial accounts:** the docs require a verified sandbox number to make calls from a trial account, and they say nothing about inbound calls. If a call fails on a trial account, ask the user to verify the phone they call from in the Plivo console. + +## Step 2: ask the setup questions + +Use the table above. + +## Step 3: test the echo bot, no phone + +Create `echo_bot.py` in the user's working directory: + +```python +# /// script +# requires-python = ">=3.11" +# dependencies = ["websockets>=14"] +# /// +import asyncio +import json + +import websockets + + +async def echo(ws): + try: + async for raw in ws: + msg = json.loads(raw) + if msg.get("event") == "media": + await ws.send(json.dumps({ + "event": "playAudio", + "media": { + "contentType": "audio/x-mulaw", + "sampleRate": 8000, + "payload": msg["media"]["payload"], + }, + })) + except websockets.ConnectionClosed: + pass + + +async def main(): + async with websockets.serve(echo, "127.0.0.1", 8765): + print("echo bot on ws://127.0.0.1:8765/ws", flush=True) + await asyncio.Future() + + +asyncio.run(main()) +``` + +Start it in the background with `uv run --script echo_bot.py`, and keep it running. Then run: + +```bash +plivo voice streams test --to ws://127.0.0.1:8765/ws --bidirectional --duration 3 -o json +``` + +Check: `frames_read_back` is above 0. If it is 0, the bot is not running or listens on another port: read its output and start it again. This step places no call and changes nothing on the account. + +## Step 4: get a number + +To use a number the user already has: + +1. List the numbers: + + ```bash + plivo numbers list --services voice -o json # pick one from data.objects[].number + ``` + +2. Attach it to `my-first-agent` only if the user calls in: a Plivo call (`voice calls make`) carries its own answer URL. +3. With the step 5 attach preview, warn the user: `numbers update --app-id` moves the number's whole application link, so its inbound calls and messages go to `my-first-agent` until you restore it. Get an explicit yes. + +To rent a new number: + +```bash +plivo numbers search --country --type local --limit 5 -o json +plivo numbers buy --dry-run +``` + +Pick one whose `voice_enabled` is true and whose `restriction` is null. Show the number, `setup_rate`, `monthly_rental_rate` and `voice_rate` from the search result. Ask the user, then run `plivo numbers buy --yes`. + +**India:** search with `--country IN --type local` and pick a landline number (a city code such as 022 or 080); landline numbers are for service and transactional calls. The accepted KYC application links to the number at purchase. If a number has a `restriction`, show its `restriction_text` and ask before you rent it. If the search is empty although the KYC check passed, stop and show both outputs to the user. If `buy` fails with `compliance_application_id is required`, follow the audio-streaming skill's India section. + +Either way, read the number and record its current `application`. This is the rollback value: + +```bash +plivo numbers get -o json +``` + +If it ends in `/Zentrunk/Trunk//`, the number is on a SIP trunk, and the rollback is `--trunk-id `, not `--app-id`. + +## Step 5: create or reuse my-first-agent + +The API matches `app_name` by prefix, so check for the exact name yourself: + +```bash +plivo api GET /Application/ --query app_name=my-first-agent -o json +``` + +**If an object in `data.objects` has `app_name` equal to `my-first-agent`, reuse it:** + +1. Read it with `plivo account applications get -o json`. +2. Record its `answer_url` and `answer_method` as this run's **resting values**. +3. Check that the recorded URL answers. Send `curl -s -i` with the recorded method and look for status 200 and a body that starts with `/`: + + ```bash + plivo numbers list -o json # 20 per page; while data.meta.next is set, run it again with --offset 20, 40, ... + ``` + + `forward` redirects every number on the app. If numbers other than the user's are attached, ask before you continue. + +**If no object matches, create the application with the demo resting URL.** Preview it first, and run it again without `--dry-run` after the user agrees: + +```bash +plivo account applications create --app-name my-first-agent \ + --answer-url https://s3.amazonaws.com/static.plivo.com/answer.xml --answer-method GET --dry-run +``` + +Read `app_id` from the output, and record the demo URL and `GET` as the resting values. + +- The name uses only lowercase letters and hyphens, because the Applications API allows only letters, digits, hyphens and underscores. +- The demo URL answers GET only, so keep `--answer-method GET`. + +Attach the number, unless step 4 rules it out. Preview first, then run it again without `--dry-run`: + +```bash +plivo numbers update --app-id --dry-run +plivo numbers get -o json # after the update: application ends with /Application// +``` + +## Step 6: echo call + +The echo bot from step 3 must still be running. `streams forward` does four things: + +1. It points the application at a tunnel to this machine. +2. It serves the Stream XML itself. +3. It checks Plivo's signature at the tunnel, then connects to the bot. +4. It restores the answer URL and method it found when it stops. + +Check the resting values first (see the rules), then preview: + +```bash +plivo account applications get -o json # answer_url and answer_method equal the recorded resting values +plivo voice streams forward --number + --app --to ws://127.0.0.1:8765/ws --dry-run +``` + +Show the preview and ask. Then run the same command with `-o table --yes` in place of `--dry-run`, in the background. Without `--yes`, `forward` asks for confirmation, and that prompt fails when no one can type an answer. Watch its output: + +- `✓ Ready. Dial …`: the tunnel is up. Place the test call the way the user chose (below). They speak for at least 10 seconds and should hear their own voice. +- `rejected: bad or missing Plivo signature`: the call came from a different account or subaccount than the CLI profile. Run `forward` under the profile that owns the number. If it shows on `/ws` for every call, the CLI is older than v1.1.3 (see step 1). +- `dial customer WS … failed`: the bot is not running or listens on another port. + +**The user calls in:** ask them to call the number. + +**Plivo calls the user:** + +1. Ask for the phone number to call, in E.164 form. On a trial account it must be a verified number. +2. Read the tunnel answer URL that `forward` set: `answer_url` in `plivo account applications get -o json` (it ends in `/answer`). +3. Preview the call, and show its price from `plivo api GET /Pricing/ --query country_iso= -o json`: take the rate of the longest `prefix` in `voice.outbound.rates[]` that matches the phone number (US +1907 costs more than +1415): + + ```bash + plivo voice calls make --from + --to --answer-url --answer-method POST --dry-run + ``` + +4. Ask, then run it again with `--yes` in place of `--dry-run`. +5. A 403 `Calls to this destination region are barred` means the account's geo permissions block that country. Professional (pay-as-you-go) accounts can allow only the US and India, in the console under Voice, Geo Permissions; other countries need an Enterprise plan. Offer that, or the user calls in. +6. `calls make` sets no time limit. If the call is still up after about 2 minutes, find it: `plivo api GET /Call/ --query status=live -o json` lists only the UUIDs of every live call on the account, so read each with `plivo api GET /Call// --query status=live -o json` and keep the one whose `to` is the phone and `from` is the number. Preview `plivo voice calls hangup --yes --dry-run`, ask, then run it without `--dry-run`. + +**India:** the test call must use an Indian phone in either direction, because India calls must stay India to India. The docs also require the server to be in India, and they do not say whether a stream to a laptop behind a tunnel counts. If the call ends with 2070 `Violates Media Anchoring`, the tunnel did not count: do not place another tunnel call in step 7. With an AI key, run step 8's one-host path on a server in India. Without one, go to step 9 and report item 2 as not met, with this reason. + +Before the call, list the calls once and note their UUIDs: calls to the number (`--direction inbound`) when the user calls in, calls to their phone (`--direction outbound`) when Plivo calls them. After the call, list them again and take the new call UUID. The list shows completed calls, so if it is not there yet, wait a few seconds and list again. If more than one call is new, ask the user: for a call in, match `from_number` to their phone; for a Plivo call, take the one that started when you placed it: + +```bash +plivo voice calls list --to --direction --limit 5 -o json +plivo voice calls get -o json # call_duration, hangup_cause_name, hangup_source +``` + +## Step 7: OpenAI bot and call + +Skip this step if the user has no AI key, and say that item 3 of the definition of done is not met. + +```bash +{ [ -d pipecat-examples ] || git clone --depth 1 https://github.com/pipecat-ai/pipecat-examples.git; } && + cd pipecat-examples/plivo-chatbot/inbound && uv sync && + { [ -f .env ] || cp env.example .env; } # safe to re-run: keeps the clone and a filled .env +``` + +Ask the user to edit `.env` in their editor: + +- **All three keys:** set `OPENAI_API_KEY`, `DEEPGRAM_API_KEY` and `CARTESIA_API_KEY`, and run the example as it is. +- **OpenAI key only:** set `OPENAI_API_KEY`. Then change `bot.py` to use one speech-to-speech service in place of the separate speech-to-text, language model and text-to-speech services: + - import `OpenAIRealtimeLLMService` from `pipecat.services.openai.realtime.llm`; + - build the pipeline as `transport.input()`, the user aggregator, the realtime service, `transport.output()` and the assistant aggregator; + - take the settings from Pipecat's own `examples/realtime/realtime-openai.py` for the Pipecat version that `uv sync` installed. Do not write them from memory, because they change between versions. +- **Plivo credentials:** the example also reads `PLIVO_AUTH_ID` and `PLIVO_AUTH_TOKEN` for its Plivo serializer. The user copies them from the Plivo console. The CLI keeps its token in the operating system's keychain; never read it from there. + +Stop the echo stage: + +1. Send Ctrl-C or `kill -TERM ` to the echo `forward` process. +2. Confirm the restore with `plivo account applications get -o json`. +3. Stop the echo bot. + +Then start the OpenAI bot in the background, unbuffered so its errors show in its output, and test it without a phone: + +```bash +PYTHONUNBUFFERED=1 uv run server.py # port 7860; serves the answer XML on GET / and the bot on /ws +plivo voice streams test --to ws://127.0.0.1:7860/ws --bidirectional --duration 10 -o json +``` + +The first connection loads Pipecat, so it can be slow. If `frames_read_back` is 0, read the server output, fix the error it shows (a missing key is common), and run the test once more. After any change to `.env` or the bot code, restart the server before you re-test: a running server keeps the env and code it loaded at start. To see which keys are set without printing their values, run `awk -F= '/^[A-Z_]+=/ {print $1, (length($2) ? "set" : "EMPTY")}' .env`. + +Check the resting values again, then point `forward` at the bot. Preview first, then run it with `-o table --yes` in the background after the user agrees: + +```bash +plivo voice streams forward --number + --app --to ws://127.0.0.1:7860/ws --dry-run +``` + +Place the test call as in step 6, the way the user chose, and ask them to talk to the bot. Check the call record the same way. + +## Step 8: offer a deploy + +Stop `forward` first with Ctrl-C or `kill -TERM `, and confirm the restore with `plivo account applications get -o json`. + +The agent works only while this machine, the bot and `forward` run. To keep it live, the answer URL and the bot need a public HTTPS host. Ask before you deploy anything, because hosting costs money. If the user says no, go to step 9. The example's `Dockerfile` builds only `bot.py`, for Pipecat Cloud. It does not include `server.py`, which serves the answer XML. + +**Before you expose the bot:** the example server's `/ws` has no Plivo signature check, so anyone who finds the URL can drive the bot on the user's AI keys. Keep it local, or add signature validation first: Plivo signs the WebSocket upgrade over `http:///`, not the `wss://` URL (recipe: the signature section listed below). + +Two paths: + +1. **Pipecat Cloud:** + - Deploy `bot.py` with the example's `Dockerfile` and `pcc-deploy.toml`. + - Host `server.py` separately with `ENV=production`, `AGENT_NAME` and `ORGANIZATION_NAME` set, as the example's README describes. +2. **One host:** run `uv run server.py` with `ENV=local` on a machine behind HTTPS. Its answer XML then points at `wss:///ws`. + +Before you change the application, check the XML: + +```bash +curl -s -i https:/// # status 200 and wss://… in the body +plivo account applications update --answer-url https:/// --answer-method GET --dry-run # preview; then run it again without --dry-run +``` + +If the number is on `my-first-agent`, a call now reaches the deployed bot. Place the test call as in step 6, the way the user chose; for a Plivo call, use `--answer-url https:/// --answer-method GET`. Check the call record as in step 6. Then record `https:///` and `GET` as the new resting values. + +For production hardening, install `plivo skill install audio-streaming` and read: + +- stage 6 (production hosting); +- stage 8 (operating it); +- its section "Callbacks, signature validation, timeouts". + +## Step 9: leave a safe resting state + +Do this on every exit, including after a failure: + +1. If `forward` still runs, stop it with Ctrl-C or `kill -TERM `. It restores the answer URL and method. +2. Confirm the resting values: + + ```bash + plivo account applications get -o json # answer_url and answer_method equal the resting values + ``` + +3. If they do not match (for example, the process was killed), set them yourself, previewing with `--dry-run` first. For the demo resting values, the command is: + + ```bash + plivo account applications update \ + --answer-url https://s3.amazonaws.com/static.plivo.com/answer.xml --answer-method GET + ``` + +4. If the number is on `my-first-agent`, ask with your question tool where it should stay: + - on `my-first-agent`, where callers reach the resting document or the deployed bot; + - back on its old application: `plivo numbers update --app-id `, or `--trunk-id ` for a trunk (step 4); preview it with `--dry-run` first. Offer this only if the recorded `application` had a value, because `numbers update` cannot clear it. + + For a number the user already had, make "back on its old application" the default, unless they deployed in step 8. +5. To stop the monthly rental of a number rented in this run, preview with `plivo numbers release --yes --dry-run` (a release refuses `--dry-run` alone), ask, then run `plivo numbers release --yes`. After a deploy, offer this only if the user asks. + +## When a call fails + +```bash +plivo voice calls get -o json # hangup_cause_code, hangup_cause_name, hangup_source +plivo voice calls diagnose # AI explanation; it shares a small per-account rate limit with `plivo ask` +``` + +| You see | Likely cause | Do this | +|---|---|---| +| 403 `Calls to this destination region are barred` on `calls make` | The account's geo permissions block the destination country | Allow it in the console (Voice, Geo Permissions): Professional accounts can allow only the US and India, other countries need Enterprise. Or the user calls in | +| 7011 Error Reaching Answer URL | `forward` is not running, or its tunnel dropped. `forward` does not notice a dropped tunnel and leaves the answer URL on it | Stop `forward` (it restores the answer URL), check the resting values, then start it again and wait for `✓ Ready.` | +| 8011 Invalid Answer XML | The answer URL returned something that is not Plivo XML | Run step 9, check the answer URL, and try again | +| 2070 Violates Media Anchoring | India: a call leg, or the bot's server, is outside India. A laptop behind a tunnel may count as outside | Follow the India note in step 6 | +| The call connects but the caller hears nothing | The bot is not running, or it listens on another port | Run the step 3 or step 7 `streams test` again | + +For anything deeper, install `plivo skill install audio-streaming`. + +## Out of scope + +- India KYC itself, and 140 or 160 series numbers (`plivo skill install audio-streaming`). +- Outbound calling campaigns, production hosting and monitoring (`plivo skill install audio-streaming`). +- SIP platforms such as LiveKit, ElevenLabs, Retell and Vapi (`plivo skill install sip-trunking`). diff --git a/first-agent-skill/embed.go b/first-agent-skill/embed.go new file mode 100644 index 0000000..46d7d24 --- /dev/null +++ b/first-agent-skill/embed.go @@ -0,0 +1,12 @@ +// Package firstagentskill embeds the Plivo first-agent skill file (SKILL.md) so +// `plivo skill install` can write it out without a network round-trip. +// Mirrors the cli-skill package: the directory is first-agent-skill (matching +// skills.sh's GitHub raw path); the package is firstagentskill. +package firstagentskill + +import _ "embed" + +// SkillMD is the contents of SKILL.md. +// +//go:embed SKILL.md +var SkillMD string diff --git a/go.mod b/go.mod index 8e5702c..7677646 100644 --- a/go.mod +++ b/go.mod @@ -11,6 +11,7 @@ require ( github.com/zalando/go-keyring v0.2.8 golang.org/x/sys v0.48.0 golang.org/x/term v0.46.0 + gopkg.in/yaml.v3 v3.0.1 ) require ( diff --git a/go.sum b/go.sum index b4b0908..6e2a092 100644 --- a/go.sum +++ b/go.sum @@ -32,6 +32,7 @@ golang.org/x/sys v0.48.0 h1:bbX/i/6MgT9BVLM9RT1thmxL04yeTAhbEz4SyadbXoo= golang.org/x/sys v0.48.0/go.mod h1:hNLxWAXmnKAxqDtdwIYC4bM9oQPEecfsnNMuSxOs3og= golang.org/x/term v0.46.0 h1:3+OXuTbaKDgwk8jTi3aSLHRlmWqHEUDUtxnbFigO4YE= golang.org/x/term v0.46.0/go.mod h1:+K02xbkittuwc0Am4abfA3Fc+XRGXkvBXNO88NCXPoc= +gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405 h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM= gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/sip-trunking-skill/SKILL.md b/sip-trunking-skill/SKILL.md index 94d8cd3..95c88c4 100644 --- a/sip-trunking-skill/SKILL.md +++ b/sip-trunking-skill/SKILL.md @@ -1,915 +1,211 @@ --- name: plivo-sip-trunking -description: Connect an AI voice platform (LiveKit, ElevenLabs, Retell, Vapi, a self-hosted stack or any SIP-capable agent) to phone calls with Plivo SIP trunking (Zentrunk) and take it live, using the Plivo CLI. Load this for "SIP trunk", "Zentrunk", "origination URI", "termination domain", " + Plivo", "call not reaching my platform", "inbound trunk / outbound trunk", "transport tcp/tls/udp", "media anchoring", a Zentrunk hangup code (4090, 4170, 4590, 4000, 4550 and others) or SIP response (404/407/408/480/486/503) on a trunk call, SIP REFER / transfer to a human from an agent, India KYC for a trunk number, or "am I ready to go live on SIP". Readiness check first, then six stages with one check each, a read-only readiness checklist, a URI format check, a hangup-code table with owner and fix, and a CLI-first debugging order. Self-contained: it carries everything the SIP trunking journey needs, including the India prerequisites, and points at plivo-audio-streaming, plivo-voice-xml and plivo-cli as separate installs for work outside that journey. +description: "Connects an AI voice platform (LiveKit, ElevenLabs, Retell, Vapi, xAI, self-hosted) to phone numbers over Plivo SIP trunking (Zentrunk). Use for origination URIs, inbound or outbound trunks, TCP/TLS/UDP transport, Zentrunk hangup codes (4090, 4170, 4590), SIP REFER transfer, India calling rules, go-live checks. Not for WebSocket bots (plivo-audio-streaming) or Plivo XML (plivo-voice-xml)." license: Apache-2.0 --- # Plivo SIP trunking for AI voice agents -## What a finding licenses you to say +Take a developer from "my agent runs on platform X" to a number that reaches it, outbound calls that connect, transfers that work, and failures they can read. Ask first: inbound, outbound or both? Which platform, hosted or self-hosted? Which country (India changes the rules)? Then run the readiness check and go stage by stage. -Three tiers, and they decide your verdict. +Evidence rule: say calls **will break** only when a documented rule or a known failure says so; otherwise call it a risk and say what to check. Hangup codes and names here come from Plivo's public table. Anything marked *observed* (SIP responses, message sequences) is seen on the wire, not a Plivo contract: never quote it as one. -- **Will break.** A documented rule says so, or a call with this shape is known to have failed. Say the calls break, and name the failure. -- **Risky.** Plausible, unverified, or seen to work in some deployments. Say what could go wrong and what to check. Name no hangup code or SIP response. -- **Style.** No functional effect. Say so. +## Objects and CLI rules -Nothing outside the first tier is a reason to tell someone their calls will break. Every hangup code stated as a fact below comes from the public table; anything in a "SIP seen" column, or marked observed, is risky and must not be quoted as a Plivo contract. +- **Inbound trunk**: origination URI (`host[:port];transport=udp|tcp|tls`) + trunk + number. **Outbound trunk**: credential or IP access control list (IP ACL) + trunk; the platform dials the trunk's `trunk_domain`, `.zt.plivo.com` (the console labels it Termination SIP Domain). Only inbound trunks attach to numbers. +- Commands: `plivo sip uris|trunks|credentials|ip-acl|calls` and `plivo numbers update --trunk-id `. `plivo sip --help` is the source of truth for flags; never invent one. Call Insights has no typed command: `plivo api GET /Zentrunk/Call//Insights/ -o json`. +- `plivo docs show ` prints a JSON envelope outside a terminal (agent shells too): add `-o table`. Its source drops field names from attribute lists and can cut a page short: for either, read `https://www.plivo.com/docs/.md`. +- Pass numbers to `numbers` commands as digits without `+` (`14155551234`). +- List commands return 20 rows by default: page with `--offset` before concluding something is missing. +- Passwords go in on stdin only (`--password-stdin`), never on a command line, in a file or in chat. Ask the user to run `read -rs SIP_PASSWORD && export SIP_PASSWORD` in the shell that runs you, or to run the command themselves. +- Quote every `--uri` value: the `;` in `host;transport=tcp` otherwise ends the shell command. -## Overview +### Write safety -You are guiding a developer (or their coding agent) from "I have an agent on platform X" to a phone number that reaches it, outbound calls that connect, transfers that work, and a way to read failures. Run the readiness check first, then go stage by stage with the check at each. Speak plainly. +- `plivo sip` create and update, `numbers update` and `numbers compliance create|update|link` have **no `--yes` gate**: they write as soon as they run without `--dry-run`. Preview with `--dry-run`, show it, get approval, then run the same command without it. Never put preview and write in one shell command. +- **Routing a number** (`numbers update --trunk-id `) reroutes a possibly live number immediately. Do it last, after the platform side (import, dispatch rule, agent) exists. First run `plivo numbers get -o json` and record `application`: it is the rollback. Pass only its trailing id: `/v1/Account//Application//` is `--app-id `, `.../Zentrunk/Trunk//` is `--trunk-id `. If `application` is empty, tell the user before asking for approval that `numbers update` cannot restore that state (an empty `--app-id` is rejected). The command reads the trunk even under `--dry-run` and refuses an outbound trunk or one not on this account, so its preview works only once the trunk exists; `--app-id` skips that check, so use `--trunk-id`. +- **Updates that hit live traffic**: `sip trunks update --status disabled|--secure|--uri` and `sip uris update --uri` take effect immediately. `get` the object and record the current values first, then preview, then apply. +- **Deletes** need `--yes` and cannot be undone. Deleting an in-use URI also deletes the trunks that point at it, so their numbers stop routing; deleting a trunk detaches every number on it; deleting a credential or IP ACL breaks the outbound trunks using it. The preview is the delete run **without** `--yes` (`--dry-run` shows no dependents: it skips the read): a URI, credential or IP ACL delete lists the dependent trunks but not their numbers, a trunk delete counts its numbers, and either prints nothing if its read fails. Count each affected trunk's numbers yourself: `plivo numbers list -o json | jq '.data.objects[] | select((.application // "") | contains("/Zentrunk/Trunk//")) | .number'`, paging with `--offset`. Disabling a trunk is reversible but an outage for its numbers, not a safe alternative. +- **Deleting a URI safely**: find its trunks with `plivo api GET /Zentrunk/Trunk/ --query "primary_uri_uuid=" -o json`, then `fallback_uri_uuid=`; count their numbers (above); repoint each trunk (`plivo sip trunks update --uri `, or `--fallback-uri`), test a call, then delete (its preview now lists no trunks). +- `sip ip-acl update --ip` replaces the whole list; `sip credentials update` always sets a new password from stdin. -Two objects do the work. An **inbound trunk** carries calls *to* the platform: origination URI, trunk, number attached. An **outbound trunk** carries calls *from* it: credential or IP list, trunk, its `trunk_domain`. Only inbound trunks attach to numbers (). The URI string `host[:port];transport=...` is the whole inbound integration. The generic guide says a transport **mismatch** is the most common reason an inbound integration fails silently (). Omitting `;transport=` is not the same thing: Plivo's own API examples publish URIs with no transport parameter, and a missing one is read as UDP, so treat an absent transport as a risk to check against what the platform documents rather than a fault. +## Readiness check (read-only; run first and again before go-live) -Use the Plivo CLI for every Plivo-side step: typed commands where they exist (`plivo numbers ...`, `plivo account ...`), the generic `plivo api /Zentrunk/...` everywhere else. **The CLI has no trunk commands**; check with `plivo --help` and never invent a flag. Preview every mutation with `--dry-run`, show the previewed request, and get the user's approval before running the same command with `--yes`. Where a step has no CLI path this file says "console only". +Stop at the first blocking answer. Steps 3-5 are inbound only and step 6 is outbound only: an outbound-only setup (for example a Vapi agent that only places calls from a Plivo caller ID) skips 3-5 and goes green without an inbound trunk. Retell needs step 6 even for inbound only. -**What this file assumes you have: nothing but this file and the `plivo` CLI.** Everything the SIP trunking journey needs is here, including the India prerequisites. Other Plivo skills are separate single files you may not have. Install one only if the task moves outside this journey: `npx skills add https://www.plivo.com/docs --skill plivo-voice-xml` for Plivo XML and answer URLs, `npx skills add https://www.plivo.com/docs --skill plivo-audio-streaming` for a WebSocket voice bot, and `plivo skill install` for the CLI's own reference. If none is installed, use `plivo --help` and , and say which source you used. +1. `plivo auth whoami -o json`: the account you meant, `cash_credits` above zero. 4030 (no credits) carries no direction in the docs, so treat zero as a risk to inbound too. +2. `plivo numbers get -o json` (the inbound number, or the outbound caller ID): on this account, `voice_enabled` true; for inbound, record `application`. India: the gates below. +3. Read `application`. `Trunk/`: run `plivo sip trunks get -o json` (fields under `data.object`) and require `trunk_direction` `inbound`, `trunk_status` `enabled`, `primary_uri_uuid` set (without it: 4310 `uri_not_found`). `Application/`: an XML application, not a trunk; stop. Any other shape: report it, do not guess. +4. `plivo sip uris get -o json`: passes the URI checks (Stage 2) and matches the platform matrix; `authentication_needed` only when the platform challenges Plivo. +5. `fallback_uri_uuid` set? Missing is a warning: the fallback is the only re-route when the primary is unreachable or errors. +6. `plivo sip trunks list --direction outbound -o json`: an `enabled` trunk with `credential_uuid` (`plivo sip credentials get `) or `ipacl_uuid` (`plivo sip ip-acl get `: no `/0`, no `/1`, nothing wider than the platform's published IPs). Its `secure` must match the platform's outbound transport: `true` only when the platform dials over TLS, `false` for TCP or UDP. +7. Concurrency and CPS: console only, Organization settings > Account limits. +8. Platform-side items (Stage 3): Plivo cannot see them, so ask. +9. Optional: one SIP OPTIONS to the URI host over its transport. A DNS or TLS error proves something; a timeout proves nothing (hosted platforms may ignore unknown sources). -What is in this file, in order: the readiness checklist, prerequisites by country, six stages, go-live, the debugging order with a short code table, what not to do, then the deep sections (India, security and limits, the platform table, request bodies to copy, SIP REFER, the Zentrunk API, the full hangup-code table). +## Platform matrix -Three questions before anything else: inbound, outbound or both (Retell needs the outbound trunk even for inbound-only)? Which platform, hosted or self-hosted? Which country are the numbers and callees in (India changes the rules below)? +Plivo publishes guides for LiveKit, ElevenLabs, Retell, Vapi and xAI, plus a generic one: `plivo docs show voice-agents/sip-trunking/integration-guides/` (https://plivo.com/docs/voice-agents/sip-trunking/integration-guides/), where `` is `livekit`, `elevenlabs`, `retell`, `vapi`, `xai-voice-agents` or `other-platforms`. -## Readiness check (run first, and again before go-live) +| Platform | Inbound URI | Outbound auth | `secure` | Platform side (Plivo cannot see it) | India | +|---|---|---|---|---|---| +| LiveKit Cloud | `;transport=tcp`; `tls` for secure trunking | credential | recommended; enable LiveKit secure trunking too | inbound trunk listing `+` **and** a dispatch rule; outbound trunk with `trunk_domain`, credential username and password | region pinning, then the endpoint LiveKit shows | +| ElevenLabs | `sip.rtc.elevenlabs.io:5060;transport=tcp` or `:5061;transport=tls` (only these two) | credential | recommended; TLS in ElevenLabs' outbound settings | number imported on a SIP trunk with an agent; outbound settings with `trunk_domain` and credential | India deployment from ElevenLabs, then `sip.rtc.in.residency.elevenlabs.io:5060;transport=tcp` | +| Retell | `sip.retellai.com;transport=tcp` (or `tls`) | credential, **required even inbound only**: Retell will not import a number without it | off by default; if on, Retell Outbound Transport = TLS | import the number with `trunk_domain` (no `sip:`), credential username, password, transport; bind inbound and outbound agents. Imports cannot be edited: delete and re-import | not in Plivo's docs; confirm with Retell | +| Vapi | `sip.vapi.ai;transport=udp` | IP ACL with the two `/32` addresses in Plivo's Vapi guide | no | register the number (BYO SIP trunk) and assign an assistant | **not supported** | +| xAI Voice Agents | `sip.voice.x.ai;transport=tls` (the console's `sip:sip.voice.x.ai;transport=tls` is fine too) | none: **inbound only**, no outbound trunk | not used | add the number to the agent (Direct SIP); allow every Plivo signaling range the xAI guide lists, or set the same SIP digest credentials on both sides | not stated; confirm with xAI | +| Other or self-hosted | `[:port];transport=` (TLS usually 5061); the API also accepts `sip:user@host` | credential; IP ACL only for published static IPs | both sides must agree | route the number to an agent; allow Plivo signaling and media (Stage 3); **no digest challenge** to Plivo unless the same username and password are on the URI | must terminate SIP and media in India | -Read-only. Nothing here changes anything. Run in order and stop at the first blocking answer. +For a platform not listed, say Plivo publishes no guide for it, take the host, port and transport from the platform's own docs, and never compose a hostname or guess a transport. The Origination URI API takes only `name`, `uri`, `authentication_needed`, `username` and `password`; anything more is a question for Plivo. OpenAI Realtime is documented over audio streaming, not SIP (`plivo skill install audio-streaming`). -1. `plivo auth whoami -o json` and `plivo account get -o json`. Look for: the account you meant, and `cash_credits` above zero. Inbound trunk calls are billed too, so a zero balance stops inbound as well; every call then ends 4030. -2. `plivo numbers get -o json`. Look for: the number is on this account and `voice_enabled` is true. The live API also returns `active` and, for Indian numbers, `compliance_status`; neither is in the published phone number schema, so treat a missing field as "not known" rather than as a pass or a failure, and confirm India compliance with `plivo numbers compliance list --country IN --status accepted -o json`. Note the `application` field: it is the current binding and your rollback value. -3. Take the id out of the `application` field and read the trunk: `plivo api GET /Zentrunk/Trunk// -o json`. How that field renders is not documented, so handle three cases rather than assuming one: a URL containing `Trunk/` means an inbound trunk, a URL containing `Application/` means an XML application and not a trunk at all (stop here, the number is not on SIP trunking), and a bare id means try the trunk GET and fall back to `plivo account applications get ` if it 404s. Say which of the three you saw. Look for: `trunk_direction` is `inbound` (an outbound trunk or an XML application cannot carry inbound), `trunk_status` is `enabled`, and `primary_uri_uuid` is set. A trunk with no primary URI has nowhere to send the call; the published 4310 row is `uri_not_found`, "Origination URI not found", so expect that code rather than asserting it for every call. -4. `plivo api GET /Zentrunk/URI// -o json`. Look for: a URI that passes the URI checklist in stage 2 and matches the platform table. Check `authentication_needed` is set only when the platform challenges Plivo, and that the trunk's `secure` flag agrees with `;transport=tls`. -5. `plivo api GET /Zentrunk/URI// -o json`. A missing fallback URI is a warning, not a blocker: it is the only way Plivo re-routes when the primary is unreachable or returns an error. -6. Outbound only: `plivo api GET /Zentrunk/Trunk/ --query trunk_direction=outbound --query limit=20 -o json` (paginate with `--query offset=N`), then `plivo api GET /Zentrunk/IPAccessControlList// -o json` when one is used. Look for: an enabled outbound trunk with a credential or a narrow IP list; no `0.0.0.0/0`, no `/0`, no `/1`. Vapi needs its two `/32` addresses. Retell will not import a number without its termination URI, even inbound only; the username and password fields on its import form are not marked required, so create the credential without claiming the import fails without it. -7. Ask the platform-side questions in stage 3. Plivo cannot see any of them, and they cause 4090 and 4410. -8. Optional reachability probe: one SIP OPTIONS to the URI host over its transport. A timeout proves nothing, because hosted platforms may ignore OPTIONS from unknown sources; a DNS or TLS error does prove something. +## India: four gates, each blocking until proved -There is no single CLI command that runs this checklist, and no `plivo sip` command tree at all. Run the steps by hand, in order. +1. **Data region.** The organization must be in the India data region. It cannot be changed (create a new organization) and is not readable over the API: confirm in the console. +2. **KYC.** A compliance application in `accepted` status (not `submitted`) linked to the number: `plivo numbers compliance list --country IN --status accepted -o json`, then `plivo numbers compliance get --expand linked_numbers -o json`. The required documents come from the live `plivo numbers compliance requirements --country IN --number-type local --user-type business -o json`, never from memory. Documents and legal details come from the user: never fill them in or invent them, and never create, update or link an application unless the user explicitly says go (it is a regulatory filing, and `create` submits at once). CLI flow: `plivo numbers compliance --help`. Process: `plivo docs show numbers/rent-india-numbers` (https://plivo.com/docs/numbers/rent-india-numbers). +3. **Media anchoring.** The platform must terminate SIP and media in India, or calls end **4590** `domestic_anchored_terms_not_met`. 4590 is not a KYC check: check KYC as its own gate. Platform support is in the matrix. **Vapi is not supported** and no Plivo-side setting fixes its 4590: send India traffic to a platform that terminates in India (LiveKit region pinning, ElevenLabs India deployment) or a self-hosted stack there; keep Vapi for other countries. `plivo docs show voice-agents/sip-trunking/deploy/calling-in-india` (https://plivo.com/docs/voice-agents/sip-trunking/deploy/calling-in-india). +4. **Traffic rules.** Both legs in India, the right number series for the call type, explicit consent, no cold calls, UCC complaints handled. These come from a Voice API page but are number-level, so they apply to trunk numbers: `plivo docs show voice/concepts/india-calling` (https://plivo.com/docs/voice/concepts/india-calling). -| Check | Verified how | Failure it prevents | -|---|---|---| -| CLI has credentials; account has credits | step 1 | 4030 no credits to start a call. The published row carries no direction and no page says inbound trunk calls are billed, so treat a zero balance as a risk to inbound as well and check the balance rather than predicting the code | -| Number rented, active, voice-enabled | step 2 | nothing routes | -| India: an accepted compliance application attached to the number; data region confirmed by you | step 2 and `plivo numbers compliance list --country IN --status accepted`, plus a console check for the region | the number cannot be rented or used for calls until the application is `accepted`. The docs give no hangup code for this, so do not expect 4590: that code is the India media-anchoring violation and is a different gate | -| Number attached to an **inbound** trunk that is enabled | step 3 | nothing routes; an outbound trunk or XML application cannot carry inbound | -| Trunk has a primary URI | step 3 | 4310 | -| URI host, port, `;transport=` match the platform table; no scheme for the four documented platforms; no URL, no private IP | step 4 | 4170 no answer; silent inbound failure | -| `authentication_needed` only when the platform challenges Plivo | step 4 | a platform that challenges Plivo with no credentials on the URI cannot complete the handshake. The published 4150 `proxy_authentication_required` row describes a **carrier** requiring proxy auth, so do not promise that code here | -| `secure` flag agrees with `transport=tls` | step 4 | one-sided TLS; 4110 | -| Fallback URI present (warning) | step 5 | platform 503 with no second route | -| Outbound trunk enabled with a credential or a narrow IP list; no `0.0.0.0/0`, `/0` or `/1` | step 6 | 407 never answered; toll fraud on open lists. The IP ACL pages describe the object and the field but publish no prohibition, so the wide-range block is this file's own rule | -| Vapi's two `/32` IPs present; Retell has an outbound trunk | step 6 | Vapi's guide instructs you to add the two IPs but does not state the failure mode, so treat a missing IP as a risk. Retell's guide does state the gate: it will not import a number without its termination URI, even inbound only | -| Platform reachable | step 8 | DNS and TLS errors (a timeout proves nothing) | -| Platform-side items Plivo cannot see | step 7 | 4090 platform 404, 4410 486 | - -## Prerequisites by country - -**India.** Read the India section below. Account in the India data region (cannot be changed; not readable over the API), KYC `accepted` and linked to the number (`plivo numbers compliance requirements|create|get|link`, with documents the user supplies), caller ID a Plivo India number, both legs in India, right number series, consent, no cold calls. **4590** `domestic_anchored_terms_not_met` is the documented India media-anchoring code (SIP 403 from Plivo, no platform leg): the published cause is a call leg outside India. KYC is a separate readiness gate, so check it separately with `plivo numbers compliance get`; do not read 4590 as proof that KYC is missing. **Vapi cannot do India**; ElevenLabs needs its India deployment and `sip.rtc.in.residency.elevenlabs.io:5060;transport=tcp`; LiveKit needs region pinning; Retell: confirm with Retell first (). - -**US.** No KYC. Caller ID must be a Plivo number you rent or a verified caller ID, else 4190. Verified status means full attestation A, which the page gives two routes to: Plivo can validate the calling line identification, or the call originated from a DID rented on the Plivo platform (). A number on the same account is the reliable route, so prefer it. Geo permissions: turn off every country you do not call (console only). SIP trunking **rejects** calls above the CPS limit with 5180 instead of queueing; pace the dialer (). Detail: "Security and limits" below. - -## Stage 1: Account and number - -```bash -plivo auth whoami -o json && plivo account get -o json # right account? cash_credits > 0? -plivo numbers list --services voice -o json # or: plivo numbers search --country US --type local --limit 5, then plivo numbers buy (preview first) -plivo numbers compliance list --country IN --status accepted -o json # India only -``` - -**Check:** the number is on this account, voice-enabled, and (India) compliance is `accepted`. - -## Stage 2: Inbound, Plivo side - -Pick the URI from the table (detail and India variants in "Platforms" below; request bodies to copy in "Request bodies for `plivo api`"): - -| Platform | URI | Outbound auth | -|---|---|---| -| LiveKit Cloud | `;transport=tcp` (`tls` for secure trunking) | credentials | -| ElevenLabs | `sip.rtc.elevenlabs.io:5060;transport=tcp` or `:5061;transport=tls` | credentials | -| Retell | `sip.retellai.com;transport=tcp` (or `tls`) | credentials, required by Retell | -| Vapi | `sip.vapi.ai;transport=udp` | IP list `44.229.228.186/32`, `44.238.177.138/32` | -| Other or self-hosted | `[:port];transport=`; the API also accepts `sip:user@host` | credentials, or its static IPs | - -Plivo publishes an integration guide for LiveKit, ElevenLabs, Retell and Vapi, and a generic guide for everything else. If the platform you were asked about is not one of the four, including OpenAI Realtime and xAI, say that Plivo does not document it, then use the "Other or self-hosted" row with the host, port and transport that platform publishes. Do not compose a hostname or guess a transport. - -Sources: , , , , , . +## Stage 1: account and number ```bash -plivo api POST /Zentrunk/URI/ --body @uri.json --dry-run # preview: prints the request, sends nothing -plivo api POST /Zentrunk/URI/ --body @uri.json --yes # only after the user approves the previewed body (data.uri_uuid) -plivo api GET /Zentrunk/URI// -o json # read back what was actually stored -plivo api POST /Zentrunk/Trunk/ --body @trunk-inbound.json --dry-run # after editing primary_uri_uuid -plivo api POST /Zentrunk/Trunk/ --body @trunk-inbound.json --yes # after approval (data.trunk_id) -plivo numbers get -o json # note the current binding: this is your rollback value -plivo numbers update --app-id --dry-run # app_id accepts an inbound trunk_id; there is no --trunk-id flag -plivo numbers update --app-id --yes # only after the user approves; this reroutes a live number -plivo numbers get -o json # confirm it now points at the trunk +plivo auth whoami -o json # right account, cash_credits > 0 +plivo numbers list --services voice -o json # or numbers search, then numbers buy (needs --yes; spends money) ``` -### Check the URI string before you create it - -Would break or mis-route calls: - -- It is a URL (`http://`, `https://`, or anything with a path). Plivo needs the SIP endpoint, not an answer URL. -- It contains a space, or the host has characters other than letters, digits, dots and hyphens, or there is no host at all. -- The port is not a number. -- `;transport=` is set to something other than `udp`, `tcp` or `tls`. -- The host is a private or unroutable address (10.x, 127.x, 192.168.x, 172.16 to 172.31, 169.254.x, 0.x). Plivo cannot reach it. -- A `sip:` or `sips:` scheme, or a user part, on LiveKit, ElevenLabs, Retell or Vapi. The documented value for those four starts with the host. The API does document the `sip:user@host` form for other or self-hosted platforms, so it is allowed there. -- A host that does not match the documented host for the platform you named. -- No `;transport=` at all, or a transport the platform does not document. LiveKit, ElevenLabs and Retell: `tcp`, or `tls` for secure trunking. Vapi: `udp`. No parameter means UDP. -- A port the platform's guide does not use. ElevenLabs publishes exactly two forms, `:5060;transport=tcp` and `:5061;transport=tls`; it does not say other ports are rejected, so treat anything else as untested rather than invalid. -- An Indian number with a non-India endpoint: Vapi is not supported at all, ElevenLabs must be the `in.residency` host, LiveKit must be the region-pinned endpoint your project gives you (copy it, do not compose it), Retell is unconfirmed. Otherwise calls end 4590. - -Fix before go-live: - -- port 5061 without `transport=tls`. For TLS itself, 5061 is what the generic integration guide's example and the technical specifications use, so prefer it, but some platform guides publish a TLS URI with no port at all: treat another port as something to confirm against that platform's own guide rather than as an error. -- A parameter other than `transport=`, or a parameter with no value. Only `transport=` is documented. -- A host ending in a dot. -- Vapi with no `;transport=`: Plivo sends UDP, which is what Vapi wants, but the docs write it explicitly. -- An unknown platform with no expected host and transport to compare against: get both from the platform's own docs before creating the URI. - -**Check:** `plivo numbers get -o json` shows the trunk and the readiness checklist has no blocking answer. Add a fallback URI when the platform offers a second region (). - -## Stage 3: Inbound, platform side (their dashboard) - -Plivo cannot see this, so ask and confirm (): LiveKit inbound trunk listing `+` **and** a dispatch rule (India: region pinning on); ElevenLabs number imported on a SIP trunk with an agent; Retell number imported (needs Stage 4 first) and an inbound agent bound; Vapi number registered with an assistant; self-hosted: Plivo signalling allowed on 5060/5061 and media UDP 10000 to 30000, and **no digest challenge** to Plivo unless the same username and password are on the Plivo URI (`authentication_needed`), or the handshake cannot complete. Do not promise 4150 for it: the published row for that code is a carrier requiring proxy auth. When this step is missing the platform answers **404** (ElevenLabs `Does not match any SIP Trunks`; other platforms use their own phrase). The published 4090 row is `destination_not_found`, "No route to destination", with a carrier framing and no direction, so read an inbound 4090 alongside the SIP flow rather than as proof of a missing import. Nothing in the docs ranks inbound failure causes, so treat "most common" as this file's experience, not a published fact. - -## Stage 4: Outbound +## Stage 2: inbound, Plivo side ```bash -# write the credential JSON first (username 5 to 20 alphanumeric; password 5 to 20 chars with one of ~!@#$%^&*()_+); never on the command line -plivo api POST /Zentrunk/Credential/ --body @cred.json --dry-run # preview (the preview shows the body, so do not paste it into chat) -plivo api POST /Zentrunk/Credential/ --body @cred.json --yes # after approval (data.credential_uuid) -# or for Vapi: plivo api POST /Zentrunk/IPAccessControlList/ --body @ipacl.json --dry-run, then --yes # data.ipacl_uuid -plivo api POST /Zentrunk/Trunk/ --body @trunk-outbound.json --dry-run # after editing credential_uuid -plivo api POST /Zentrunk/Trunk/ --body @trunk-outbound.json --yes # after approval -plivo api GET /Zentrunk/Trunk// -o json # data.object.trunk_domain = .zt.plivo.com (the create response has no domain) +plivo sip uris create --name --uri "" --dry-run # then without --dry-run; prints uri_uuid +plivo sip uris get -o json # read back what was stored +plivo sip trunks create --name --direction inbound --uri --dry-run # add --fallback-uri ; then without --dry-run ``` -Put `trunk_domain` (no `sip:`, no spaces), the credential **username** (not its name) and password into the platform's outbound trunk or number import. Caller ID = a Plivo number on this account. `secure: true` here means TLS in the platform too (Retell "Outbound Transport = TLS", LiveKit secure trunking). - -**Check:** re-run the readiness checklist, including the outbound steps, with no blocking answer, and the first call's SIP flow ends in 100/183. On a trunk that authenticates by credentials you will normally see INVITE, 407, then a second INVITE carrying them: that is the digest handshake, and the docs describe it in words (the trunk challenges the INVITE and the platform's auth block answers it, ). A trunk that authenticates by IP ACL has no challenge and no 407, and the trunk API takes either `ipacl_uuid` or `credential_uuid`, so the absence of a 407 is not a fault. Do not whitelist `0.0.0.0/0` to "make it work". +Inbound TLS is the URI's `;transport=tls`, not `--secure`. If the platform challenges Plivo on inbound, its username and password go on the URI, a different secret from the outbound credential: `printf '%s' "$PLATFORM_SIP_PASSWORD" | plivo sip uris create --name --uri "" --authentication-needed --username --password-stdin`. -## Stage 5: First call, both directions - -Dial the number from a phone; then place one outbound call from the platform to your own phone. Then read what Plivo saw: +Route the number only after Stage 3, following the routing rule in Write safety: ```bash -plivo api GET /Zentrunk/Call/ --query limit=5 -o json # newest records: hangup_cause_code, hangup_cause_name, hangup_source, transport_protocol, srtp -plivo api GET /Zentrunk/Call//Insights/ -o json # rtt, jitter, packet_loss, plivo_quality_score +plivo numbers get -o json # record application: the rollback +plivo numbers update --trunk-id --dry-run # then, after approval, without --dry-run +plivo numbers get -o json # confirm Trunk/ ``` -**Check:** `hangup_cause_code` 3000 or 3010 with a non-zero duration in both directions. The published table gives 3010 as `normal_hangup`, a normal hangup from the user, and nothing more, so a 3010 with 0 s duration is a call that ended at once without saying who ended it: read the platform logs and the SIP flow before concluding anything. Anything else: **Debugging** below. - -## Stage 6: Transfer to a human with SIP REFER (only if you need it) - -The platform sends `REFER` with `Refer-To: .zt.plivo.com>`; Plivo answers `202`, dials the target, reports progress with `NOTIFY 100/180/200` (answer each with 200 OK), and sends your leg a `BYE` when the target answers (). Rules: `Refer-To` **must** use your `.zt.plivo.com` trunk domain (other domains and arbitrary SIP URIs are blocked); no private IPs; E.164 target, else 400; UDP, TCP and TLS all fine; chained transfers allowed; **if the transfer target hangs up the whole call ends**; caller ID on the transfer leg is the caller's number on inbound calls and your Plivo number on outbound; do not send BYE before `NOTIFY 200 OK`; the destination country must be allowed in Geo Permissions; two call records. Some endpoints send `tel:` or `sips:` targets and Plivo may accept them; only the documented form is supported, so use it. Detail: "Transfer to a human with SIP REFER" below. - -**Check:** one test transfer; `NOTIFY 200 OK` seen, then Plivo's BYE. After a `NOTIFY 4xx/5xx` the caller hears silence until the agent speaks, and callers commonly hang up within seconds: make the agent speak as soon as the sipfrag is a failure. - -## Go-live - -- Re-run the readiness checklist for both directions, including the reachability probe; no blocking answers, warnings understood. -- Geo permissions trimmed to the countries you call (console only). Credentials never pasted anywhere; IP lists narrow. -- India: KYC accepted on every number, platform pinned to India, consent captured. US: Plivo caller ID, dialer under the CPS limit. -- A fallback URI, and a `secure` decision made on both sides. -- Watch daily: `plivo api GET /Zentrunk/Call/ --query hangup_cause_code=4090 --query limit=20 -o json` (and 4170, 4590, 4030). Outbound agents see 4410, 4340 and 4550 every day; those are outcomes, not faults. - -## Debugging with the CLI, in this order - -1. `plivo api GET /Zentrunk/Call/ --query limit=20 -o json` (filters: `call_direction`, `hangup_cause_code`, `hangup_source`, `from_number`, `to_number`, `end_time__gte=YYYY-MM-DD HH:MM`); then `plivo api GET /Zentrunk/Call// -o json` and `.../Insights/`. -2. Map the code with the table below, or with the full table in "Zentrunk hangup codes" at the end. `hangup_source`: `customer` = your platform, `carrier` = network side, `zentrunk` = Plivo. -3. Console Zentrunk, Logs, the call: **Call Stats** (trunk, transport, secure) and **SIP logs** (message flow, final response, PCAP). Trust the hangup code for Plivo's conclusion and the SIP flow for what the platform said; they can disagree. Read it in three lines: inbound with no INVITE towards your URI = Plivo refused (4590, 4030 or 4310); INVITE repeated with no reply = 4170; the platform's own 4xx is the answer (404 = not imported, 401/407 = your server challenged Plivo, 486 = no dispatch rule or agent, 503 = platform down). Outbound on a credential-authenticated trunk: one 407 then a second INVITE is normal, and a flow that **ends** at 407 means the credentials are wrong, whatever the hangup code says. On an IP ACL trunk there is no 407 at all. -4. `plivo voice calls diagnose ` is built for Voice API calls; whether it accepts a trunk call UUID is not verified. Try it once; do not loop it (rate limited with `plivo ask`). Then `plivo ask --call-uuid "..."` or Plivo support with the UUID and the PCAP. - -The public hangup-code table publishes a code, a name, a cause and a fix. It does not publish the SIP response that goes with each code. The **SIP seen** column below is therefore observational, gathered from SIP flows, not a Plivo contract: use it to recognise a flow you are already looking at, never as the reason for a conclusion. Where a row names a code as documented it is from the public table. - -| Code / cause | SIP seen (observed, not published) | Owner | Fix | -|---|---|---|---| -| in 4090 destination_not_found | platform 404. The phrase is the platform's own; Plivo's docs record only ElevenLabs `Does not match any SIP Trunks` () | platform config | import `+`, dispatch rule or agent on the platform | -| in 4170 request_timeout_customer | INVITE repeated, no reply (or Plivo 408) | platform config | URI host, port, `;transport=`; reachability probe; firewall; platform online | -| in 4150 proxy_authentication_required | platform 401/407 | the published row is a **carrier** requiring proxy auth; reading it as your platform challenging Plivo is observational | stop challenging Plivo, or URI `authentication_needed` plus the same credentials | -| in 4410 user_busy | platform 486 | the published table says only `user_busy`, destination is busy | inbound: check the dispatch rule, agent availability and concurrency on the platform, because it is the destination. Outbound this is a normal outcome. The code alone does not assign ownership; read the SIP flow | -| in 5360 / 4350 | platform 503 / 480 | platform config | platform status; fallback URI; TLS consistency | -| in 4420 carrier_cancelled | caller CANCEL, 487 (a racing platform 404 gets the 4090 fix) | caller | nothing; answer faster if ringing is long | -| any 4590 domestic_anchored_terms_not_met | 403 from Plivo, no platform leg on inbound (observed; 403 is documented for 4650, not for this code) | Plivo policy (India) | the SIP trunking page's cause is that the orchestration platform does not terminate SIP and media in India; the Voice API pages phrase the same rule as both legs staying in India. Move the platform endpoint and both legs into India. KYC is a separate gate, so check it too, but this code does not report it | -| any 4030 insufficient credits | from Plivo | customer Plivo config | credits, auto-recharge | -| in 4310 uri_not_found | 404 from Plivo | customer Plivo config | set `primary_uri_uuid` | -| out, flow ends at 407 (any code) | 407 with no second INVITE | platform config | credential username (not name) and password, or the platform IPs in the IP list | -| out 4000 bad_request | 400 | platform config per the docs | inspect the packet, do not retry without fixing. Commonly seen, not documented: the 400 arrives after 183 ringing; check the flow before assuming a syntax error, and collect the UUID for Plivo support | -| out 4190 unknown_caller_id | 403 from Plivo | platform config | caller ID = Plivo number on this account | -| out 4560 / 4570 barred_country / barred_number (docs also list 4650) | 403 Barred | customer Plivo config (geo permissions) | console Zentrunk, Geo Permissions | -| out 4100 prefix_not_supported | 404 Prefix Not Supported | platform config (dial list) | E.164 with country code | -| out 4410 / 4340 / 4550 | 486 / 480 / platform CANCEL | carrier or platform timeout | normal outcomes; very short 4550 cancels count as abandoned calls (US) | -| out 4090 / 4160 / 5000 / 5300 / 5350 / 6000 / 6040 / 4630 / 4370 | 404 / 503 / 488 / 482 | carrier/destination | retry; Plivo support if concentrated on one destination | -| out 5180 cps_limit_reached | 503 from Plivo | platform config (pacing) | pace the dialer; the docs route a CPS increase through the console assistant rather than a support ticket | -| any 5220 service_interrupted_by_customer | 200 then platform 4xx (or no answer to a mid-call request) | platform config | platform logs at the drop time; re-INVITE and UPDATE handling | -| any 3040 abnormal_hangup_due_to_reinvite | re-INVITE rejected | platform config | what changed mid-call. The published row says only "Re-invite rejected, check for changes in session parameters during call"; the transferred-leg association is observational, so collect Call-ID and PCAP for Plivo support rather than assuming a transfer caused it | -| transfer: `NOTIFY 4xx/5xx` (docs examples 486, 408; 480 and 503 also seen. No hangup code; the agent leg stays up) | REFER 202, then the sipfrag | transfer target, or geo permissions | agent speaks at once, retries or continues | -| transfer: `NOTIFY 100 Trying` then nothing | agent leg ended early | platform config | keep the leg up until `NOTIFY 200` or a failure | - -## What not to do - -- Don't leave `;transport=` off the URI because LiveKit happens to answer over UDP. Write what the platform documents. -- Don't skip the platform-side import because the Plivo side is green. The platform's 404 is the inbound failure this file sees most, though nothing in the docs ranks causes. -- Don't paste an answer URL or a `sip:` URI into the origination URI field for LiveKit, ElevenLabs, Retell or Vapi. The URI checklist refuses both. -- Don't whitelist `0.0.0.0/0` (or any `/0`) in an IP list to get outbound working; use credentials. The docs do not warn about this explicitly, so it is this file's rule, not a Plivo one. -- Don't assume `secure: true` on the trunk means the signalling is encrypted. With Secure Trunking enabled Plivo uses SRTP for media, and the page says SRTP works regardless of whether SIP signalling is on TCP or UDP. If you want the signalling encrypted too, turn TLS on in the platform and use `transport=tls`. -- Don't run an Indian number without a linked, accepted KYC application, and don't dial outside India from it. -- Don't put numbers without a country code in the dial list (4100). -- Don't read a 407 as an auth failure. On a credential-authenticated trunk, one 407 followed by a second INVITE is the digest handshake. An IP ACL trunk is not challenged and shows no 407, so do not treat a missing 407 as a fault either. -- Don't treat outbound 4410, 4340 and 4550 as configuration problems; they are normal outbound outcomes. Inbound 4410 points at the platform because the platform is the destination, but the published table says only `user_busy`, so confirm with the SIP flow before assigning blame. Do treat a 4550 within a second or two as a dialer timeout bug. -- Don't retry a 4000 without reading the SIP flow first. - -## India readiness for SIP trunking agents - -Treat every item as blocking until proved. The items below are what a SIP trunk needs; each is enough to work from on its own. - -1. The business is registered in India and the Plivo organisation uses the **India data region**. The data region cannot be changed; create a new organisation if needed. Not readable over the API: confirm in the console. (, ) -2. A compliance (KYC) application in `accepted` status is attached to the number. Draft, submitted, rejected, suspended, expired or missing is not enough; renting and calling both need `accepted`. (, ) - - The whole flow runs from the CLI, and one rule governs it: `plivo numbers compliance requirements --country IN --number-type local --user-type business -o json` returns the document types required right now, and you supply exactly those. The public pages disagree on how many documents are needed, so never hard-code one or two: the live requirements response is the source of truth. The certificate files and the exact legal details (legal business name as printed on the certificate, CIN or Udyam registration number, GSTIN, registered address, contact email) come from the user and you must never fill them in or invent them. The first application must be sealed and signed by an authorised signatory. Then, previewing each writing step first: - - ```bash - plivo numbers compliance requirements --country IN --number-type local --user-type business -o json # read-only - plivo numbers compliance create --data @app.json --file documents[0].file=@first_document.pdf --dry-run - plivo numbers compliance create --data @app.json --file documents[0].file=@first_document.pdf --yes -o json - plivo numbers compliance get --expand documents -o json # read-only; poll until it leaves "submitted" - plivo numbers compliance link --link +9180XXXXXXXX= --dry-run - plivo numbers compliance link --link +9180XXXXXXXX= --yes - plivo numbers compliance list --country IN --status accepted -o json # read-only; confirms the state - ``` +### URI checks - Review for 022 and 080 landline numbers is automated and usually takes a few minutes. `submitted` is not `accepted`. If a create is rejected, read `rejection_reason`, fix the document or the field and run `compliance update`, which replaces every document, so re-attach them all. A compliance application is a regulatory filing: submit it only when the user has explicitly said go. -3. The caller ID is a Plivo-rented India number. Using one on the same account is this file's recommendation, not a rule the India page states. -4. Plivo and the AI platform terminate SIP and media in India; violation fails with **4590**. The Voice API name for the same rule is `violates_media_anchoring`; SIP trunking uses 4590. () -Items 5, 6 and 8 come from the Voice API India pages, not from any SIP trunking page: the SIP trunking India checklist covers account region, a KYC'd number, platform region, the trunk URI and a test call. They are number-level regulatory rules, so they still apply to a number on a trunk, but say where they come from. +Breaks or misroutes calls: -5. Number series: landline (022, 080) for service and transactional calls only; 140 for promotional calls only; 160 for BFSI service and transactional calls only. Using the wrong series is itself a violation, and complaints from such calls count as UCC even with consent. 140 and 160 numbers are not provisioned through the compliance API: they go through Tata DLT registration, a signed declaration, a NOC per number and voice header and template approval, which takes several business days and runs through Plivo support. There is no CLI for that path. -6. Explicit digital consent for every commercial call; cold calling is prohibited; complaints are treated as UCC. Complaints arrive on the console UCC dashboard and through the UCC API (`plivo api GET /Ucc/`, `plivo api GET /Ucc//`, and `plivo api POST /Ucc//` with `--dry-run` then `--yes` to submit proof; there is no typed CLI command). Opt-in proof is due within a few business days of a complaint, unresolved complaints block the compliance application, and repeated complaints suspend it. Remove a complainant from the list at once; calling them again is itself a violation. Read for the current timers rather than quoting one from memory. -7. Platform support: LiveKit via region pinning; ElevenLabs via an India deployment and `sip.rtc.in.residency.elevenlabs.io:5060;transport=tcp`; **Vapi not supported**; Retell: confirm with Retell, Plivo's docs do not verify it. -8. Inbound to India: India to India. Outbound: Indian number to an Indian destination. +- A URL (`http://`, `https://`, any path): Plivo needs the SIP endpoint, not an answer URL. +- A space, no host, a host with characters other than letters, digits, dots and hyphens, a non-numeric port, or `transport=` other than `udp`, `tcp` or `tls`. +- A private or unroutable host (10.x, 127.x, 192.168.x, 172.16-31.x, 169.254.x, 0.x). +- A host that differs from the documented host for the named platform. +- An Indian number with a non-India endpoint (4590). -Commonly seen, not documented, and safe to check: - -- The documented cause of 4590 is a call leg outside India, nothing else. Check KYC as its own gate rather than inferring it from this code. -- LiveKit India endpoints: use the region-based endpoint LiveKit gives you after region pinning. Copy it; do not compose a hostname from parts. - -The readiness checklist proves the number's state from the Number API, and `plivo numbers compliance list --country IN --status accepted` proves the application state. It cannot infer the organisation's data region or where the platform terminates media; those are manual confirmations. `compliance_status` on the number record is returned by the live API but is not in the published phone number schema, so treat its absence as "not known". - -## Security and limits: geo permissions, caller ID, secure trunking, IP lists, CPS - -Docs: , , , , , , . - -### Geo permissions (outbound) - -- Console only: Zentrunk, Geo Permissions. All countries allowed by default; deselect the ones you never call; changes apply immediately. No API endpoint is documented, so `plivo api` cannot read or set it. -- High Risk Permissions toggle (on by default) blocks premium and high-risk prefix groups. -- A blocked destination fails with SIP `403 Barred Country`; the geo-permissions page says code **4650**, the hangup table lists **4560 `barred_country`** and 4650. Call records commonly show 4560. 4570 `barred_number` is the per-number block. -- For an agent that calls one country, turn every other country off before go-live. A leaked credential then costs one country's rates. - -### Caller ID and STIR/SHAKEN (US and Canada) - -- Caller ID must be a Plivo number you rent, or a verified caller ID, else **4190 `unknown_caller_id`**. A number rented on the same account is the form that also attests A / Verified, so prefer it. -- STIR/SHAKEN is automatic. A call with a Plivo DID from the same account is signed A / Verified; anything else is B or C / Not Verified; non-US destinations show Not Applicable. Toll-free numbers may show a misleading status. -- Call record fields: `stir_verification`, `attestation_indicator` on `GET /Zentrunk/Call//`. - -### US call quality and CPS - -- Default 2 CPS. **SIP trunking rejects attempts above the CPS limit with 5180; it does not queue them.** Pace the dialer. The technical-specifications page says trunk-level overflow fails and account-level overflow queues; this skill follows the voice-agents page and says pace either way. -- Raise the CPS limit through Plivo support. -- US quality thresholds (abandoned and short calls) are on the US call quality page; read it for the current numbers. - -### Secure trunking (TLS plus SRTP) - -- Trunk field `secure=true` = TLS signalling and SRTP media for that trunk. Mirror it on the platform: LiveKit secure trunking, Retell Outbound Transport = TLS, ElevenLabs TLS URI on 5061. -- Inbound: `:5061;transport=tls` in the URI. Outbound: the platform dials `.zt.plivo.com` over TLS 5061. -- Mismatch: **4110 `secure_trunking_disabled`** is documented for one direction only, TLS or SRTP used against a trunk that does not have Secure Trunking enabled. A secure-flagged trunk dialled over UDP still gets SRTP on the media, and the reverse mismatch has no documented outcome, so treat either mismatch as a warning to fix rather than a predicted code. - -### IP lists and credentials - -- An IP ACL is a source-IP whitelist for the outbound trunk. `0.0.0.0/0`, `0.0.0.0` or `128.0.0.0/1` means anyone who learns the trunk domain can place calls on your account; the docs do not warn about this explicitly, so block it yourself. -- Credential rules: username 5 to 20 alphanumeric; password 5 to 20 chars, alphanumeric plus `~!@#$%^&*()_+`, at least one special character. The password is never returned. Write it to a file and pass `--body @file`; never on a command line or in chat. -- Use the credential **username** on the platform, not the credential name. -- Plivo signalling and media: ports 5060 (UDP/TCP), 5061 (TLS), media 10000 to 30000 (the hangup table's 3020 row says 10000 to 20000; use the wider range). Signalling CIDRs per region are on the technical-specifications page; Mumbai and Singapore are listed as regions but have no CIDR row there, so India-region customers should ask Plivo support rather than guess. - -### Not available over the API (console or support only) - -Geo permissions, CPS increases, premium-number unblocking, the SIP flow and PCAP (console Zentrunk, Logs), the account data region. - -## Platforms: what goes in the Plivo URI, how the platform authenticates, what to do on its side - -Anything not in Plivo's docs is marked **not in docs**. - -| Platform | Inbound URI (create at `/Zentrunk/URI/`) | India | Outbound auth | `secure` | Platform-side step, inbound | Platform-side step, outbound | -|---|---|---|---|---|---|---| -| **LiveKit Cloud** | `;transport=tcp` (your project's SIP endpoint). `;transport=tls` for secure trunking | Enable region pinning on the project and use the region-based endpoint LiveKit gives you; copy it, do not compose it | Credentials. The docs' API example sets `"secure": true`; then enable secure trunking in LiveKit too | recommended | LiveKit **inbound trunk** listing the Plivo number and a **dispatch rule** | LiveKit outbound trunk with address `.zt.plivo.com`, the credential username and password | -| **LiveKit self-hosted** | `[:5060];transport=tcp` or `:5061;transport=tls` | Your servers must be in India | Credentials or an IP list of your egress IPs | both sides must agree | Allow Plivo signalling IPs; if your server challenges Plivo, set the same username and password on the Plivo URI (`authentication_needed`), else 4150 | same as Cloud | -| **ElevenLabs** | `sip.rtc.elevenlabs.io:5060;transport=tcp` or `sip.rtc.elevenlabs.io:5061;transport=tls` | `sip.rtc.in.residency.elevenlabs.io:5060;transport=tcp` after ElevenLabs sets up an India deployment | Credentials; secure trunking recommended | recommended | Import the Plivo number on a SIP trunk and link an agent. A `404 Does not match any SIP Trunks` means this step is missing | Termination domain `.zt.plivo.com` plus the credentials in the number's outbound settings | -| **Retell** | `sip.retellai.com;transport=tcp` (or `;transport=tls`, TLS 1.2+) | Plivo's docs do not verify an India endpoint; confirm with Retell first | Outbound trunk **required**: Retell will not import a number without its termination URI, even inbound only. The import form's user name and password fields are not marked required and the API example omits the password, so create the credential but do not claim the import fails without it | off by default; if on, set Outbound Transport = TLS in Retell | Import the number (Connect via SIP trunking) with termination URI `.zt.plivo.com` (no `sip:`), username, password, transport; bind an inbound agent | Bind an outbound agent. Retell cannot edit an imported number: delete and re-import | -| **Vapi** | `sip.vapi.ai;transport=udp` | **Not supported** | IP list `44.229.228.186/32`, `44.238.177.138/32` | no | Register the number in Vapi (BYO SIP trunk) and assign an assistant | Vapi dials `.zt.plivo.com` from the two IPs | -| **Other or self-hosted** | The platform's SIP host and the transport **it** documents: `host[:port];transport=...`. The generic integration guide's TLS example uses port 5061 and the technical specifications list 5061 for TLS, so prefer 5061 when the platform does not say otherwise. Some platform guides publish a TLS URI with no port at all, so do not treat 5061 as mandatory: write what that platform's own Plivo integration guide shows. The API also accepts `sip:user@host` | Must terminate SIP and media in India | Credentials if it supports digest auth or has dynamic IPs; IP list only for published static IPs | both sides must agree | Register the Plivo number and route it to an agent | Termination domain plus credentials; caller ID = a Plivo number on the account; OPTIONS pings at most one per 10 to 15 s, to the outbound trunk only | - -Sources: , , , , , , . - -### Platforms Plivo does not document - -Plivo publishes SIP trunking integration guides for LiveKit, ElevenLabs, Retell and Vapi, plus one generic guide for everything else. Some platforms customers ask about, including OpenAI Realtime and xAI, have no Plivo guide at all. - -For those, say so plainly, then follow the "Other or self-hosted" row. Get the SIP host, the port and the transport from that platform's own documentation, and get the corresponding Plivo-side requirements from Plivo. Do not invent a hostname, a transport, or an API field to carry a value the Origination URI API does not document: `/Zentrunk/URI/` documents `uri`, `authentication_needed`, `username` and `password`, and nothing else. If a platform needs something outside that set, that is a question for Plivo before it is a request body. - -### Transport parameter - -- "The transport parameter must match what your platform expects. A mismatch is the most common reason an inbound integration fails silently." () -- LiveKit, ElevenLabs, Retell: TCP by default, TLS for secure trunking. Vapi: UDP. No `;transport=` means UDP. -- LiveKit commonly answers over UDP when the parameter is missing, so treat that as a warning rather than a blocker; write what the docs say for new setups. A missing transport parameter is a common cause of inbound no-reply failures (4170). -- The SIP trunking API page shows `sip.livekit.cloud:5060` and `sip.vapi.ai:5060` without a transport and Vapi with `authentication_needed: true`; the integration guides disagree and are what this skill follows. - -### Outbound authentication and the digest handshake - -Prefer credentials for LiveKit Cloud, ElevenLabs and Retell; use Vapi's two published IPs for Vapi. An IP list containing `0.0.0.0/0` lets anyone who learns your trunk domain place calls on your account. The IP ACL page publishes no prohibition, so this is this file's rule, not a Plivo one. +Fix before go-live: -The docs describe the handshake in words: the trunk challenges the INVITE and the platform's configured auth section answers it (). On the wire that is Plivo answering the INVITE with 407 and the platform re-sending it with credentials. The 407 code and the two-INVITE sequence are commonly seen, not written down in Plivo's docs. A flow that **ends** at 407 means the platform never authenticated; the hangup code recorded for that varies, so read the SIP flow. Fix: the credential username (not its name) and password on the platform, or add the platform's egress IPs to the IP list. +- No `;transport=`, or one the platform does not document. Plivo's generic guide calls a transport mismatch the most common reason an inbound integration fails silently, and Plivo documents no default, so always set what the platform documents; LiveKit may answer over UDP without it, but do not rely on that. +- A port the platform's guide does not use, or 5061 without `transport=tls`. Some guides publish a TLS URI with no port: write what the guide shows. +- A `sip:` or `sips:` scheme or a user part where the platform's guide shows the host-first form (xAI's `sip:` form is fine). +- Any parameter other than `transport=`, a parameter with no value, or a host ending in a dot. +- A platform with no documented host and transport to compare against: get both before creating the URI. -### Questions to ask because Plivo cannot see them +## Stage 3: inbound, platform side -| Platform | Ask | -|---|---| -| LiveKit | Does the LiveKit inbound trunk list `+` exactly, with a dispatch rule? Region pinning on for Indian numbers? | -| ElevenLabs | Is `+` imported on a SIP trunk with an agent? For India, was the trunk created against the `in.residency` host? | -| Retell | Is `+` imported with termination URI `.zt.plivo.com` and the credential username, and is an inbound agent bound? | -| Vapi | Is `+` registered in Vapi with an assistant? (Indian numbers will not work.) | -| A platform Plivo does not document | What SIP host, port and transport does it publish, and what does it need on the Plivo side? Confirm both with the platform and with Plivo before creating anything. | -| Self-hosted | Is Plivo allowed through your firewall on 5060/5061 and UDP 10000 to 30000, and does your server accept INVITEs from Plivo without a digest challenge, or are the same credentials on the Plivo URI? | +Ask about and confirm the matrix's platform-side column. When it is missing the platform answers **404** (ElevenLabs: `Does not match any SIP Trunks`) and the record usually shows 4090 `destination_not_found`, whose published row has a carrier framing, so read it with the SIP flow. A missing LiveKit dispatch rule or no available agent usually shows as a platform 486, recorded as 4410 (observed). -## Request bodies for `plivo api`, one subsection per platform +Self-hosted: allow every Plivo signaling range on 5060 (UDP/TCP) and 5061 (TLS), and media on 10000-30000, from `plivo docs show sip-trunking` (https://plivo.com/docs/sip-trunking, "Signaling / Media IP addresses"). A server that digest-challenges Plivo without the same credentials on the URI never completes the call. Do not promise 4150 for it: that published row is a carrier requiring proxy auth. -Usage: write the body to a file, then `plivo api POST /Zentrunk/URI/ --body @uri.json --dry-run`, then the same with `--yes` once the user approves. Replace every `<...>` placeholder first. Passwords: edit the file, never paste them on a command line. Order: URI, inbound trunk, attach the number; credential or IP list, outbound trunk, then GET the trunk to read `trunk_domain`. Fields come from the API reference: , , , . +## Stage 4: outbound -Attaching the number is the same for every platform. The Number API's `app_id` accepts an inbound `trunk_id`; there is no `--trunk-id` flag. Pass the number as digits without `+`. +Credential rules: username 5 to 20 alphanumeric characters; password 5 to 20 characters from alphanumerics and `~!@#$%^&*()_+`, with at least one special character. ```bash -plivo numbers get -o json # note the current application for rollback -plivo numbers update --app-id --dry-run # preview -plivo numbers update --app-id --yes # only after the user approves -plivo numbers get -o json # confirm it now points at the trunk -``` - -### LiveKit - -URI, TCP (the usual choice), TLS for secure trunking, and the India form (region-pinned endpoint from your project; Plivo publishes no India hostname): - -```json -{ - "name": "livekit-primary", - "uri": ";transport=tcp" -} -``` - -```json -{ - "name": "livekit-primary-tls", - "uri": ";transport=tls" -} -``` - -```json -{ - "name": "livekit-india", - "uri": ";transport=tcp" -} -``` - -```json -{ - "name": "livekit-inbound", - "trunk_direction": "inbound", - "trunk_status": "enabled", - "primary_uri_uuid": "" -} -``` - -```json -{ - "name": "livekit-outbound-credential", - "username": "<5 to 20 alphanumeric chars>", - "password": "<5 to 20 chars, alphanumeric plus ~!@#$%^&*()_+ with at least one special char; write it here, never on a command line>" -} +printf '%s' "$SIP_PASSWORD" | plivo sip credentials create --name --username --password-stdin --dry-run # preview redacts it; then without --dry-run +plivo sip ip-acl create --name --ip /32 --ip /32 --dry-run # instead of a credential, for published static IPs (Vapi) +plivo sip trunks create --name --direction outbound --credential --dry-run # or --ip-acl ; --secure for TLS and SRTP; prints trunk_domain ``` -```json -{ - "name": "livekit-outbound", - "trunk_direction": "outbound", - "trunk_status": "enabled", - "credential_uuid": "", - "secure": true -} -``` - -### ElevenLabs - -```json -{ - "name": "elevenlabs-primary", - "uri": "sip.rtc.elevenlabs.io:5060;transport=tcp" -} -``` - -```json -{ - "name": "elevenlabs-primary-tls", - "uri": "sip.rtc.elevenlabs.io:5061;transport=tls" -} -``` - -```json -{ - "name": "elevenlabs-india", - "uri": "sip.rtc.in.residency.elevenlabs.io:5060;transport=tcp" -} -``` - -```json -{ - "name": "elevenlabs-inbound", - "trunk_direction": "inbound", - "trunk_status": "enabled", - "primary_uri_uuid": "" -} -``` - -```json -{ - "name": "elevenlabs-outbound-credential", - "username": "<5 to 20 alphanumeric chars>", - "password": "<5 to 20 chars, alphanumeric plus ~!@#$%^&*()_+ with at least one special char; write it here, never on a command line>" -} -``` - -```json -{ - "name": "elevenlabs-outbound", - "trunk_direction": "outbound", - "trunk_status": "enabled", - "credential_uuid": "", - "secure": true -} -``` - -### Retell - -Retell needs the credential and the outbound trunk even when you only want inbound calls. `secure` is false here because Retell defaults to TCP; set it true only together with Outbound Transport = TLS in Retell. - -```json -{ - "name": "retell-primary", - "uri": "sip.retellai.com;transport=tcp" -} -``` - -```json -{ - "name": "retell-primary-tls", - "uri": "sip.retellai.com;transport=tls" -} -``` - -```json -{ - "name": "retell-inbound", - "trunk_direction": "inbound", - "trunk_status": "enabled", - "primary_uri_uuid": "" -} -``` - -```json -{ - "name": "retell-outbound-credential", - "username": "<5 to 20 alphanumeric chars>", - "password": "<5 to 20 chars, alphanumeric plus ~!@#$%^&*()_+ with at least one special char; write it here, never on a command line>" -} -``` - -```json -{ - "name": "retell-outbound", - "trunk_direction": "outbound", - "trunk_status": "enabled", - "credential_uuid": "", - "secure": false -} -``` - -### Vapi - -Vapi authenticates by source IP, not by credential, and the two addresses are exactly as published. Indian numbers do not work with Vapi. - -```json -{ - "name": "vapi-primary", - "uri": "sip.vapi.ai;transport=udp" -} -``` - -```json -{ - "name": "vapi-inbound", - "trunk_direction": "inbound", - "trunk_status": "enabled", - "primary_uri_uuid": "" -} -``` - -```json -{ - "name": "vapi-source-ips", - "ip_addresses": [ - "44.229.228.186/32", - "44.238.177.138/32" - ] -} -``` - -```json -{ - "name": "vapi-outbound", - "trunk_direction": "outbound", - "trunk_status": "enabled", - "ipacl_uuid": "", - "secure": false -} -``` - -### Other or self-hosted - -Use the host and transport the platform documents. The fallback URI is used when the primary is unreachable or returns an error. An IP list is only for published static egress IPs; use a credential for anything dynamic. - -```json -{ - "name": "platform-primary", - "uri": "[:port];transport=" -} -``` +Give the platform the `trunk_domain` (no `sip:`, no spaces), the credential **username** (not its name) and the password. If the platform sends SIP OPTIONS health checks, point them at the `trunk_domain` only, at most one every 10 to 15 s; more may get blocked. Caller ID: a Plivo number on this account; anything else risks 4190 `unknown_caller_id`, and a same-account number gets STIR/SHAKEN attestation A (`plivo docs show sip-trunking/concepts/stir-shaken`, https://plivo.com/docs/sip-trunking/concepts/stir-shaken). -```json -{ - "name": "platform-fallback", - "uri": ";transport=" -} -``` - -```json -{ - "name": "platform-inbound", - "trunk_direction": "inbound", - "trunk_status": "enabled", - "primary_uri_uuid": "" -} -``` - -```json -{ - "name": "platform-inbound", - "trunk_direction": "inbound", - "trunk_status": "enabled", - "primary_uri_uuid": "", - "fallback_uri_uuid": "" -} -``` - -```json -{ - "name": "platform-outbound-credential", - "username": "<5 to 20 alphanumeric chars>", - "password": "<5 to 20 chars, alphanumeric plus ~!@#$%^&*()_+ with at least one special char; write it here, never on a command line>" -} -``` - -```json -{ - "name": "platform-source-ips", - "ip_addresses": [ - "/32", - "/32" - ] -} -``` - -```json -{ - "name": "platform-outbound", - "trunk_direction": "outbound", - "trunk_status": "enabled", - "credential_uuid": "", - "secure": false -} -``` - -```json -{ - "name": "platform-outbound", - "trunk_direction": "outbound", - "trunk_status": "enabled", - "credential_uuid": "", - "ipacl_uuid": "", - "secure": false -} -``` - -### A platform Plivo does not document - -There is no fifth set of request bodies here, on purpose. Plivo publishes SIP trunking integration guides for LiveKit, ElevenLabs, Retell and Vapi, plus one generic guide for anything else. For a platform with no Plivo guide, including OpenAI Realtime and xAI, use the "Other or self-hosted" bodies above and fill them in like this: - -1. Ask the platform for the exact SIP host, port and transport it expects for inbound calls, and for the source IPs or credentials it uses outbound. Copy those values; do not compose a hostname from parts. -2. Check them against the URI checklist in stage 2, which is platform-independent: a bare host, a numeric port if any, `;transport=` set to `udp`, `tcp` or `tls`, no scheme unless the platform asks for the `sip:user@host` form the API documents, and no private address. -3. Create the URI with `--dry-run` first, then run it again with `--yes`, then `GET /Zentrunk/URI//` and read back exactly what was stored before you attach a trunk or a number. -4. If the platform asks for something the Origination URI API does not document (`uri`, `authentication_needed`, `username`, `password` are the documented fields), stop and ask Plivo. Do not send an undocumented field and do not tell the customer one exists. -5. Tell the customer plainly that Plivo does not publish a guide for their platform, so the Plivo side is the generic path and the platform side is theirs to confirm. - -## Transfer to a human with SIP REFER - -Docs: , , , . - -### How it works (same for inbound and outbound calls) - -1. The call is up between the caller and your platform over the Plivo trunk. -2. Your platform sends `REFER` with `Refer-To: .zt.plivo.com>`. -3. Plivo answers `202 Accepted`, then `NOTIFY 100 Trying`, `180 Ringing` while the target rings. Answer every NOTIFY with `200 OK`. -4. Target answers: `NOTIFY 200 OK`; Plivo bridges caller and target and sends your leg a `BYE`. -5. Target busy or no answer: `NOTIFY 4xx/5xx`. The documented examples are `486 Busy Here` and `408 Request Timeout`; `480` and `503 Transfer Failed` are also commonly seen but are not in Plivo's docs. The caller is still on your leg; retry another number or keep talking. +`--secure` means TLS signaling and SRTP media on that trunk, so the platform must dial over TLS too. TLS or SRTP against a non-secure trunk ends 4110 `secure_trunking_disabled`; a secure trunk with the platform on TCP drops calls or loses audio (Retell guide). -**Do not send BYE before `NOTIFY 200 OK`**; it drops the caller mid-transfer. +Digest handshake (observed): INVITE, 407, then a second INVITE with credentials is normal on a credential trunk; an IP ACL trunk is not challenged and shows no 407. A flow that **ends** at 407, or 4180 `call_rejected_unauthorized`, means a wrong username or password, or a platform source IP missing from the IP ACL. Never open the list to `0.0.0.0/0` (or any `/0` or `/1`) to make it work: anyone who learns the `trunk_domain` could then call on the account. The CLI warns on wide ranges but does not block them. -### Rules (the readiness check for a transfer) +## Stage 5: first call, both directions -- `Refer-To` must use **your** trunk domain, ending in `.zt.plivo.com`. Other domains and arbitrary SIP URIs are blocked. -- No private IP targets. -- The target is an E.164 number reachable through your trunk; a missing or malformed `Refer-To` gives `400 Bad Request`. -- Works on UDP, TCP and TLS trunks. -- Plivo handles codec re-negotiation between the legs. -- Caller ID on the transfer leg: inbound calls show the original caller's number; outbound calls show your Plivo number. -- Chained transfers allowed; each REFER replaces the previous transfer leg. -- **If the transfer target hangs up, the whole call ends.** The caller is not reconnected to your endpoint. -- International targets must be enabled in Geo Permissions. -- Two call records: the original leg and the transfer leg; allow up to 60 s for the transfer record. -- Outbound prerequisite: at least two Plivo numbers (customer leg caller ID and transfer destination). -- REFER is sent by your endpoint. `NOTIFY` is listed as not supported on the technical-specifications page, which means Plivo does not accept NOTIFY from you; Plivo does send NOTIFY to report progress. - -The documented outbound target form is `sip:+E164@.zt.plivo.com`. Write that. Some endpoints emit `tel:+E164` or `sips:+E164@` instead; Plivo publishes nothing about those, so do not assume either works. - -### Design rules for the agent - -- After a `NOTIFY 4xx/5xx` the caller hears silence until the agent speaks. Callers commonly hang up within seconds. Make the agent say something ("I could not reach a colleague, let me help") as soon as the sipfrag is a failure, then retry or continue. -- A dialog that ends after `NOTIFY 100 Trying` means the agent leg was dropped before a result. Keep the leg up until `NOTIFY 200` or a failure. -- Hold music while the target rings: play it from your endpoint before sending REFER; the caller otherwise hears the target's ring-back. -- If DTMF is not recognised after a transfer to an IVR, custom `X-` headers do not reach the target, or a re-INVITE on the transferred leg is rejected (3040, 5220): collect the Call-ID and the PCAP from the console SIP logs and contact Plivo support. Plivo's documented custom-header contract is the Voice API `X-PH-` prefix (), not a SIP trunking page. - -### Platform notes (their docs, not Plivo's; not verified here) - -- LiveKit: `TransferSIPParticipant` sends the REFER; give it the trunk-domain form. -- ElevenLabs: the transfer-to-number tool over SIP trunking sends REFER through the same trunk. -- If your platform can only transfer by dialling a new call, the alternative is a warm transfer inside the platform (a second outbound call through the same trunk, bridged by the agent). That costs a second billed leg and keeps the AI in the media path. - -### Troubleshooting - -| Symptom | Check | -|---|---| -| `403 Forbidden` to REFER | `Refer-To` domain is not your `.zt.plivo.com` trunk domain, or a private IP, or the country is barred in Geo Permissions | -| `400 Bad Request` | `Refer-To` missing or not `sip:+E164@.zt.plivo.com` | -| `202` then the caller is dropped | your endpoint sent BYE before `NOTIFY 200 OK` | -| `202` but no NOTIFY arrives | your endpoint is not listening on the address it advertised in `Contact` | -| `NOTIFY 4xx/5xx` (documented examples 486, 408; 480 and 503 also commonly seen) | target busy, unreachable or not answering; the caller hears silence until the agent speaks | -| `NOTIFY 100 Trying` then nothing | the agent leg ended early; keep it up until a final NOTIFY | -| No audio after transfer | codec mismatch between legs; Plivo support with the Call-ID | - -## The Zentrunk API through `plivo api` (exact paths and fields) - -The CLI has **no typed SIP trunk commands**: no `plivo sip ...`, no trunk, URI, credential or IP-list command, no geo-permissions command. Every trunk object goes through the generic escape hatch `plivo api `. Account-scoped paths such as `/Zentrunk/Trunk/` expand to `/v1/Account//Zentrunk/Trunk/`. POST, PUT, PATCH and DELETE need `--yes`; `--dry-run` previews without sending. JSON output is `{"data": ...}`; list endpoints return `data.meta` and `data.objects`; a single trunk comes back under `data.object`. Page size 1 to 20 (`--query limit=20 --query offset=20`). The only typed commands this skill uses are `plivo numbers get|list|update|compliance ...`, `plivo account get`, `plivo auth whoami`. - -Docs: , , , , , . - -### Origination URI (`/Zentrunk/URI/`): where Plivo sends inbound calls - -| Field | Notes | -|---|---| -| `uri` | **required**. The API page documents the formats IP, IP:port, FQDN, FQDN:port and `sip:pbx@example.com` and never mentions `;transport=`. The `;transport=udp\|tcp\|tls` parameter is documented on the platform pages instead; the four AI platform guides use the host-first form without a scheme. | -| `authentication_needed` | default false. Set true only if the platform's inbound trunk challenges Plivo with a username and password. The page defines the field as "Whether Plivo should authenticate when sending calls" and gives no failure behaviour; the published 4150 row is a **carrier** requiring proxy auth, so do not promise that code here. | -| `username`, `password` | required when `authentication_needed=true`; password never returned | +Dial the number from a phone, then place one outbound call from the platform to your own phone (no outbound for xAI). ```bash -plivo api POST /Zentrunk/URI/ --body @uri.json --dry-run # preview, then run again with --yes after approval -plivo api GET /Zentrunk/URI/ -o json # read-only: data.objects[].uri_uuid, .uri -plivo api GET /Zentrunk/URI// -o json # read-only -plivo api POST /Zentrunk/URI// --body '{"uri":"sip.rtc.elevenlabs.io:5061;transport=tls"}' --dry-run # update: preview -plivo api POST /Zentrunk/URI// --body '{"uri":"sip.rtc.elevenlabs.io:5061;transport=tls"}' --yes # then apply -plivo api DELETE /Zentrunk/URI// --dry-run # preview: read the URI first and record it, this is not undoable -plivo api DELETE /Zentrunk/URI// --yes # a URI attached to a live trunk: inbound calls fail at once +plivo sip calls list --limit 5 -o json # hangup_cause_code, hangup_cause_name, hangup_source, transport_protocol, srtp +plivo sip calls get -o json +plivo api GET /Zentrunk/Call//Insights/ -o json # rtt, jitter, packet_loss, plivo_quality_score ``` -### Trunk (`/Zentrunk/Trunk/`) - -| Field | Notes | -|---|---| -| `trunk_direction` | **required**: `inbound` or `outbound`. Only inbound trunks attach to numbers. | -| `trunk_status` | `enabled` (default) or `disabled` | -| `secure` | default false; SRTP media plus TLS signalling. Mirror it on the platform. | -| `primary_uri_uuid`, `fallback_uri_uuid` | inbound; fallback is used when the primary is unreachable or returns an error | -| `credential_uuid`, `ipacl_uuid` | outbound: one of them required, both allowed | -| `trunk_id`, `trunk_domain` | read-only; `trunk_domain` = `.zt.plivo.com`, the termination domain the platform dials. GET the trunk to read it. | +`sip calls list` strips a leading `+` from `--from-number` and `--to-number`. `plivo voice calls` reads Voice API calls, not trunk calls. -```bash -plivo api POST /Zentrunk/Trunk/ --body @trunk-inbound.json --dry-run # preview, then --yes after approval -plivo api POST /Zentrunk/Trunk/ --body @trunk-outbound.json --dry-run # preview, then --yes after approval -plivo api GET /Zentrunk/Trunk/ --query trunk_direction=inbound --query limit=20 -o json # read-only -plivo api GET /Zentrunk/Trunk// -o json # read-only: data.object.trunk_domain, .trunk_status, .secure, .primary_uri_uuid -plivo api POST /Zentrunk/Trunk// --body '{"trunk_status":"disabled"}' --dry-run # disabling a trunk stops calls: preview, record the current status, then --yes -``` +**Pass:** 3000 or 3010 with a non-zero duration in both directions. A 3010 with 0 s ended at once without saying who ended it: read the platform logs and the SIP flow. -Errors on create: 400 invalid parameter, 401 bad auth, 422 missing conditional field (for example no `ipacl_uuid` or `credential_uuid` on an outbound trunk). +## Stage 6: transfer to a human with SIP REFER (optional) -### Credential and IP access control list (outbound auth) +- The platform sends REFER with `Refer-To: @>`. Only your own `.zt.plivo.com` `trunk_domain` works: other domains, arbitrary SIP URIs and private IPs get 403, and a missing or malformed `Refer-To` gets 400. `tel:` or `sips:` targets are undocumented: do not rely on them. LiveKit sends REFER from `TransferSIPParticipant` (LiveKit's docs): give it this form. +- Plivo answers 202 and reports progress with NOTIFY (100, 180, then 200 or 4xx/5xx); answer each with 200 OK. Never send BYE before `NOTIFY 200 OK`: Plivo sends the agent leg a BYE once the target answers. +- After a `NOTIFY 4xx/5xx` the caller is still on the agent leg, in silence: make the agent speak at once, then retry or continue. A dialog that ends after `NOTIFY 100` means the agent leg dropped too early. +- If the target hangs up, the whole call ends. International targets need Geo Permissions. Expect two call records (the transfer leg's can take up to 60 s). REFER legs count toward concurrency. Outbound transfers need two Plivo numbers. +- Flow, restrictions and troubleshooting: `plivo docs show sip-trunking/concepts/sip-refer-inbound` (https://plivo.com/docs/sip-trunking/concepts/sip-refer-inbound) and `plivo docs show sip-trunking/concepts/sip-refer` (https://plivo.com/docs/sip-trunking/concepts/sip-refer). -`username` 5 to 20 alphanumeric; `password` 5 to 20 chars, alphanumeric plus `~!@#$%^&*()_+`, at least one special character; never returned. The same username and password go into the platform's outbound trunk; use the **username**, not the credential name. `ip_addresses` is an array, required on create, and an update replaces the whole list. Never `0.0.0.0/0`. +## Go-live -```bash -plivo api POST /Zentrunk/Credential/ --body @cred.json --dry-run # write the file first; never put the password on the command line -plivo api POST /Zentrunk/Credential/ --body @cred.json --yes # after approval -plivo api GET /Zentrunk/Credential/ -o json # read-only: data.objects[].credential_uuid, .username (no password) -plivo api DELETE /Zentrunk/Credential// --dry-run # preview: this breaks any trunk using it -plivo api DELETE /Zentrunk/Credential// --yes # after approval -plivo api POST /Zentrunk/IPAccessControlList/ --body @ipacl.json --dry-run # preview, then --yes; an update replaces the whole list -plivo api GET /Zentrunk/IPAccessControlList// -o json # read-only: .ip_addresses -``` +- The readiness check passes for every direction in use, including the probe; warnings understood. +- Concurrency and CPS sized for peak. SIP trunking **rejects** calls above either limit (5180 CPS, 5190 concurrency) and never queues; every PSTN leg counts, inbound and outbound, shared with Voice API. Pace the dialer. Current limits and how to raise them: `plivo docs show sip-trunking/concepts/account-limits` (https://plivo.com/docs/sip-trunking/concepts/account-limits). US abandoned and short-call thresholds: `plivo docs show voice-agents/sip-trunking/deploy/us-call-quality-and-cps` (https://plivo.com/docs/voice-agents/sip-trunking/deploy/us-call-quality-and-cps). +- Geo Permissions (console: Zentrunk, Geo Permissions) trimmed to the countries the agent calls: `plivo docs show sip-trunking/concepts/geo-permissions` (https://plivo.com/docs/sip-trunking/concepts/geo-permissions). +- A fallback URI; a `secure` decision made on both sides; credentials never pasted anywhere; IP lists narrow. +- India: all four gates. US: Plivo caller ID, dialer under the CPS limit. +- Watch: `plivo sip calls list --hangup-cause-code 4090 -o json` (and 4170, 4180, 4590, 4030, 5180, 5190). Outbound 4410, 4340 and 4550 are outcomes, not faults, but every unanswered US attempt counts toward the abandoned-call threshold. -### Calls (`/Zentrunk/Call/`): call records and quality +## Debugging, in this order -```bash -plivo api GET /Zentrunk/Call/ --query limit=20 -o json # filters: call_direction, hangup_cause_code, hangup_source, from_number, to_number, end_time__gte -plivo api GET /Zentrunk/Call// -o json # hangup_cause_code, hangup_cause_name, hangup_source, transport_protocol, srtp, trunk_domain, stir_verification -plivo api GET /Zentrunk/Call//Insights/ -o json # rtt, jitter, packet_loss, plivo_quality_score -``` +1. `plivo sip calls list -o json` (filters: `--direction`, `--hangup-cause-code`, `--hangup-source`, `--from-number`, `--to-number`, `--since`, `--until`), then `plivo sip calls get -o json` and Insights. +2. `hangup_source`: `customer` = your platform, `carrier` = the network side, `zentrunk` = Plivo. Map the code with the table below. +3. Console: Zentrunk, Logs, the call. Call Stats shows trunk, transport and secure; SIP logs show the message flow, the final response and a PCAP download. The hangup code is Plivo's conclusion and the SIP flow is what the platform said; they can disagree. Reading the flow (observed): inbound with no INVITE to your URI usually means Plivo refused (4590, 4030, 4310); INVITE repeated with no reply usually ends 4170; otherwise the platform's own response is the answer (404 not imported, 401/407 it challenged Plivo, 486 no dispatch rule or agent, 503 down). +4. `plivo sip calls diagnose ` asks Plivo's assistant about one trunk call (`plivo voice calls diagnose` refuses trunk calls). If it cannot retrieve the call, use `plivo sip calls get`. It shares a small rate limit with `plivo ask`: do not loop it. Then Plivo support with the call UUID and the PCAP. -`plivo voice calls list|get` read the Voice API, not SIP trunking call records. `plivo voice calls diagnose ` is built for Voice API calls; whether it accepts a trunk call UUID is **not verified**. Try it once; it is rate limited together with `plivo ask`, so do not loop it. - -How `plivo numbers get` renders a trunk-attached number's `application` field is **not verified** on a live account; accept a URL containing `Trunk/`, a bare id, or an `Application/` URL, and say so when it is none of those. - -Not available over the API: geo permissions (console), CPS increases (Plivo support), the SIP flow and PCAP (console Zentrunk, Logs), the account data region. - -## Zentrunk hangup codes for AI agent calls: owner, fix, what the SIP flow shows - -Every row comes from the public table unless another page is named. - -Where to read a code: `plivo api GET /Zentrunk/Call// -o json` gives `hangup_cause_code`, `hangup_cause_name`, `hangup_source` (`customer` = your platform, `carrier` = the network side, `zentrunk` = Plivo). The final SIP response on the platform leg and the message flow are in the console: Zentrunk, Logs, the call, SIP logs (). Trust the hangup code for Plivo's conclusion and the SIP flow for what the platform said; the two can disagree. - -Owner values: **platform config** = the AI platform or your SIP server side; **customer Plivo config** = your trunk, URI, credential, number or account setup on Plivo; **Plivo policy** = Plivo refused on account or regulatory grounds; **Plivo** = Plivo-side handling; **carrier/destination** = the network or callee; **caller**; **expected outcome**. - -### Read the SIP message sequence in three lines - -- Inbound: Plivo's INVITE to your URI, then the platform's answer. No platform leg at all usually means Plivo refused the call itself (4590, 4030 or 4310). INVITE repeated with nothing back usually means 4170. Both readings come from SIP flows, not from a published mapping. -- Outbound on a credential-authenticated trunk: INVITE, 407, then a second INVITE with credentials is the digest handshake. The docs state it in words (the trunk challenges the INVITE, the platform's auth block answers it); the 407 code and the two-INVITE sequence are observed on the wire, not published values. A sequence that **ends** at 407 means the platform never authenticated; the hangup code recorded for that varies, so read the flow. A trunk authenticated by IP ACL is not challenged, so it shows no 407. -- Anything after 200 OK is a connected call; 3000 and 3010 are normal ends. - -### Codes common on AI agent trunks - -| Code | Direction | Wire (final on the platform leg) | Owner | Fix | -|---|---|---|---|---| -| 3000 | both | 200 then BYE | expected outcome | nothing (normal hangup by the carrier side) | -| 3010 | both | 200 then BYE | expected outcome | nothing. The published table says only `normal_hangup`, a normal hangup from the user; a 0 s duration means the call ended at once but does not name who ended it, so read the platform logs and the SIP flow | -| 4090 | inbound | platform **404**. Only ElevenLabs's phrase `Does not match any SIP Trunks` appears in Plivo's docs (); LiveKit `No trunk found` / `Does not match Trunks or Dispatch Rules` and Retell `Invalid destination` are the platforms' own phrases, commonly seen, not in Plivo's docs | platform config | import `+` on the platform, bind an agent or dispatch rule; the readiness checklist confirms the Plivo attachment | -| 4090 | outbound | 404 (sometimes 503) | carrier/destination | verify the E.164 destination; dial-list quality | -| 4170 | inbound | no response at all (INVITE repeated), or 100 then Plivo 408 | platform config | URI host, port, `;transport=`; platform online; reachability probe; firewall | -| 4150 | inbound | platform 401/407 | published row is **carrier** proxy auth; the inbound platform-challenge reading is observational | stop challenging Plivo, or `authentication_needed` with the same credentials on the URI | -| 4410 | inbound | platform 486 (`Rejected`) | platform config | dispatch rule, agent availability, concurrency | -| 4410 | outbound | 486 | carrier/destination | normal outcome; retry | -| 4420 | inbound | CANCEL from the caller side; a racing platform 404 gets the 4090 fix | caller | nothing, or answer faster | -| 4440 | inbound | 487 | Plivo | read the flow; Plivo support with the UUID | -| 5220 | both | 200 then a platform 4xx to a re-INVITE, or no answer (408) to a mid-call request | platform config | platform logs at the drop time | -| 5360 | inbound | platform 503 | platform config | platform status; add a fallback URI | -| 5000 | outbound | 503 | carrier/destination | retry; Plivo support if per-country | -| 4000 | outbound | **400**, or a sequence ending at 407 | platform config | inspect the packet; do not retry without fixing. Commonly seen, not documented: the 400 arrives after the far end returned 183 ringing; check the flow before assuming a syntax error, and if the INVITE is well formed collect the UUID for Plivo support | -| 4010 | outbound | 503 | carrier/destination | retry; Plivo support if repeated | -| 4040 | outbound | 503 | carrier/destination | check the number; Plivo support if systematic | -| 4100 | outbound | 404 `Prefix Not Supported` | platform config (dial list) | dial E.164 with the country code | -| 4160 | outbound | 503 | carrier/destination | retry | -| 4340 | outbound | 480 | carrier/destination | normal outcome | -| 4370 | outbound | 482 | carrier/destination | not dialling your own trunk number? else Plivo support | -| 4550 | outbound | platform CANCEL | platform config | normal if intended; very short cancels count as abandoned calls (US) | -| 4560 | outbound | Plivo 403 `Barred Country` | customer Plivo config (geo permissions) | console Zentrunk, Geo Permissions. The geo-permissions page documents **4650** for the same block | -| 4570 | outbound | Plivo 403 `Barred Number` | Plivo policy | drop the number, or Plivo support if legitimate | -| 4590 | both | Plivo 403 (inbound: refused before any platform leg), observed | Plivo policy (India) | the SIP trunking page's cause is that the orchestration platform does not terminate SIP and media in India; the Voice API pages phrase the same rule as both legs staying in India. Move the platform endpoint and both legs into India. Nothing published maps this code to compliance, so check KYC as its own gate rather than reading it out of this code | -| 4630 | outbound | 488 | carrier/destination | offer PCMU/PCMA, RFC 2833 DTMF; fix the secure flag if you offer SRTP only | -| 5300 | outbound | 404 | carrier/destination | retry | -| 5310 | outbound | mid-call carrier 4xx | carrier/destination | carrier issue; Plivo support if frequent | -| 5330 | outbound | 504 | carrier/destination | retry | -| 5350 | outbound | 503 | carrier/destination | retry later | -| 6000 | outbound | 503 or 486 | carrier/destination | retry later | -| 6040 | outbound | 503 | carrier/destination | retry; Plivo support if frequent | - -### Other codes in the public table (docs meaning) - -| Code | Cause | Fix | Owner | +| Code (name) | Dir | Seen on the wire (observed) | Fix | |---|---|---|---| -| 3020 | RTP timeout | network; allow RTP ports (the table says 10000-20000, the technical-specifications page says 10000-30000; use the wider range) | platform config | -| 3030 | Credits exhausted mid-call | add credits, auto-recharge | customer Plivo config | -| 3040 | Re-INVITE rejected | inspect changed session parameters; on a transferred leg collect the Call-ID and PCAP for Plivo support | platform config | -| 4020 | Authentication required | attach correct credentials | customer Plivo config | -| 4030 | Insufficient credits (`insufficient_plivo_credits`; call records may show `insufficient_credits`) | add credits. The published row carries no direction and no page says inbound trunk calls are billed, so treat a zero balance as a risk to inbound too rather than a documented cause | customer Plivo config | -| 4050 | Number blacklisted for verification | Plivo support | Plivo policy | -| 4060 | Endpoint authentication failed | correct endpoint credentials | platform config | -| 4070 | Trunk not found | use the exact `trunk_domain` | platform config | -| 4080 | No matching inbound route | correct incoming route settings | customer Plivo config | -| 4110 | Secure trunking disabled | `secure=true`, or non-secure transport on both sides | customer Plivo config | -| 4120 | Invalid destination format | E.164 | platform config | -| 4130 | Caller ID non-numeric | numeric E.164 Plivo number | platform config | -| 4140 | Caller ID too short | full E.164 Plivo number | platform config | -| 4180 | Invalid credential or unauthorised source | correct credentials or IP list | platform config | -| 4190 | Unknown caller ID | caller ID = a Plivo number on this account | platform config | -| 4200 | Do Not Originate caller ID | another outbound-capable number | platform config | -| 4220 | Unsupported media type | PCMU, PCMA, telephone-event | platform config | -| 4230 | Unsupported URI scheme | correct the SIP URI format | platform config | -| 4270 | Session interval too small | increase the session timer | platform config | -| 4310 | Origination URI missing | create a URI, set `primary_uri_uuid` | customer Plivo config | -| 4320 | Origination URI cannot resolve | fix DNS or the host | customer Plivo config | -| 4330 | Zentrunk temporarily unavailable | retry; check CPS | Plivo | -| 4350 | Callee unavailable (customer side, 480) | platform online; TLS consistency | platform config | -| 4360 | Call-ID mismatch | check duplicate Call-IDs | platform config | -| 4380 | Too many SIP hops | reduce proxy hops | platform config | -| 4430 | SDP rejected | correct SDP compatibility | platform config | -| 4500 | Request already pending | wait for the current request | platform config | -| 4520 | Security agreement required | implement the required security mechanism | platform config | -| 4540 | Zentrunk authentication failure | Plivo support | Plivo | -| 4580 | Invalid SIP packet | headers and request format | platform config | -| 4610 | Request URI rejected | correct URI format | platform config | -| 4620 | Codec rejected | offer PCMU or PCMA | platform config | -| 4640 | Endpoint rejected SDP | correct SDP for the platform | platform config | -| 4650 | Geo Permissions block (SIP 403 Barred Country) | enable the destination; Plivo support if geo permissions were disabled by Plivo | customer Plivo config | -| 5010 | Port capacity reached | request more capacity | Plivo | -| 5020 | Function not implemented | use a supported SIP method | platform config | -| 5030 | Carrier does not implement request | different request method | carrier/destination | -| 5040 | Endpoint does not implement request | update endpoint behaviour | platform config | -| 5180 | CPS limit reached (rejected, not queued) | pace within CPS; the docs route a CPS increase through the console assistant rather than a support ticket | platform config (pacing) | -| 5190 | Concurrent call limit exceeded | reduce concurrency; ask for a higher limit | Plivo policy | -| 5200 | Server timer expired | retry | Plivo | -| 5230 | Endpoint 5xx or 6xx mid-call | platform logs | platform config | -| 5240 | Mid-call media error | network and NAT | platform config | -| 5250 | Plivo-side service error | retry; Plivo support if persistent | Plivo | -| 5260 | Plivo-side routing error | retry; Plivo support if persistent | Plivo | -| 5270 | Plivo-side media error | retry; Plivo support if persistent | Plivo | -| 5290 | Trunk URI fetch error | retry; verify the trunk has a primary URI | Plivo | -| 5320 | Carrier 5xx or 6xx mid-call | carrier status | carrier/destination | -| 5340 | Endpoint timeout | endpoint responsiveness | platform config | -| 6020 | Destination does not exist | correct the destination | carrier/destination | -| 6030 | Answer timeout | handle no-answer | carrier/destination | -| 6070 | Session description rejected | correct SDP | carrier/destination | - -### Transfer outcomes (no hangup code; read the NOTIFY sipfrag) - -A failed REFER leaves the agent leg up, so it never shows as a hangup code. - -| NOTIFY sipfrag | Owner | Fix | -|---|---|---| -| `100 Trying` then `200 OK` | expected outcome | nothing; Plivo sends BYE to the agent leg | -| `100 Trying` then `4xx` or `5xx` (documented examples `486 Busy Here`, `408 Request Timeout`; `480` and `503 Transfer Failed` also commonly seen, not documented) | transfer target or route | the agent speaks at once, then retries another number or continues; check Geo Permissions and balance. Callers often hang up within seconds of silence | -| `100 Trying` only | platform config | keep the agent leg up until `NOTIFY 200` or a failure; do not send BYE early | - -### Name differences between the docs table and call records (quote the one you mean) - -| Code | Docs | Call record | -|---|---|---| -| 4030 | `insufficient_plivo_credits` | `insufficient_credits` | -| 4550 | `user_cancelled` | `customer_cancelled` | -| 5220 / 5310 | `service_interrupted_by_customer` / `..._by_carrier_4xx` | `service_interrupted_middialog_by_customer` / `..._middialog_by_carrier_4xx` | -| 4010 | `unauthorized_by_carrier` | `call_rejected_unauthorized_by_carrier` | -| geo block | 4650 (geo-permissions page) and 4560 (hangup table) | 4560 | - -A code that is not in the public table still appears in `hangup_cause_name`. Do not invent a meaning for it: use `plivo ask` or Plivo support with the call UUID. - -## When this skill does not have the answer - -Do not guess, and do not fill the gap from general knowledge of other platforms. In order: - -1. **Read the current documentation.** Every page on is available as Markdown by adding `.md` to its URL, and lists every page. Start at , and . From a terminal `plivo docs search ` searches the full text of every page, `plivo docs list` prints the index and `plivo docs show ` prints one page; those three need no credentials and are not rate limited, so reach for them before the assistant. -2. **Ask Plivo's assistant from the terminal**: `plivo ask ""`. It reads the documentation and can see the account, so it answers things this file cannot: what a specific call did, whether a compliance application is accepted, what a destination costs. It is limited to five requests per ten minutes per account, so save it for the question you cannot answer another way. `plivo voice calls diagnose ` is the same assistant pointed at one call; it is built for Voice API calls and whether it accepts a trunk call UUID is not verified, and it shares that limit, so try it once and do not loop it. -3. **If you have no CLI access**, tell the person you are working with to ask the same question to the assistant in the Plivo console. - -Treat the answer as evidence, not as final. If it contradicts the documentation, say that it does and prefer the documentation for published behaviour. If it gives a number the documentation does not publish, repeat it as something the assistant said, not as a documented fact. - -For account state read it yourself with `plivo api GET ... -o json`, and for one specific call read the console SIP logs. For CLI behaviour `plivo --help` outranks this file: if the two disagree, the CLI is right and this file needs updating, and you should say so. Rules marked here as observed rather than documented are safe checks, not Plivo commitments. Never invent flags, API fields, hangup codes or platform IPs. Where Plivo's pages disagree with each other (4560 vs 4650, `user_cancelled` vs `customer_cancelled`, RTP port range, CPS queue vs reject, API examples without `;transport=`), this file names both and which one it follows. - -## CANNOT - -- **Cannot write Plivo XML.** A SIP trunk hands the call to your platform. There is no answer URL and no XML on this path. If you came here looking for XML, that is a separate install: `npx skills add https://www.plivo.com/docs --skill plivo-voice-xml`, or read . For a WebSocket bot, `--skill plivo-audio-streaming`. -- **Cannot invent CLI flags.** There is no `plivo sip ...`, no `numbers update --trunk-id`, no trunk, URI, credential or IP-list command, no geo-permissions command. Trunk objects go through `plivo api /Zentrunk/...`; geo permissions, CPS increases, the SIP flow and PCAP are console or support only. Say so. -- **Cannot infer account state.** Look the number, trunk, URI, credentials and IP lists up with the readiness checklist; never assume a number is attached, a trunk enabled, a URI's transport right, or KYC accepted. -- **Cannot treat an accepted API request as a working call.** `Trunk created successfully` means the object exists. A call works when the record shows 3000/3010 with a non-zero duration and the SIP flow shows the platform answering 200. Test both directions. -- **Cannot spend or reconfigure without a preview.** Before `numbers update --app-id`, `numbers buy`, or any `plivo api POST|DELETE --yes`, show the exact request (`--dry-run`) and the rollback (`plivo numbers update --app-id `; for trunks the previous field values). Deleting a URI or credential attached to a live trunk breaks calls at once. -- **Cannot fill in or submit KYC.** Business details and documents come from the user; a compliance application is a regulatory filing and is submitted only when the user says so. -- **Cannot see the platform side.** Dispatch rules, number imports, agent bindings, region pinning, platform egress IPs: ask, then verify with a call. -- **Cannot invent API fields.** The Origination URI API documents `uri`, `authentication_needed`, `username` and `password`. Do not send anything else and do not tell a customer a field exists because another platform has one. Read the URI back with `GET /Zentrunk/URI//` after every create. -- **Cannot present an undocumented platform as supported.** Plivo publishes guides for LiveKit, ElevenLabs, Retell, Vapi and a generic self-hosted path. For anything else, including OpenAI Realtime and xAI, say Plivo does not document it and get the values from that platform and from Plivo. -- **Cannot promise that `plivo voice calls diagnose` understands trunk calls,** or that any transfer target form other than the documented one keeps working. -- **Cannot handle credentials.** Passwords go in a file passed with `--body @file`, never on the command line, in chat, or in this skill's output; Plivo never returns them. - -This skill does not cover: PBX interconnection (3CX, Asterisk, FreeSWITCH, FreePBX, FusionPBX, Twilio BYOC), carrier routing and rates, WebSocket Audio Streaming (separate install, `npx skills add https://www.plivo.com/docs --skill plivo-audio-streaming`), Voice API `` and transfer flows and Plivo XML (separate install, `--skill plivo-voice-xml`), SMS, number porting, or how any agent platform works inside. It cannot see your platform's logs or your Plivo account's data except through the CLI commands it names. +| 3000, 3010 `normal_hangup` | both | 200, then BYE | none (3010 with 0 s: read the platform logs) | +| 4090 `destination_not_found` | in | platform 404 | import `+`; bind an agent or dispatch rule | +| 4170 `request_timeout_customer` | in | INVITE repeated, no reply | URI host, port and transport; firewall; platform online; probe | +| 4150 `proxy_authentication_required` | in | platform 401/407 | stop challenging Plivo, or `authentication_needed` with the same credentials on the URI (the published row names a carrier) | +| 4410 `user_busy` | in | platform 486 | dispatch rule, agent availability, platform concurrency | +| 4350, 5360 | in | platform 480, 503 | platform status; fallback URI; TLS consistency | +| 4420 `carrier_cancelled` | in | caller CANCEL, 487 | none; answer faster | +| 4310 `uri_not_found` | in | no platform leg | set `primary_uri_uuid` | +| 4030 `insufficient_plivo_credits` | any | no platform leg | add credits; auto-recharge | +| 4590 `domestic_anchored_terms_not_met` | any | Plivo 403, no platform leg | India: platform must terminate SIP and media in India (not a KYC check) | +| 4180 `call_rejected_unauthorized` | out | Plivo 403 | credential username and password, or the platform IP in the IP ACL | +| 4190 `unknown_caller_id` | out | Plivo 403 | caller ID = a Plivo number on this account | +| 4110 `secure_trunking_disabled` | out | | `secure` on the trunk, or non-TLS on both sides | +| 4000 `bad_request` | out | 400, often after 183 | read the packet before any retry; support with the UUID if the INVITE looks valid | +| 4100 `prefix_not_supported` | out | 404 | E.164 with the country code | +| 4560 `barred_country`, 4650 | out | Plivo 403 Barred Country | enable the country in Geo Permissions | +| 4570 `barred_number` | out | Plivo 403 | drop the number, or support if legitimate | +| 4630 | out | 488 | offer PCMU or PCMA; fix the secure flag | +| 4410, 4340, 4550 | out | 486, 480, platform CANCEL | normal outcomes; a 4550 within a second or two is a dialer timeout bug | +| 4090, 4160, 5000, 5300, 5350, 6000, 6040 | out | 404, 502, 503 | carrier or destination: check E.164, retry; support if one destination keeps failing | +| 5180 `cps_limit_reached`, 5190 `concurrent_call_limit_exceeded` | any | rejected at once | pace the dialer; raise the limit | +| 5220, 3040 | any | 200, then a platform 4xx to a re-INVITE | platform logs at the drop time; support with Call-ID and PCAP | +| REFER `NOTIFY 4xx/5xx` | | no hangup code; agent leg stays up | the agent speaks at once | + +Every other code, and the current wording of these: `plivo docs show sip-trunking/troubleshooting/zentrunk-hangup-codes` (https://plivo.com/docs/sip-trunking/troubleshooting/zentrunk-hangup-codes). Call records sometimes carry other names (`insufficient_credits`, `customer_cancelled`), so filter by code. For a code missing from that page, do not invent a meaning: `plivo ask` or Plivo support with the UUID. + +## Where Plivo's docs disagree + +- Geo block: the hangup table gives 4560 `barred_country` and also lists 4650; the geo-permissions page gives 4650. Expect either. +- RTP ports: the 3020 row says 10000-20000; the `sip-trunking` page and technical specifications say 10000-30000. Allow the wider range. +- Signaling ranges: 14 on the `sip-trunking` page, 8 on technical specifications. Allow all 14. +- India anchoring: `voice/concepts/india-calling` names it `violates_media_anchoring` (Voice code 2070); trunk calls end 4590 `domestic_anchored_terms_not_met`. Filter trunk calls by 4590. +- `;transport=`: Origination URI API examples omit it; the integration guides set it. Follow the guides. +- Caller ID: technical specifications require a Plivo number; the generic guide also allows a verified caller ID. Use a Plivo number on this account. +- NOTIFY: technical specifications list it as not supported. That means Plivo does not accept NOTIFY from you; it still sends NOTIFY to report REFER progress. + +## When this file is not enough + +Use `plivo docs search ` and `plivo docs show ` first (no credentials, no rate limit), then `plivo ask ""`, which can see the account but has a small rate limit. Treat its answers as evidence and prefer the docs where they conflict. `--help` outranks this file on flags. Never invent flags, API fields, hangup codes, hostnames or IPs. Out of scope: PBX interconnection, carrier rates, Plivo XML (`plivo skill install voice-xml`) and WebSocket bots (`plivo skill install audio-streaming`). diff --git a/skills.sh b/skills.sh index 5dc10e1..91b1f66 100755 --- a/skills.sh +++ b/skills.sh @@ -10,11 +10,12 @@ # # Fetches SKILL.md — a single-file reference written for LLM coding agents — and # drops it where the agent auto-loads it. If you already have the binary, -# `plivo skill install [cli|audio-streaming|sip-trunking|voice-xml|all]` does +# `plivo skill install [cli|first-agent|audio-streaming|sip-trunking|voice-xml|all]` does # the same thing offline. # # Available skills: # cli use the `plivo` CLI instead of raw curl +# first-agent take a new user to a first AI voice agent on a real call # audio-streaming connect a WebSocket voice bot to calls with # sip-trunking connect an AI voice platform over SIP trunking # voice-xml write and fix Plivo Voice XML @@ -35,19 +36,21 @@ RAW="https://raw.githubusercontent.com/${REPO}/main" # ─── Resolve which skill(s) ────────────────────────────────────────────────── # Each entry is "selector:source-dir:install-dir". CLI_SKILL="cli:cli-skill:plivo-cli" +FIRST_SKILL="first-agent:first-agent-skill:plivo-first-agent" STREAM_SKILL="audio-streaming:audio-streaming-skill:plivo-audio-streaming" SIP_SKILL="sip-trunking:sip-trunking-skill:plivo-sip-trunking" XML_SKILL="voice-xml:voice-xml-skill:plivo-voice-xml" case "${1:-cli}" in cli) WANTED="$CLI_SKILL" ;; + first-agent) WANTED="$FIRST_SKILL" ;; audio-streaming) WANTED="$STREAM_SKILL" ;; sip-trunking) WANTED="$SIP_SKILL" ;; voice-xml) WANTED="$XML_SKILL" ;; - all) WANTED="$CLI_SKILL $STREAM_SKILL $SIP_SKILL $XML_SKILL" ;; + all) WANTED="$CLI_SKILL $FIRST_SKILL $STREAM_SKILL $SIP_SKILL $XML_SKILL" ;; *) echo "✗ Unknown skill: $1" >&2 - echo " Available: cli, audio-streaming, sip-trunking, voice-xml, all" >&2 + echo " Available: cli, first-agent, audio-streaming, sip-trunking, voice-xml, all" >&2 exit 1 ;; esac diff --git a/voice-xml-skill/SKILL.md b/voice-xml-skill/SKILL.md index e276129..a2fd77c 100644 --- a/voice-xml-skill/SKILL.md +++ b/voice-xml-skill/SKILL.md @@ -1,1193 +1,267 @@ --- name: plivo-voice-xml -description: Write and fix Plivo Voice XML, the document your answer URL returns to control a phone call. Load this for "Plivo XML", "answer URL", "action URL", IVR and keypad menus, Speak and Play prompts, SSML and voices, GetDigits and GetInput, DTMF, Dial with Number or User, call forwarding and simultaneous or sequential ringing, Redirect, Wait, PreAnswer, Hangup and call screening, Record and voicemail, Conference rooms, MultiPartyCall roles, sending an SMS from a call flow, element ordering and nesting rules, callback and action URL parameters, X-Plivo-Signature-V3 validation, webhook timeouts and retries, and the XML errors 8011, 8012, 8013 and 8014. Element and behaviour reference plus ready patterns. For WebSocket voice bots use plivo-audio-streaming; for SIP platforms use plivo-sip-trunking. +description: "Writes and debugs Plivo Voice XML, the document an answer URL returns to control a call: Speak, Play, GetDigits/GetInput IVRs, Dial forwarding, Record and voicemail, Conference, MultiPartyCall. Use for Plivo XML, answer or action URLs, 8011/8012 invalid XML, 7011 URL errors, X-Plivo-Signature-V3 checks. Not for WebSocket voice bots (plivo-audio-streaming) or SIP trunks (plivo-sip-trunking)." license: Apache-2.0 --- # Plivo Voice XML -You are helping a developer, or their coding agent, write the XML document their answer URL returns, and understand what Plivo will do with it. Answer with a document plus the rule behind it. Speak plainly. Every rule here carries a docs page; rules marked "commonly seen" come from practice and have no docs page. +Rules here come from the Plivo docs unless marked *observed* (seen on real calls, no docs page). Say a call will break only for a documented rule or a code the evidence shows; raise anything observed or undocumented as a risk. Never invent an element, attribute, value or hangup code. Plivo's handling of an unknown element or attribute is undocumented, so a call that worked does not prove an attribute is real: remove anything undocumented. -What is in this file, in order: the contract, the questions to ask, the element list, the ordering and nesting rules, the patterns, the URL contract, the failure codes, what not to do, then the deep sections (the full element reference, every pattern with a document you can copy, the URL contract in detail, and the shapes that break a call). +When this file runs out: `plivo docs search ""` and `plivo docs show ` (no credentials needed), then `plivo ask ""` (rate limited; it can see the account). Prefer the docs where `plivo ask` disagrees with them. This skill cannot see the user's server: ask for the exact HTTP status and bytes. -**Testing what you write.** The Plivo CLI places one call against your XML. Preview it first, because a call costs money and dials a real phone: +Install another skill when the task leaves XML: `plivo skill install audio-streaming` for WebSocket voice bots and ``, `plivo skill install sip-trunking` for SIP trunks (no answer URL, no XML). For CLI flags run `plivo --help`. -```bash -plivo voice calls make --from --to \ - --answer-url https://YOUR-HOST/plivo/answer --answer-method POST --dry-run # prints the request, sends nothing -plivo voice calls make --from --to \ - --answer-url https://YOUR-HOST/plivo/answer --answer-method POST --yes # only after the user agrees -plivo voice calls diagnose # what actually happened, including the XML errors below -``` - -`--answer-method` defaults to `GET`. Set `POST` explicitly, or Plivo will fetch a route your handler may not serve and you will debug the wrong thing. - -**What this file assumes you have: nothing but this file and the `plivo` CLI.** Every element, attribute, ordering rule, URL contract and failure shape you need to write and fix Plivo XML is here. Other Plivo skills are separate single files you may not have. Install one only if the task moves outside XML: - -- `npx skills add https://www.plivo.com/docs --skill plivo-audio-streaming` for the WebSocket voice bot journey: ``'s own attributes, the WebSocket protocol, streaming readiness and debugging a failed agent call. -- `npx skills add https://www.plivo.com/docs --skill plivo-sip-trunking` for SIP trunks and agent platforms reached over SIP, which have no answer URL and no XML at all. -- `plivo skill install` for `plivo-cli`, the CLI's own reference. The CLI writes that file out itself. - -If none is installed, do not stall: use `plivo --help` and the public docs, and say which source you used. - -## What a finding licenses you to say - -Three tiers, and they decide your verdict. - -- **Will break.** A documented rule says so, or a call with this shape is known to have failed. Say the call breaks, and name the failure. -- **Risky.** Plausible, unverified, or seen to work in some deployments. Say what could go wrong and what to check. Name no hangup code. -- **Style.** No functional effect. Say so. - -Nothing outside the first tier is a reason to tell someone their call will break. A true observation about a document is not a verdict on its own: a design risk you would raise in review is still a document that connects a call. Rules below carry a docs source when they are in the first tier; a rule marked "commonly seen", "observed" or "not documented" is risky. - -## The contract in six lines - -1. Plivo requests your answer URL and expects one XML document back. -2. Content-Type must be `application/xml` or `text/xml`, the body must be valid XML, and it must be under 100 KB. -3. Answer well inside 15 seconds. -4. The root element is ``. Children run top to bottom, one at a time. -5. An empty `` hangs the call up. So does running out of elements. -6. A body that is not a Plivo XML document is `8011 Invalid Answer XML`. No usable HTTP response at all is `7011`, a different code with a different fix. - -Sources: , , . - -## Ask before you write - -1. Inbound (someone calls your Plivo number) or outbound (you placed the call through the API)? Both fetch the same kind of document, but the parameters differ. -2. What should the caller hear first: text to speech, a recorded file, or nothing? -3. Does the caller choose something? Keypad only, speech, or either? -4. Where does the call go: another number, a SIP address, a room, voicemail, or nowhere? -5. Should the call be recorded, and does the caller need to be told? -6. What ends the call, and what should the caller hear before it does? - -Then copy the closest document from "Patterns and the documents to copy" below and change the URLs and text. Do not invent attributes: if it is not in the element reference below, it is not documented. - -## The elements - -| Element | Does | -|---|---| -| `` | Text to speech. `voice`, `language`, `loop`. SSML with a `Polly.` voice | -| `` | Play an audio file from an HTTPS URL. `loop` | -| `` | Send tones on the current call. `async` | -| `` | Collect keypad digits, post them to `action` | -| `` | Collect speech or digits, post them to `action`. Preferred for new work | -| `` | Connect the call to `` or `` | -| `` | Hand control to another URL of yours | -| `` | End the call, optionally with a `reason` or a `schedule` | -| `` | Pause. Also beep and silence detection | -| `` | Play media before the call is answered | -| `` | Record a message, or the whole session in the background | -| `` | Join a named room, up to 20 people | -| `` | Join a room with roles, coaching and per participant control | -| `` | Send an SMS from inside the call flow | -| `` | Open a WebSocket audio stream. Its own attributes and the WebSocket protocol are in `plivo-audio-streaming`, a separate install | - -Pages: (Speak, Play, DTMF), (GetDigits, GetInput), (Dial, Redirect, Hangup, Wait, PreAnswer), , , , , . - -Full attribute tables, defaults and allowed values are in "Element reference" below. - -## Ordering and nesting, the part people get wrong - -These are the parent and child pairs the docs list. The nesting section is headed "Some elements can be nested inside others" and never says the list is exhaustive or that other nesting is rejected, so treat undocumented nesting as untested and move the element out rather than claiming Plivo rejects the document (, ): - -| Parent | Allowed children | -|---|---| -| `Response` | any element | -| `GetDigits` | `Speak`, `Play` | -| `GetInput` | `Speak`, `Play` | -| `Dial` | `Number`, `User` | -| `PreAnswer` | `Speak`, `Play`, `Wait` | - -Six ordering rules: - -- **`redirect` decides who owns the rest of the call, and this is a risk to raise rather than a verdict.** On `GetDigits`, `GetInput`, `Record`, `Dial` and `Conference`, `redirect` defaults to `true`, so when the `action` URL answers, Plivo runs the XML it returns. Those are default rows, not a documented statement that the elements below are discarded, and the docs' own sequential dialling example leaves `redirect` at its default on two `` elements and still expects the trailing `` to run. Say the content below may be skipped for a caller who responds; do not say the call breaks. With `redirect="false"` the `action` URL is still called, its answer is ignored, and the next element in your document runs. This is also why a bad action document only fails the call when `redirect` is `true`. -- **Put a fallback after every element that can produce nothing.** After `GetDigits` or `GetInput` with no input, and after a `Dial` that nobody answered, execution falls through to the next element. If there is no next element the call ends silently. -- **`` goes before the thing you want recorded.** It starts immediately, runs in the background until the call ends, and ignores `timeout`, `finishOnKey` and `playBeep`. To capture both parties on a transfer, use `startOnDialAnswer="true"` and place it before ``. -- **`` is the end of the document.** Nothing written after it runs, because control has moved to the new URL. -- **`` is the end of the call, unless it is scheduled.** `` sets a timer and lets the following elements keep running. -- **Put `` first.** It plays before the call is answered, so anything that answers the call should not precede it. The page gives three limitations and no ordering rule: only `Speak`, `Play` and `Wait` go inside; the call is not answered during PreAnswer, so some carriers may time out; keep it under 30 seconds. The ordering itself is this file's inference, so treat a `` that is not first as a risk to raise, not a rejected document. - -## Patterns - -Every one of these has a document you can copy in "Patterns and the documents to copy" below, with the rule behind the choice. - -| You want | Shape | -|---|---| -| Keypad menu | `GetDigits` wrapping the prompt, then a fallback `Speak` and `Hangup` | -| Speech or keypad menu | `GetInput inputType="dtmf speech"` with `hints` | -| Forward a call | `Dial` with one `Number`, `callerId` set | -| Ring several people at once | one `Dial`, several `Number` children | -| Try people in turn | several `Dial` elements, each with a `timeout` | -| Dial a SIP address | `Dial` with one `User`, `sipAuthUsername` and `sipAuthPassword` | -| Forward, then voicemail | `Dial` then `Redirect`, or `Dial` then `Record` | -| Take a voicemail | `Speak`, then `Record` with `maxLength` and `finishOnKey` | -| Record a whole conversation | `Record recordSession="true"` before `Dial` | -| Conference room | `startConferenceOnEnter` false for guests, true for the host | -| Contact centre room with roles | `MultiPartyCall role="Agent"` | -| Reject a caller on purpose | `Hangup reason="rejected"` | -| Say you are closed, then stop | `Speak` then `Hangup` | -| Acknowledge a callback | `` returned to a `callbackUrl` or hangup URL | -| Custom ringback | `PreAnswer` with `Play`, then `Dial` | -| SSML prompt | `Speak voice="Polly.Joanna"` with `prosody` and `break` | -| Text the caller a link | `Speak` then `Message` | - -## URLs and callbacks - -Two kinds of URL come out of an XML document, and mixing them up is a common bug (): - -- **`action`** expects XML back. Plivo runs it. If it is unreachable you get `7012`, and if it is not XML you get `8012`. -- **`callbackUrl`** expects nothing back. It is a notification. Return `200` with an empty body or an empty ``. - -Set a **Fallback Answer URL** on the application, or `fallback_url` on the API call, so a failing primary URL does not kill every call. Plivo retries webhooks and can deliver the same callback twice, so make handlers idempotent on `CallUUID`. Validate `X-Plivo-Signature-V3` rather than putting a token in the URL (). Timeouts, retry counts and retry policy are set with URL fragments such as `#ct=2000&rc=3&rp=ct,rt` (). Full parameter lists per element are in "The URL contract" below. - - -## Two cautions before you name a code - -**Do not name a hangup code the evidence does not pick.** 7011 and 8011 are different failures: 7011 means Plivo never got a usable HTTP response, and 8011 means it got one and the body was not a Plivo XML document. A non-XML body you can see in a `curl` is an 8011 shape, but if the same request also returned a non-2xx status it is a 7011 and the body is beside the point. Read the status line first, then the body, and name a code only when the evidence picks one. - -**Do not promise what an undocumented input will do.** The docs describe valid documents; they do not say what Plivo does with an invalid one beyond reporting 8011. A raw `&` inside an attribute value is invalid XML and you should fix it, but do not tell a customer it is definitely why their call failed, and do not tell them it is safe either. Report it as a defect to fix, then find the evidence for the actual failure. - -## When the call fails - -| Code | Name | What it means | First move | -|---|---|---|---| -| 8011 | Invalid Answer XML | The answer URL replied, but not with a Plivo XML document | Print the exact bytes your server sent, then read "What breaks, and how to tell which" | -| 8012 | Invalid Action XML | An `action` document was bad. Only fails the call when `redirect="true"` | Check the second document, not the first | -| 8013 / 8014 | Invalid Transfer / Redirect XML | Same problem on a transfer or redirect URL | Same | -| 7011 | Error Reaching Answer URL | A non 2xx response from the answer URL, or no response at all: 404, 401, 405, timeout, dead host | Check status code and method, not the XML. A 200 with an empty body is 8011, not this | -| 7012 / 7013 / 7014 | Error Reaching Action / Transfer / Redirect URL | Same, on the later URL | Same | -| 4010 | End Of XML Instructions | The document ran out. Normal | Nothing, if the call was meant to end | -| 3020 / 3010 with hangup source Answer XML | Rejected / Busy Line | Observed pairing, not a documented one. The routing page documents only the audible effect (a rejection tone, a busy signal) and the hangup table describes 3020 and 3010 as coming from the called party. Call records show these two codes with source Answer XML after a `` | Intended screening, or a placeholder someone forgot | - -Codes: . - -Three different empties get confused, so keep them apart. **No usable HTTP answer** (a non 2xx, a timeout, a dead host) is 7011: the hangup table defines it as a non 2xx response from the answer URL, so there was nothing to parse. **A 200 whose body is not Plivo XML** is 8011: JSON, HTML, plain text, or a zero length body, which the XML overview lists as "Empty response" among the causes of invalid XML. **An empty ``** is neither: it is valid XML that runs and immediately ends the call. - -The nine shapes that produce 8011 and 8012, each with the fix, are in "What breaks, and how to tell which" below. The short list: a non XML body, a JSON error page from your framework or from object storage answering a POST, plain text, leading bytes before the XML declaration, an unescaped `&` in a URL attribute, a Twilio element such as `` or ``, a `` or attribute left empty by a template, an unsupported ``, and a second document that was never tested because only the first one was. Free text in `Hangup reason` and a truncated document are commonly seen and are worth fixing, but no page names an outcome for either. - -## What not to do - -- Do not copy Twilio XML. ``, ``, ``, `` and `` are not Plivo elements. The nearest Plivo forms are ``, ``, `` and `` (). -- Do not put a raw `&` inside an attribute value. Write `&`. A query string with two parameters is the usual way this breaks (docs, plus commonly seen). -- Do not return `` as a placeholder while you build. It ends the call gracefully the moment it runs, so the call answers and stops and the record looks like a document that finished normally, which you cannot tell apart from a real fault later. Return `` instead. `` and `` are the forms that give the caller a rejection or busy signal, and they show as 3020 and 3010. -- Do not point a number at a console flow application and then serve your own XML from somewhere else. The number uses whatever application it is attached to (commonly seen). -- Do not use a `language` or `voice` that is not in the tables. `voice` is `WOMAN` or `MAN`, or a `Polly.` name; SSML only works with `Polly.` voices (, ). -- Do not leave `log="true"` on a `GetDigits` or `GetInput` that collects a PIN or a card number. -- Do not build a `Redirect` loop with no exit. Every branch must reach a document that does not redirect. -- Do not assume `action` and `callbackUrl` behave the same. Only `action` expects XML. -- Do not host prompt audio on a slow or plain HTTP host. `` needs HTTPS, mp3 or wav, under 10 MB. - -## When this skill does not have the answer - -Do not guess, and do not fill the gap from general knowledge of other platforms. In order: - -1. **Read the current documentation.** Every page on is available as Markdown by adding `.md` to its URL, and lists every page. Start at and the element page for whatever you are writing. From a terminal `plivo docs search ` searches the full text of every page, `plivo docs list` prints the index and `plivo docs show ` prints one page; those three need no credentials and are not rate limited, so reach for them before the assistant. -2. **Ask Plivo's assistant from the terminal**: `plivo ask ""`. It reads the documentation and can see the account, so it answers things this file cannot: what a specific call did, whether a compliance application is accepted, what a destination costs. It is limited to five requests per ten minutes per account, so save it for the question you cannot answer another way. `plivo voice calls diagnose ` is the same assistant pointed at one call, and it shares that limit, so do not loop either. -3. **If you have no CLI access**, tell the person you are working with to ask the same question to the assistant in the Plivo console. - -Treat the answer as evidence, not as final. If it contradicts the documentation, say that it does and prefer the documentation for published behaviour. If it gives a number the documentation does not publish, repeat it as something the assistant said, not as a documented fact. - -Never invent an element, an attribute, an allowed value or a hangup code. Where Plivo's own pages disagree, the element reference below names both readings and which one this file follows. - -## CANNOT - -- **Cannot see your server.** A document that looks right here can still fail with 7011 because of a status code, an auth check or a dead host. Ask for the exact bytes and the exact HTTP status. -- **Cannot confirm a call worked.** Well formed XML is not a working call. Only the call record and someone who heard the audio can say that. -- **Cannot decide legal questions.** Recording notices, consent, retention and calling hours need the user's own legal review. This skill states the platform behaviour only. -- **Cannot cover the voice agent journey.** Readiness, WebSocket protocol, streaming debugging and go live belong to `plivo-audio-streaming`, and SIP trunks and agent platforms to `plivo-sip-trunking`. Both are separate installs (`npx skills add https://www.plivo.com/docs --skill `); if the user does not have one, say so and point at the public docs section rather than improvising the answer. -- **Cannot cover the rest of the platform.** Number provisioning, compliance and KYC, the Voice API beyond the URLs named here, the Browser SDK, messaging beyond ``, and the console flow application builder are all out of scope. - -## Element reference: every documented attribute - -Every attribute below is documented on a Plivo docs page, named under each element. Nothing here is inferred. Defaults are Plivo's documented defaults, not recommendations. `` is deliberately not covered here; install `plivo-audio-streaming` for it. - -Attribute names are case sensitive and camelCase. Element names are case sensitive too: `` is not ``. - -Two warnings about completeness. First, some deployments carry attributes on `Dial`, `Conference`, `MultiPartyCall`, `User`, `GetInput` and `Wait` that are not in any of the tables below. Some of them may work, but none of them is documented, so this file does not list them and you should not recommend one. If a deployment already uses one, say that it is undocumented and ask Plivo support before relying on it. Second, what Plivo does with an attribute or element it does not recognise is not documented, so "the call still worked" is not evidence that an attribute is real, and neither is a failure proof that the attribute caused it. Remove anything undocumented rather than reasoning about how it is handled. - -### Response - -Root element. Children run in order, one at a time. +Before writing, pin down: inbound or outbound, what the caller hears first, keypad or speech input, where the call goes, whether it is recorded (and the caller told), and how it ends. -An empty `` ends the call when it runs. Returned to a `callbackUrl` or a hangup URL it is simply an acknowledgement, which is fine and common. +## Validate, fix, repeat -Nesting rules for the whole language are in "Ordering and nesting" above. No element other than `Response`, `GetDigits`, `GetInput`, `Dial` and `PreAnswer` takes children. `MultiPartyCall`, `Conference`, `Redirect`, `Play`, `DTMF` and `Message` carry their value as element text. +Run this loop on every document you write or change, including every document an `action` or `` URL returns: -### Speak +1. **Parse it.** `xmllint --noout answer.xml`, or any XML parser. Every `&` inside an attribute must be `&`. +2. **Fetch it the way Plivo will**, with the same method, and read the status line, the `Content-Type` and the first bytes of the body: + ```bash + curl -s -i -X POST https://YOUR-HOST/plivo/answer \ + -d 'CallUUID=test&From=%2B10000000000&To=%2B10000000001&Direction=inbound&CallStatus=ringing' + ``` + Repeat for each `action` URL with that element's parameters (`Digits=1`, `DialStatus=no-answer`, and so on). +3. **Review the flow.** Something after every element that can yield nothing, an exit on every redirect loop, `log="false"` on anything that collects a secret, a Fallback Answer URL on the application. +4. **Place one call, preview first.** A call costs money and rings a real phone. + ```bash + plivo voice calls make --from --to --answer-url https://YOUR-HOST/plivo/answer --answer-method POST --dry-run + plivo voice calls make --from --to --answer-url https://YOUR-HOST/plivo/answer --answer-method POST --yes # only after the user agrees + plivo voice calls diagnose + ``` + `--answer-method` defaults to `GET`. Set `POST` explicitly, or Plivo fetches a route your handler may not serve. +5. **Fix what failed and go back to step 1.** Well-formed XML is not a working call: only the call record and someone who heard the audio confirm it. -Text to speech. Text goes in the element body. Docs: . +## The answer-URL contract -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `voice` | string | `WOMAN` | `WOMAN` or `MAN` | -| `language` | string | `en-US` | see the language table below | -| `loop` | integer | `1` | number of repeats. `0` repeats until the call ends | +- Plivo requests the answer URL (inbound: the number's application; outbound: `answer_url` on the API call) and runs the one document it returns. +- Reply with `Content-Type: application/xml` or `text/xml`, valid XML, under 100 KB, well inside 15 seconds. +- The root is ``. Children run top to bottom, one at a time. Element and attribute names are case-sensitive: `` is not ``, and `sip_auth_username` is not `sipAuthUsername`. +- Running out of elements ends the call (4010, normal). An empty `` ends it at once: a fine acknowledgement to a callback, a dropped call from an answer URL. +- No usable HTTP answer (non-2xx, timeout, dead host) is 7011. A 2xx whose body is not Plivo XML (JSON, HTML, plain text, an empty body) is 8011. +- Every request carries `CallUUID`, `From`, `To`, `CallStatus`, `Direction`. Inbound: `From` is the caller, `To` your number. Outbound: `From` is your caller ID, plus `ALegUUID` and `ALegRequestUUID`. SIP headers arrive as `X-PH-`. +- Set a Fallback Answer URL on the application (`fallback_url` on an API call) so one failing primary URL does not fail every call. -**Languages for the `WOMAN` and `MAN` voices**: `da-DK`, `nl-NL`, `en-AU`, `en-GB`, `en-US`, `fr-FR`, `fr-CA`, `de-DE`, `it-IT`, `pl-PL`, `pt-PT`, `pt-BR`, `ru-RU`, `es-ES`, `es-US`, `sv-SE`. Not every language has both a woman and a man; the docs table says which. This is the list the documentation prints. Treat a language outside it as unverified: it may work, but test it on a real call before you rely on it, and tell the user it is untested rather than either promising it or claiming Plivo rejects it. For `en-IN` and `hi-IN` the documented route is a `Polly.` voice that covers them. +## Docs pages -**Polly voices.** Set `voice="Polly."` to use an Amazon Polly voice. Polly covers 27 languages and more than 40 voices, including `hi-IN` (`Polly.Aditi`) and `en-IN` (`Polly.Raveena`, `Polly.Aditi`) which the table above does not list. Full voice list: . Polly voices must carry the `Polly.` prefix. +Outside a terminal, `docs show` and `docs search` print a JSON envelope: add `-o table`. A CLI page can stop early (`voice/xml/input` does): fetch `https://www.plivo.com/docs/.md` for the whole page as clean Markdown, not the HTML URL. -**Docs disagreement.** The audio output page says the allowed values are `WOMAN` and `MAN`. The same page's SSML section and the SSML concept page both use `Polly.` names in the same attribute. This file follows the SSML pages: `Polly.` names are valid in `voice`, and they are required for SSML. - -**SSML.** Only works with a Polly voice. Maximum 3,000 characters per ``. Supported tags include ``, ``, ``, ``, `

`, ``. Plivo does not support `` or ``. - -**Nesting.** `` may sit inside ``, `` and ``. - -### Play - -Plays an audio file. The URL goes in the element body. Docs: . - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `loop` | integer | `1` | `0` loops until the call ends | - -File rules: mp3 or wav, served over HTTPS, maximum 10 MB, 8 kHz or 16 kHz mono recommended. - -The body must be a URL. Putting a sentence inside `` does not speak it; use `` (commonly seen mistake). - -**Nesting.** `` may sit inside ``, `` and ``. - -### DTMF - -Sends DTMF tones on the current call. The digits go in the element body. Docs: . - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `async` | boolean | `true` | `true` starts the next element while tones are still being sent; `false` waits | - -Allowed characters: `0-9`, `*`, `#`, `w` (wait 0.5 s), `W` (wait 1 s). - -When you are dialling out and want to enter an extension, the docs recommend `sendDigits` on `` instead of ``. +| Covers | CLI | Web | +|---|---|---| +| `Speak`, `Play`, `DTMF` | `plivo docs show voice/xml/audio-output` | https://plivo.com/docs/voice/xml/audio-output | +| `GetDigits`, `GetInput` | `plivo docs show voice/xml/input` | https://plivo.com/docs/voice/xml/input | +| `Dial` (`Number`, `User`), `Redirect`, `Hangup`, `Wait`, `PreAnswer` | `plivo docs show voice/xml/routing` | https://plivo.com/docs/voice/xml/routing | +| `Record`, transcription, recording retention | `plivo docs show voice/xml/record` | https://plivo.com/docs/voice/xml/record | +| `Conference` | `plivo docs show voice/xml/conference` | https://plivo.com/docs/voice/xml/conference | +| `MultiPartyCall`, roles, status events, AI agent attributes | `plivo docs show voice/xml/multiparty-call` | https://plivo.com/docs/voice/xml/multiparty-call | +| `Message` (SMS from a call flow) | `plivo docs show messaging/xml/overview` | https://plivo.com/docs/messaging/xml/overview | +| `Stream` (use plivo-audio-streaming) | `plivo docs show voice/xml/audio-streaming` | https://plivo.com/docs/voice/xml/audio-streaming | +| Nesting, request parameters, response rules | `plivo docs show voice/xml/overview` | https://plivo.com/docs/voice/xml/overview | +| `action` versus `callbackUrl`, idempotency | `plivo docs show voice/concepts/callbacks` | https://plivo.com/docs/voice/concepts/callbacks | +| Timeouts, retries, edge region, which URLs they apply to | `plivo docs show voice/concepts/callback-configurations` | https://plivo.com/docs/voice/concepts/callback-configurations | +| Signature validation | `plivo docs show voice/concepts/signature-validation` | https://plivo.com/docs/voice/concepts/signature-validation | +| SSML tags, Polly voices per language | `plivo docs show voice/concepts/ssml` | https://plivo.com/docs/voice/concepts/ssml | +| Every hangup code | `plivo docs show voice/troubleshooting/hangup-causes` | https://plivo.com/docs/voice/troubleshooting/hangup-causes | + +## Ordering and nesting + +Documented children: `GetDigits` and `GetInput` take `Speak` and `Play`; `Dial` takes `Number` and `User`; `PreAnswer` takes `Speak`, `Play` and `Wait`; everything else is a child of `Response`. `Speak` may also hold SSML tags with a `Polly.` voice. The docs never say other nesting is rejected: call it undocumented and move the element out. + +- **`redirect` decides who owns the rest of the call.** It defaults to `true` on `GetDigits`, `GetInput`, `Record`, `Dial` and `Conference`: when the `action` URL answers, Plivo runs the XML it returns, so the elements below may not run for a caller who responded. With `redirect="false"` the URL is still called, its body is ignored, and the next element runs. A bad or unreachable action document fails the call (8012, 7012) only when `redirect` is `true`. Raise skipped trailing elements as a risk, not a broken call. +- **Put a fallback after anything that can yield nothing.** No input after `retries`, or an unanswered `Dial`, falls through to the next element. With none, the call ends in silence. +- **`` goes before what it records.** It runs in the background until the call ends and ignores `timeout`, `finishOnKey` and `playBeep`. Before a ``, add `startOnDialAnswer="true"` to skip the ringing. +- **Treat `` as the end of the document.** Plivo continues the call at the new URL; the docs do not say what happens to siblings after it, so move them into the next document. +- **`` ends the call; `` only starts a timer**, and the following elements keep running. +- **Put `` first.** That is an inference; the docs list only its limits: `Speak`, `Play` and `Wait` inside, under 30 seconds, some carriers time out, the caller is not billed. Never `loop="0"` inside it when a `` follows, or the `Dial` may never run. For ringback while dialling, `dialMusic` on `` is simpler. + +## `action` versus `callbackUrl` + +- `action` expects XML, and Plivo runs it when `redirect` is `true`; returning `OK` or JSON there is then an 8012. +- `callbackUrl`, `ring_url` and the hangup URL are notifications. Return 200 with an empty body or ``. +- Retries can deliver the same request twice. Make handlers idempotent on `CallUUID` (plus the element context for `action`, `RecordingID` for recordings). +- Per-URL timeouts, retries and edge region go in a URL fragment, for example `https://example.com/answer#ct=2000&rt=3000&rc=2&rp=ct,rt`. Keys, ranges and defaults are on `voice/concepts/callback-configurations`. Fragments do not apply to audio URLs in `` and ``. +- Element URL methods default to `POST`, except the MultiPartyCall `enterSoundMethod`, `exitSoundMethod`, `startRecordingAudioMethod` and `stopRecordingAudioMethod`, which default to `GET`. Test each URL with the method it is actually configured with. + +## The elements most documents need + +### Speak and Play + +- `text`. `voice`: `WOMAN` (default), `MAN`, or `Polly.`, which SSML requires. `loop` defaults to `1`; `0` repeats until the call ends. +- `language` defaults to `en-US`. Documented for `WOMAN` and `MAN`: `da-DK`, `nl-NL`, `en-AU`, `en-GB`, `en-US`, `fr-FR`, `fr-CA`, `de-DE`, `it-IT`, `pl-PL`, `pt-PT`, `pt-BR`, `ru-RU`, `es-ES`, `es-US`, `sv-SE`, with no `MAN` for `da-DK`, `fr-CA`, `ru-RU`, `sv-SE` and no `WOMAN` for `pt-PT`. A language outside that table is untested: test it on a real call before shipping it, and neither promise that it works nor claim that Plivo rejects it. For `hi-IN` use `Polly.Aditi`; for `en-IN`, `Polly.Raveena` or `Polly.Aditi`. +- SSML: a `Polly.` voice, at most 3,000 characters per ``, and no `` or ``. +- `https://...`: HTTPS, mp3 or wav, at most 10 MB, 8 or 16 kHz mono recommended. Text inside `` is not spoken. ### GetDigits -Collects keypad digits and posts them to `action`. The docs recommend `` for new applications. Docs: . - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `action` | URL | none | where the digits are posted | -| `method` | string | `POST` | `GET` or `POST` | -| `numDigits` | integer | `99` | maximum digits to collect | -| `timeout` | integer | `5` | seconds to wait for the first digit | -| `digitTimeout` | integer | `2` | seconds between digits | -| `finishOnKey` | string | `#` | a digit, `#`, `*`, or `none` | -| `retries` | integer | `1` | attempts when no input arrives | -| `redirect` | boolean | `true` | `true` means the action document takes over the call | -| `playBeep` | boolean | `false` | beep after the prompts, before collecting | -| `validDigits` | string | `1234567890*#` | digits the caller may press | -| `invalidDigitsSound` | URL | none | audio played on an invalid digit | -| `log` | boolean | `true` | set `false` for PINs and card numbers | +The docs recommend `GetInput` for new work. -**Children:** `` and `` only. The prompt plays while Plivo waits, and collection starts as soon as the first digit is pressed. - -**Flow:** prompts play, optional beep, digits are collected until `numDigits`, `finishOnKey` or a timeout, digits are posted to `action`, and the action document runs. After `retries` attempts with no input, execution falls through to the next element in your document. - -**Action parameters:** `Digits`, which excludes the `finishOnKey` character, plus all the standard request parameters. +| Attribute | Default | Notes | +|---|---|---| +| `action` | none | receives `Digits` (without the `finishOnKey` key) and the standard parameters | +| `method` | `POST` | | +| `numDigits` | `99` | maximum digits | +| `timeout` | `5` | seconds to wait for the first digit | +| `digitTimeout` | `2` | seconds between digits | +| `finishOnKey` | `#` | a digit, `#`, `*` or `none` | +| `retries` | `1` | "Retry attempts if no input"; with no digits after `retries` attempts, the next element runs. Undocumented: whether the prompt replays, and whether `1` means one try or two | +| `redirect` | `true` | | +| `playBeep` | `false` | beep after the prompts | +| `validDigits` | `1234567890*#` | | +| `invalidDigitsSound` | none | audio URL played on an invalid digit | +| `log` | `true` | set `false` for PINs and card numbers | ### GetInput -Collects speech, digits, or either, and posts the result to `action`. Docs: . - -Core: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `action` | URL | required | where the result is posted | -| `method` | string | `POST` | `GET` or `POST` | -| `inputType` | string | none | `dtmf`, `speech`, or `dtmf speech` | -| `redirect` | boolean | `true` | `true` means the action document takes over the call | -| `log` | boolean | `true` | set `false` for sensitive input | - -Timing: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `executionTimeout` | integer | `15` | total seconds, 5 to 60 | -| `digitEndTimeout` | string | `auto` | seconds between digits, 2 to 10, or `auto` | -| `speechEndTimeout` | string | `auto` | seconds of silence that end speech, 2 to 10, or `auto` | -| `startInputTimeout` | integer | none | seconds to wait for the caller to start | -| `retries` | integer | `1` | attempts when no valid input arrives | - -DTMF: `numDigits` default `32`, range 1 to 32; `finishOnKey` default `#`. - -Speech: `language` default `en-US`; `speechModel` default `default`, also `command_and_search` and `phone_call`; `hints`, a comma separated list of phrases; `profanityFilter` default `false`. - -Callbacks: `interimSpeechResultsCallback` and `interimSpeechResultsCallbackMethod` (default `POST`). - -**Children:** `` and `` only. - -**Hints limits:** 500 phrases per request, 10,000 characters in total, 100 characters per phrase. - -**Documented speech languages**, listed in the docs as "common languages include": `en-US`, `en-GB`, `en-AU`, `es-US`, `es-ES`, `fr-FR`, `de-DE`, `it-IT`, `pt-BR`, `ja-JP`, `zh-CN`. The list is explicitly not exhaustive, so a code that is not on it may or may not work. Test it before relying on it. What Plivo does with a code the recogniser does not accept is not documented, so treat an unlisted code as untested rather than as known to be rejected. - -**Action parameters:** `InputType` (`dtmf` or `speech`), `Digits`, `Speech`, `SpeechConfidenceScore`, `BilledAmount`, plus the standard request parameters. - -**Interim callback parameters:** `StableSpeech`, `UnstableSpeech`, `Stability`, `SequenceNumber`. - -Speech recognition is billed per 15 second increment. - -### Dial - -Connects the current call to another party. Must contain at least one `` or ``. Docs: . - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `action` | URL | none | receives the dial result | -| `method` | string | `POST` | `GET` or `POST` | -| `timeout` | integer | none | seconds to wait for an answer | -| `timeLimit` | integer | `14400` | maximum seconds once connected | -| `callerId` | string | the caller's own id | number shown to the person you dial | -| `callerName` | string | the caller's name | maximum 50 characters | -| `hangupOnStar` | boolean | `false` | caller presses `*` to drop the other leg | -| `redirect` | boolean | `true` | `true` means the action document takes over | -| `callType` | string | `voice` | `voice` or `whatsapp`. WhatsApp cannot reach the phone network and does not support machine detection | -| `callbackUrl` | URL | none | live dial events, no XML expected back | -| `callbackMethod` | string | none | `GET` or `POST` | -| `confirmSound` | URL | none | returns XML played to the person you dialled | -| `confirmKey` | string | none | key they must press to accept | -| `confirmTimeout` | integer | none | seconds to wait for that key | -| `dialMusic` | URL or `real` | none | ringback. `real` plays the carrier's own ringing | -| `digitsMatch` | string | none | DTMF patterns to report from the caller side | -| `digitsMatchBLeg` | string | none | DTMF patterns to report from the dialled side | -| `sipHeaders` | string | none | `key=value,key2=value2` | - -#### Number - -Dials a phone number. The number goes in the element body. - | Attribute | Default | Notes | |---|---|---| -| `sendDigits` | none | DTMF sent after answer. `w` is a 0.5 s pause | -| `sendDigitsMode` | none | `rfc2833` sends telephone events instead of inband tones | -| `sendOnPreanswer` | `false` | send the digits during early media | -| `sipHeaders` | none | headers for this number only | - -An empty `` is not a valid target and fails the document (commonly seen). Leave the element out rather than emitting it empty. - -#### User - -Dials a SIP address. The `sip:` URI goes in the element body. - -| Attribute | Notes | -|---|---| -| `sipHeaders` | headers for this user only | -| `sipAuthUsername` | SIP digest username, for endpoints that challenge with 401 or 407 | -| `sipAuthPassword` | 8 to 128 characters, required when the username is set | +| `action` | required | receives `InputType` (`dtmf` or `speech`), `Digits`, `Speech`, `SpeechConfidenceScore`, `BilledAmount` | +| `method` | `POST` | | +| `inputType` | none | `dtmf`, `speech`, or `dtmf speech` (the first one detected wins) | +| `redirect` | `true` | | +| `log` | `true` | set `false` for sensitive input | +| `executionTimeout` | `15` | total seconds, 5 to 60 | +| `digitEndTimeout`, `speechEndTimeout` | `auto` | 2 to 10 seconds, or `auto` | +| `startInputTimeout` | none | seconds for the caller to start | +| `retries` | `1` | | +| `numDigits` | `32` | 1 to 32 | +| `finishOnKey` | `#` | | +| `language` | `en-US` | the docs list "common languages include" `en-US`, `en-GB`, `en-AU`, `es-US`, `es-ES`, `fr-FR`, `de-DE`, `it-IT`, `pt-BR`, `ja-JP`, `zh-CN`; test any other code | +| `speechModel` | `default` | or `command_and_search`, `phone_call` | +| `hints` | none | comma-separated phrases; at most 500 phrases, 10,000 characters, 100 per phrase | +| `profanityFilter` | `false` | | +| `interimSpeechResultsCallback` | none | receives `StableSpeech`, `UnstableSpeech`, `Stability`, `SequenceNumber`; its method defaults to `POST` | -Attribute names are camelCase: `sipAuthUsername`, not `sip_auth_username`. - -**Reserved `sipHeaders` prefixes**, silently dropped, case insensitive: `PH-`, `Plivo`, `FS-`, `SipAuth`, `ZT-`, `Twilio`, and the exact name `ClientRegion`. - -**SIP auth failures** appear on the `action` URL as `DialHangupCause` `sip_auth_failed` (code 4240, `DialStatus` `failed`) or `sip_auth_timeout` (code 4250, `DialStatus` `timeout`). - -#### Dialling several people - -- **At once:** several `` children inside one ``. The first to answer is connected. -- **In turn:** several `` elements one after another, each with its own `timeout`. - -#### Dial action and callback parameters - -Action URL, sent when the dial finishes: `DialStatus` (`completed`, `busy`, `failed`, `cancel`, `timeout`, `no-answer`), `DialRingStatus`, `DialHangupCause`, `DialALegUUID`, `DialBLegUUID`. - -Callback URL, live events with no XML expected back: `DialAction` (`answer`, `connected`, `hangup`, `digits`), `DialBLegStatus`, `DialALegUUID`, `DialBLegUUID`, `DialBLegDuration`, `DialBLegBillDuration`, `DialBLegFrom`, `DialBLegTo`, `DialDigitsMatch`, `DialDigitsPressedBy` (`ALeg` or `BLeg`), `DialBLegHangupCauseName`, `DialBLegHangupCauseCode`, `DialBLegHangupSource`, `STIRVerification`. - -### Redirect - -Hands control to another URL of yours. The URL goes in the element body. Docs: . +### Dial -| Attribute | Type | Default | +| Attribute | Default | Notes | |---|---|---| -| `method` | string | `POST` | - -The redirect URL receives the standard request parameters. The page says `` transfers call execution to a different URL and Plivo continues the call there; it does not separately say that siblings below it are skipped, so treat anything after a `` as dead code to move rather than a documented failure. Every branch must eventually reach a document that does not redirect. - -### Hangup - -Ends the call. Docs: . +| `action` | none | receives `DialStatus` (`completed`, `busy`, `failed`, `cancel`, `timeout`, `no-answer`), `DialRingStatus`, `DialHangupCause`, `DialALegUUID`, `DialBLegUUID` | +| `method` | `POST` | | +| `redirect` | `true` | | +| `timeout` | `120` (hangup-causes 6010; routing: none) | seconds to ring | +| `timeLimit` | `14400` | seconds once connected | +| `callerId` | the caller's | use a number you own | +| `callerName` | the caller's | at most 50 characters | +| `dialMusic` | none | a URL that returns XML, or `real` for the carrier's ringback | +| `confirmSound`, `confirmKey`, `confirmTimeout` | none | an XML URL played to the callee, the key they press to accept, seconds to wait | +| `callbackUrl`, `callbackMethod` | none | live `DialAction` events (`answer`, `connected`, `hangup`, `digits`); no XML expected | +| `hangupOnStar` | `false` | the caller presses `*` to drop the callee | +| `sipHeaders` | none | `key=value,key2=value2` | + +`callType` (`voice` or `whatsapp`), `digitsMatch` and `digitsMatchBLeg` are on `voice/xml/routing`. + +- `` holds a phone number. `sendDigits="wwww1234"` sends DTMF after answer (`w` is 0.5 seconds); also `sendDigitsMode="rfc2833"`, `sendOnPreanswer`, `sipHeaders`. Never emit an empty `` (*observed* to fail the document). +- `` holds a `sip:` URI. `sipAuthUsername` with `sipAuthPassword` (8 to 128 characters) answers a 401 or 407 challenge. Failures reach `action` as `DialHangupCause` `sip_auth_failed` (4240) or `sip_auth_timeout` (4250). +- `sipHeaders` keys starting `PH-`, `Plivo`, `FS-`, `SipAuth`, `ZT-` or `Twilio` (any case), and the name `ClientRegion`, are silently dropped. +- Several `` children in one `` ring at once and the first to answer wins; several `` elements ring in turn. + +### Record, Redirect, Hangup + +- ``: `action`, `method` `POST`, `redirect` `true`, `fileFormat` `mp3` or `wav`, `timeout` `15` (seconds of silence), `maxLength` `60` (raise it for voicemail), `finishOnKey` `#`, `playBeep` `true`, `recordSession` `false`, `startOnDialAnswer` `false`, `recordChannelType` `stereo` (one party per channel) or `mono`, `callbackUrl`. `action` receives `RecordUrl`, `RecordingID`, `RecordingDuration`, `RecordingDurationMs`, `RecordingStartMs`, `RecordingEndMs`, `Digits`. With `recordSession` or `startOnDialAnswer` the first durations are `-1`; the real values arrive at `callbackUrl`. `RecordUrl` links to the file: download it (Record page: deleted after 30 days; Recordings API: storage billed past 90). +- `https://...`: the URL receives the standard parameters and must return a document. +- ``: `reason` takes only `rejected` (a rejection tone) or `busy` (a busy signal); `schedule` is in seconds. Do not leave `` as a placeholder while you build: the call looks like it finished normally. Return a `` instead. + +### Everything else, in a line each + +- `Wait`: `length` (default 1 second), `silence`, `minSilence`, `beep` for beep detection. It is not a documented child of `GetDigits`. +- `DTMF`: `0-9`, `*`, `#`, `w` and `W` pauses; `async` defaults to `true`. To reach an extension after dialling, use `sendDigits` on ``. +- `Conference`: the room name as text; `maxMembers` 1 to 20 (default 20). Moderated: guests join with `startConferenceOnEnter="false"` and a `waitSound`, the moderator with `startConferenceOnEnter="true" endConferenceOnExit="true"`. A URL `enterSound` or `exitSound` must return XML with `Play`, `Speak` or `Wait`, not an audio file (`beep:1`, `beep:2` are built in). The page's `waitSound` examples point at `.xml` URLs: return XML there too, such as `` hold music. Two callers who join the same name are bridged. +- `MultiPartyCall`: the name as text, at most 10 participants, `role` `Customer`, `Agent`, `Supervisor` or `ai-agent`. `coachMode="true"` lets agents, not customers, hear a supervisor. Hold-music URLs return XML. Prefer it to `Conference` when you need roles, coaching, per-participant hold and mute, or API control. +- `Message`: `src` (a number you own), `dst`, `type="sms"`, `callbackUrl`, the text as the body. Several destinations are separated by `<`, which inside the attribute must be written `<`. -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `reason` | string | none | `rejected` gives the caller a rejection tone, `busy` gives a busy signal | -| `schedule` | integer | none | seconds to wait. Following elements keep running meanwhile | - -`reason` takes only those two documented values. Free text there is not a documented use and should not be relied on (commonly seen, undocumented). - -If your document does not end with ``, the call ends anyway once every element has run. - -### Wait - -Pauses execution. Docs: . - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `length` | integer | `1` | seconds to wait | -| `silence` | boolean | `false` | `true` plays silence instead of hold music | -| `minSilence` | integer | none | milliseconds of silence to detect | -| `beep` | string | none | `true`, or beep parameters | - -Beep parameters are a comma separated string, for example `beep="duration=300,inter_silence=50,intra_silence=500,threshold=256"`, with those values as defaults. - -`` is not a documented child of ``. Only `` and `` are. Not documented is not the same as rejected. Say "this nesting is not documented, so it may be ignored rather than honoured; move it outside the element to be safe", and do not claim Plivo rejects the document unless a documented error says so. The same wording applies to every other nesting claim below. - -### PreAnswer - -Plays media before the call is answered, so the caller is not billed for it. Docs: . - -Children: ``, ``, `` only. - -Limits: only those three elements are allowed; the call is not answered during this phase, so some carriers time out; keep it under 30 seconds. - -### Record - -Records audio and reports where the file is. Docs: . - -Basic: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `action` | URL | none | receives the recording details | -| `method` | string | `POST` | `GET` or `POST` | -| `fileFormat` | string | `mp3` | `mp3` or `wav` | -| `redirect` | boolean | `true` | `true` means the action document takes over | - -Timing: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `timeout` | integer | `15` | seconds of silence that stop the recording | -| `maxLength` | integer | `60` | maximum seconds | -| `finishOnKey` | string | `#` | a digit, `#`, `*`, or `none` | -| `playBeep` | boolean | `true` | beep before recording starts | - -Session recording: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `recordSession` | boolean | `false` | record the whole call in the background | -| `startOnDialAnswer` | boolean | `false` | start when the dialled party answers | -| `recordChannelType` | string | `stereo` | `mono` or `stereo`. Stereo puts each party on its own channel | - -Transcription: `transcriptionType` (`auto`, `hybrid`, `manual`), `transcriptionUrl`, `transcriptionMethod` (default `POST`), `transcriptionReportType` (`full` or `compact`, default `compact`). Transcription is English only, 500 ms to 4 hours, under 2 GB. - -Callbacks: `callbackUrl`, `callbackMethod` (default `POST`). - -**Behaviour notes.** With `recordSession="true"` the recording starts at once and runs until the call ends, and `timeout`, `finishOnKey` and `playBeep` are ignored. With `recordSession` or `startOnDialAnswer` set, the durations in the initial `action` request are `-1`; the real values arrive at `callbackUrl`. - -**Action parameters:** `RecordUrl`, `RecordingID`, `RecordingDuration`, `RecordingDurationMs`, `RecordingStartMs`, `RecordingEndMs`, `Digits`. - -**Callback parameters:** the same list without `Digits`. - -**Transcription parameters:** `transcription`, `transcription_charge`, `transcription_rate`, `duration`, `call_uuid`, `recording_id`, `error`. - -Recordings are deleted after 30 days, so download what you need. - -### Conference - -Joins a named room. The room name goes in the element body. Maximum 20 participants. Docs: . - -Basic: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `muted` | boolean | `false` | join muted, still hears others | -| `enterSound` | string | empty | `beep:1`, `beep:2`, or a URL | -| `exitSound` | string | empty | `beep:1`, `beep:2`, or a URL | -| `maxMembers` | integer | `20` | 1 to 20 | -| `timeLimit` | integer | `86400` | maximum seconds | -| `hangupOnStar` | boolean | `false` | member presses `*` to leave | -| `stayAlone` | boolean | `true` | keep the room open with one member left | - -Moderation: `startConferenceOnEnter` default `true`, `endConferenceOnExit` default `false`, `waitSound` a URL played while waiting for the room to start. - -Recording: `record` default `false`, `recordFileFormat` default `mp3`, plus `transcriptionType`, `transcriptionUrl` and `transcriptionMethod`. - -Callbacks: `action`, `method` (default `POST`), `callbackUrl`, `callbackMethod` (default `POST`), `redirect` default `true`. - -DTMF: `digitsMatch`, `floorEvent` default `false`, `relayDTMF` default `true`. - -An `enterSound` URL must return XML containing `Play`, `Speak` or `Wait`, and the same is documented for the MultiPartyCall hold-music URLs. The conference page states it for `enterSound` only, so read it as very likely true for `waitSound` and `exitSound` rather than as a published rule for those two. It is a URL that returns a document, not an audio file. - -**Action parameters:** `ConferenceName`, `ConferenceUUID`, `ConferenceMemberID`, `RecordUrl`, `RecordingID`. - -**Callback parameters:** `ConferenceAction` (`enter`, `exit`, `digits`, `floor`, `record`), `ConferenceName`, `ConferenceUUID`, `ConferenceMemberID`, `CallUUID`, `ConferenceDigitsMatch`, `RecordUrl`, `RecordingID`, `RecordingDuration`, `RecordingDurationMs`, `RecordingStartMs`, `RecordingEndMs`. - -### MultiPartyCall - -Creates or joins a multi party call with roles. The name goes in the element body. Docs: . - -Roles: `Customer`, `Agent`, `Supervisor`, and `ai-agent` for an AI agent connected over WebSocket streaming. - -MPC level: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `maxDuration` | integer | `14400` | 300 to 28800 seconds | -| `maxParticipants` | integer | `10` | 2 to 10 | -| `record` | boolean | `false` | record the MPC | -| `recordFileFormat` | string | `mp3` | `mp3` or `wav` | -| `recordMinMemberCount` | integer | `1` | 1 or 2 members before recording starts | -| `waitForAgent` | boolean | `false` | customers hear wait music until an agent joins | -| `recordCoachVoice` | boolean | none | include the supervisor's voice in the recording | -| `startRecordingAudio` | URL | none | XML for audio played when recording starts | -| `startRecordingAudioMethod` | string | `GET` | `GET` or `POST` | -| `stopRecordingAudio` | URL | none | XML for audio played when recording stops | -| `stopRecordingAudioMethod` | string | `GET` | `GET` or `POST` | - -Hold music: `waitMusicUrl` and `waitMusicMethod`, `agentHoldMusicUrl` and `agentHoldMusicMethod`, `customerHoldMusicUrl` and `customerHoldMusicMethod`. These URLs must return XML with `Play`, `Speak` or `Wait`. - -Callbacks: `statusCallbackUrl`, `statusCallbackMethod`, `statusCallbackEvents`, `recordingCallbackUrl`, `recordingCallbackMethod`. - -Participant level: - -| Attribute | Type | Default | Notes | -|---|---|---|---| -| `role` | string | required | `Agent`, `Supervisor`, or `Customer` | -| `mute` | boolean | `false` | join muted | -| `hold` | boolean | `false` | join on hold | -| `coachMode` | boolean | `true` | supervisors only. Agents hear the supervisor, customers do not | -| `stayAlone` | boolean | `false` | stay when alone | -| `startMpcOnEnter` | boolean | `true` | start the MPC on joining | -| `endMpcOnExit` | boolean | `false` | end the MPC on leaving | - -Entry and exit sounds: `enterSound` default `beep:1`, `exitSound` default `beep:2`, each accepting `none`, `beep:1`, `beep:2` or a URL, with `enterSoundMethod` and `exitSoundMethod` defaulting to `GET`. - -Actions: `onExitActionUrl`, `onExitActionMethod`, `relayDTMFInputs`. - -AI agent stream attributes, used only with `role="ai-agent"`: `aiAgentStreamServiceUrl`, `aiAgentStreamContentType` (default `audio/x-l16;rate=8000`), `aiAgentStreamStatusCallbackUrl`, `aiAgentStreamStatusCallbackMethod` (default `POST`), `aiAgentStreamSamplingRate`, `aiAgentStreamExtraHeaders`. The attributes are documented; whether this form behaves exactly like the streaming API is a question for the `plivo-audio-streaming` skill. - -**Status callback event groups:** `mpc-state-changes`, `participant-state-changes`, `participant-speak-events`, `participant-digit-input-events`, `add-participant-api-events`, `participant-audio-events`. Pass them as a comma separated list. - -**Status callback parameters:** `EventName`, `EventTimestamp`, `MPCUUID`, `MPCName`, `MemberID`, `ParticipantRole`, `ParticipantCallUUID`, `ParticipantCoachMode`, `MPCDuration`, `MPCBilledDuration`, `MPCBilledAmount`. - -**On exit parameters:** `MPCUUID`, `MPCFriendlyName`, `MemberID`, `ParticipantCallUUID`, `ParticipantJoinTime`, `ParticipantEndTime`, `ParticipantRole`. - -**Conference or MultiPartyCall.** Conference takes 20 participants and has no roles; MultiPartyCall takes 10, has roles, coach mode, per participant hold and mute, and fuller API control. Use Conference for a simple bridge and MultiPartyCall for a contact centre. - -**Docs disagreement.** The roles table writes the roles capitalised (`Customer`, `Agent`, `Supervisor`) while the AI role is lower case (`ai-agent`). Both cases are commonly seen for the human roles. This file follows the docs and writes `Agent`, `Supervisor`, `Customer`, `ai-agent`. - -### Message +## When the call fails -Sends an SMS from inside a call flow. The message text goes in the element body. Docs: . +70xx means the HTTP request failed: check the status, method and host, not the XML. 80xx means the body was wrong. Read the status line before the body, and name a code only when the evidence picks it. -| Attribute | Type | Notes | +| Code | Meaning | Check first | |---|---|---| -| `src` | string | sending number, must be one you own | -| `dst` | string | destination. Several numbers are separated by `<` | -| `type` | string | `sms` | -| `callbackUrl` | string | receives delivery reports | -| `callbackMethod` | string | `GET` or `POST`, default `POST` | - -`` is documented in the messaging XML reference, not in the voice XML reference, and it does not appear in the voice element table on the voice XML overview page. It works inside a call flow per the messaging page. +| 7011 (7012 action, 7013 transfer, 7014 redirect URL) | non-2xx or no response | a GET or POST mismatch; a 401 from Basic auth or a token on the URL (Plivo has no documented way to send credentials, so validate the signature instead); a missing route; a slow host. 7012 fails the call only when `redirect="true"` | +| 7022 to 7024, 7032 to 7034 | the action, transfer or redirect URL is not `http(s)`, or its method is not GET or POST | the attribute value | +| 8011 | the answer URL replied, but not with a Plivo document | the number is attached to a console flow application, which answers with JSON you never wrote (*observed*; check this first); a framework JSON or HTML error page; a Twilio element (`` *observed* as 8011; ``, ``, ``, `` are not Plivo elements); a raw `&` in an attribute; bytes before `` from a template; a non-table `` (untested) | +| 8012 (8013 transfer, 8014 redirect) | a later document was bad; 8012 fails the call only when `redirect="true"` | the action document nobody tested: it returns `OK`, JSON, or handles only the happy branch. Run step 2 of the loop on that URL | +| 4010 | the document ran out of elements | nothing, if the call was meant to end | +| 3020 or 3010 with hangup source Answer XML | *observed* for `` and `reason="busy"`; the docs describe these codes as the called party rejecting or busy | deliberate screening, or a forgotten placeholder | -### Standard request parameters +**8011 from a console flow application.** Confirm: `plivo numbers get -o json` (the `application` URI ends in the app id), `plivo account applications get -o json` (its `answer_url`), then the step 2 `curl` to it: JSON back is the cause. Fix: attach an XML application. -Sent with every request to an answer, action, redirect or fallback URL: `CallUUID`, `From`, `To`, `CallStatus`, `Direction`. - -- Inbound: `From` is the caller, `To` is your Plivo number, `Direction` is `inbound`. -- Outbound: `From` is the caller id you set, `To` is the destination, `Direction` is `outbound`. -- Outbound calls also carry `ALegUUID` and `ALegRequestUUID`. -- Forwarded calls may carry `ForwardedFrom`, subject to the carrier. -- Completed calls carry `HangupCause`, `Duration`, `BillDuration`, `TotalCost`. -- `CallStatus` values: `ringing`, `in-progress`, `completed`, `busy`, `failed`, `timeout`, `no-answer`. -- SIP calls carry custom headers with an `X-PH-` prefix. Sending `sipHeaders="CustomId=123"` produces `X-PH-CustomId=123`. - -Docs: . - -### Hangup causes seen in call records - -`NORMAL_CLEARING`, `USER_BUSY`, `NO_ANSWER`, `CALL_REJECTED`, `UNALLOCATED_NUMBER`, `NETWORK_OUT_OF_ORDER`. The numeric code list is at ; the ones caused by XML are in "What breaks, and how to tell which" below. - -## Patterns and the documents to copy - -Each document below is small and well formed. Replace every `example.com` URL, every `+1000000000x` number and every prompt with your own. Every attribute used is documented in the element reference above. - -### The four things a document can do +```bash +plivo account applications create --app-name --answer-url https://YOUR-HOST/plivo/answer --answer-method POST --fallback-answer-url https://YOUR-HOST/plivo/fallback --dry-run +plivo numbers update --app-id --dry-run +``` -Every Plivo XML document does one or more of these, in this order: +Neither prompts, so drop `--dry-run` only after the user agrees; the old app id is the rollback. Making the flow itself answer is out of scope. -1. **Say something.** `Speak`, `Play`. -2. **Ask something.** `GetDigits`, `GetInput`. -3. **Connect something.** `Dial`, `Conference`, `MultiPartyCall`, `Stream`. -4. **End or hand off.** `Hangup`, `Redirect`, or simply running out of elements. +The call's debug log in the console (Voice, Logs, Calls, the call) shows Plivo's parser message with a line and column, for example `not well-formed (invalid token): line 1, column 271`. `plivo voice calls diagnose ` reads it for you. -`Record` sits alongside all of them: it either records a message on its own, or records the session in the background. +Do not port Twilio XML as is. The Plivo forms are ``, ``, `` and ``. Build documents with an XML library or the Plivo SDK's XML classes so escaping is never done by hand. -If the document does not reach step 3 or step 4 the call still ends, because a document that runs out of elements hangs up. +## Three patterns ### Keypad menu ```xml - - For sales press 1. For support press 2. To hear this again press 3. + + For sales press 1. For support press 2. - We did not get a choice. Goodbye. - - -``` - -Rules: - -- The prompt goes **inside** `GetDigits`, so the caller can interrupt it by pressing a key. -- Only `Speak` and `Play` may go inside. -- The two elements **after** `GetDigits` are the no input path. Without them the call ends in silence after `retries` attempts. -- `validDigits` stops the caller entering something you have no branch for. -- Set `log="false"` when the digits are a PIN or a card number. -- The `action` document replaces this one, because `redirect` defaults to `true`. - -What that menu's `action` URL returns for one branch: - -```xml - - Connecting you to sales. - - +10000000001 - - -``` - -To loop back to the menu, have the action document return a `` to the menu URL. Give the loop an exit: a counter in the query string, or a `Hangup` after N passes. - -### Speech or keypad menu - -Same shape with `GetInput`, which the docs recommend for new work. - -```xml - - - Tell me what you need, or press 1 for sales and 2 for support. - - Sorry, I did not catch that. Goodbye. + We did not get a choice. Goodbye. ``` -The action URL receives `InputType` so you can tell which one the caller used, plus `Digits` or `Speech`. `hints` improves recognition of the words you actually expect. +- The prompt sits inside `GetDigits`, so a key press interrupts it. +- The two elements after it are the no-input path. Without them the call ends in silence. +- The `action` document replaces this one, because `redirect` defaults to `true`. Handle every `Digits` value plus an unexpected one. To repeat the menu, return a `` to the menu URL with an exit, such as a counter in the query string. +- For speech as well, use the same shape with `` and branch on `InputType`. -### Forwarding a call - -```xml - - - +10000000001 - - -``` - -- `callerId` is what the person you dial sees. Use a number you own. -- `timeout` is how long you ring before giving up. -- The `action` URL receives `DialStatus`, one of `completed`, `busy`, `failed`, `cancel`, `timeout`, `no-answer`. Handle all six, not just `completed`. -- With `redirect` left at its default, the action document takes over. With `redirect="false"` the next element in this document runs instead. - -**Ring several people at once:** several `` children in one ``. First to answer wins. - -```xml - - - +10000000001 - +10000000002 - +10000000003 - - -``` - -**Try people in turn:** several `` elements, each with its own `timeout`. - -```xml - - - +10000000001 - - - +10000000002 - - Sorry, nobody is available. Please try again later. - - -``` - -**Dial a SIP address:** `sipAuthUsername` and `sipAuthPassword` when the far end challenges. Failures come back as `sip_auth_failed` (4240) or `sip_auth_timeout` (4250) on the action URL. - -```xml - - - sip:queue@example.com - - -``` - -**Make sure a human, not a voicemail, takes it:** `confirmSound` returns a short document, and `confirmKey` is the key they must press to accept. - -**Silence while ringing:** set `dialMusic`, either a URL that returns a document or the literal `real` to pass the carrier's own ringing through. - -**Dial an extension:** ``, where each `w` is half a second. - -### Voicemail - -`Record` has the same `redirect` default as `GetDigits` and `Dial`: `true`. With an `action` URL set and `redirect` left alone, Plivo runs the document that URL returns when the recording completes, so a thank-you written under the `Record` may never play. That is the default row, not a documented statement that the rest of your document is discarded, so treat it as a risk to design around rather than a broken call. There are two correct shapes, and the difference is only where the thank-you lives. - -**Shape 1, everything in one document.** `redirect="false"` keeps control here, so the recording details are still posted to `action` and Plivo then carries on to the next element. - -```xml - - Leave a message after the beep, then press hash. - - Thank you. Goodbye. - - -``` - -**Shape 2, let the action URL finish the call.** Leave `redirect` at its default and put the thank-you in the document `/plivo/voicemail` returns. Use this when the thank-you depends on the recording, for example a different message when the caller hung up without speaking. - -```xml - - Leave a message after the beep, then press hash. - - -``` - -and `/plivo/voicemail` returns: - -```xml - - Thank you. Goodbye. - - -``` - -The `action` URL receives `RecordUrl`, `RecordingID` and the durations either way. `maxLength` defaults to only 60 seconds, so set it. - -**Forward, then voicemail.** Put the `Record` after the `Dial` in the same document. Both need `redirect="false"`: on the `Dial` so execution continues here when nobody answers, and on the `Record` so the thank-you below it still runs. +### Forward, then voicemail ```xml +10000000001 - Nobody is available. Leave a message after the beep, then press hash. - - Thank you. Goodbye. - - -``` - -### Recording a conversation - -```xml - - - This call is recorded for quality. - - +10000000001 - - -``` - -- `` goes **before** the thing you want recorded. It starts in the background and runs until the call ends. -- `startOnDialAnswer="true"` waits for the other party to answer, so you do not record the ringing. -- With `recordSession` set, `timeout`, `finishOnKey` and `playBeep` do nothing. -- The durations in the first `action` request are `-1`. Use `callbackUrl` for the real values. -- `stereo` puts each party on their own channel, which is what analytics tools want. `mono` is smaller. -- Tell the caller. Recording notice rules are legal, not technical. -- Recordings are deleted after 30 days. - -### Conference rooms - -Guests join with `startConferenceOnEnter="false"` and a `waitSound`; the host joins with `startConferenceOnEnter="true"` and usually `endConferenceOnExit="true"`. - -```xml - - weekly-standup - -``` - -```xml - - weekly-standup - -``` - -`waitSound`, `enterSound` and `exitSound` take a **URL that returns a document** containing `Play`, `Speak` or `Wait`, not an audio file directly. `beep:1` and `beep:2` are the built in shortcuts. - -A conference is also the simplest way to bridge two separate inbound calls: give both the same room name. Twenty participants maximum. - -### Rooms with roles - -Use `MultiPartyCall` when you need roles, coaching, or per participant hold and mute. - -```xml - - support-call-1 - -``` - -A supervisor joins the same room name with `role="Supervisor"` and `coachMode="true"`: agents hear them, customers do not. `onExitActionUrl` gives you a document to run when a participant leaves, which is where a post call survey goes. - -### Screening and out of hours - -```xml - - - -``` - -`rejected` gives a rejection tone, `busy` gives a busy signal. This is a legitimate pattern at scale: allow lists, blocked callers, closed hours. It shows up in call records as 3020 or 3010 with hangup source Answer XML, which is an observation and not a mapping the docs publish. - -A friendlier version says something first: - -```xml - - Our office is closed. We are open Monday to Friday, nine to five. + Nobody is available. Leave a message after the beep, then press hash. + + Thank you. Goodbye. ``` -Decide which one you want. They are not hard to tell apart later: a deliberate `` is seen in call records as 3020 with your answer document behind it, while a broken deployment records 7011 or 8011. Choose the rejection deliberately rather than by accident. - -### Custom ringback before answering +- `redirect="false"` on `Dial` lets an unanswered call fall through to the voicemail prompt; on `Record` it keeps the thank-you below it. Both `action` URLs are still called (log `DialStatus`, store `RecordUrl`) and their bodies are ignored. +- If the prompt must never follow a completed call, keep `redirect` at its default and have the dial `action` return the voicemail document only when `DialStatus` is not `completed`. -**Avoid `loop="0"` in a `PreAnswer` that has a `Dial` after it.** `loop="0"` is documented as an infinite loop and children run one at a time top to bottom, so the `PreAnswer` may never finish and the `Dial` below it may never be reached. Nothing documents that outcome, and the public routing page's custom-ringback example uses `loop="0"` in exactly this shape, so raise it as a risk and use a finite loop sized to your ring time rather than calling the document broken. +### Record the whole call ```xml - - https://example.com/audio/ringback.mp3 - - + + This call is recorded. + +10000000001 ``` -`loop="0"` is only correct when nothing needs to run afterwards, for instance a hold document returned to `dialMusic` or `waitSound`, where the element is meant to play until the call moves on for another reason. - -If what you actually want is ringback while you dial, `dialMusic` on `` is usually the better tool: it plays to the caller while the other leg rings, and the literal value `real` passes the carrier's own ringing through. - -`PreAnswer` takes only `Speak`, `Play` and `Wait`, and should stay under 30 seconds because the call is not answered yet and some carriers give up: those are the three limitations the page lists. Putting it first is this file's inference from what it does, not a documented rule. - -### SSML prompts - -SSML needs a `Polly.` voice. - -```xml - - Your reference is AB12Please keep it safe. - -``` - -Maximum 3,000 characters per ``. `` and `` are not supported. - -### Sending an SMS from a call - -`` sends an SMS mid flow. `src` must be a number you own. - -```xml - - Thanks. I am texting you the link now. - Here is the link you asked for: https://example.com/booking - - -``` - -### Acknowledging a callback - -```xml - -``` - -Return that, or an empty 200, to any `callbackUrl`, `ring_url` or hangup URL. Never return it from an answer URL unless you want the call to end. - -### XML around a voice bot - -If the call is going to a WebSocket voice bot, the bot workflow, readiness and debugging belong to **`plivo-audio-streaming`**, a separate install. What belongs here is the XML that surrounds ``, and its ordering rules: - -- **A greeting before the bot.** `` or `` before `` runs first, so it delays the bot by however long it takes. Keep it short. -- **A keypad menu in front of the bot.** `` before `` works like any other menu. Remember that `redirect` defaults to `true`, so a caller who presses a key gets the action document instead of the `` you wrote below it. A caller who presses nothing still falls through to it after `retries` attempts. Either set `redirect="false"`, or repeat the `` in the action document, and raise it as a risk rather than a broken document. -- **Recording.** `` goes **before** ``, for the same reason it goes before ``: it must be running while the audio flows. -- **Handing off to a human.** `` with `` or ``, and an `action` URL that reads `DialStatus`. Handle `busy`, `no-answer`, `timeout` and `failed`, not only `completed`. -- **Continuing after the bot.** `` after `` sends the call to a URL of yours when the stream ends, instead of the call ending. `` after `` ends it deliberately. -- **Putting the bot in a room.** `MultiPartyCall` with `role="ai-agent"` and the `aiAgentStream*` attributes. - -The `` element's own attributes, the WebSocket protocol, and every question about whether the bot is ready for production are out of scope here. Install `plivo-audio-streaming` (`npx skills add https://www.plivo.com/docs --skill plivo-audio-streaming`) or read . - -### A checklist before you ship a document - -1. Does it parse? Run it through any XML parser. -2. Is every `&` inside an attribute written `&`? -3. Does every element that can produce nothing have something after it? -4. Have you tested every URL the document names, each with the method it is actually configured with, and looked at the body? Most documented XML element URL methods default to `POST`, but not all: `startRecordingAudioMethod`, `stopRecordingAudioMethod`, `enterSoundMethod` and `exitSoundMethod` on `` default to `GET`. The `method`, `callbackMethod` or `transcriptionMethod` you set on the element wins, as does the method set on the application or on the API request. Test each URL with the method it is actually configured with rather than assuming. -5. Does the account have a Fallback Answer URL set? -6. Is `log="false"` on anything that collects a secret? -7. Does every `Redirect` loop have an exit? -8. Does the caller get told the call is recorded, when it is? - -## The URL contract: callbacks, signatures, timeouts - -Which URL Plivo calls, when, what it sends, what it expects back, and how to secure and tune it. - -### The four call level URLs +- `Record` comes first and runs in the background until the call ends. `startOnDialAnswer` skips the ringing, and the default `stereo` puts each party on its own channel. +- Take `RecordUrl` and the real durations from `callbackUrl`. +- Whether and how to tell the caller is the user's legal question; this skill states platform behaviour only. -Set on the voice application for inbound calls, or in the API request for outbound calls. Docs: . - -| URL | When | Expects | -|---|---|---| -| Primary Answer URL, or `answer_url` | as soon as the call is answered | one Plivo XML document | -| Fallback Answer URL, or `fallback_url` | when the primary answer URL is not reachable | one Plivo XML document | -| `ring_url` | when the call starts ringing | nothing. Return 200 | -| Hangup URL, or `hangup_url` | when the call is disconnected | nothing. Return 200 | - -`answer_url` is mandatory for an outbound API call, and a Primary Answer URL is mandatory on a voice application; the rest are optional. For an outbound API call, `fallback_url` is invoked if `answer_url` fails after 3 retries or a 60 second timeout (). Default method for each is `POST`. - -Set the fallback. Without it, a primary answer URL that flaps under load turns every call into a failed call. - -### The two element level URLs - -These come out of the XML itself, and mixing them up is a frequent bug. - -**`action`** expects XML back. Plivo runs whatever it returns. It is invoked at the end of an element's execution, for example when the caller has finished entering digits. - -**`callbackUrl`** expects nothing back. It is a notification about something that happened during an element's execution, for example a conference participant being muted. Return HTTP 200. An empty body or `` are both fine. - -Returning the word `OK` or a JSON status object to an `action` URL matters only when `redirect` is left at its default `true`, because only then does Plivo fetch that document to run it: the parse fails and the call ends with 8012. With `redirect="false"` the URL is still called but its answer is ignored, so the same body is harmless. Returning it to a `callbackUrl` is always harmless. - -`redirect` controls whether the `action` document takes over the call. It defaults to `true` on `GetDigits`, `GetInput`, `Record`, `Dial` and `Conference`. With `redirect="false"` the URL is still called, the answer is ignored, and the next element in your document runs. A bad or unreachable action document only fails the call when `redirect` is `true`. - -### What every request carries - -Standard parameters on the answer, fallback, action and redirect URLs: `CallUUID`, `From`, `To`, `CallStatus`, `Direction`. - -Direction changes the meaning of `From` and `To`: - -- inbound: `From` is the caller, `To` is your Plivo number. -- outbound: `From` is the caller id you set, `To` is the destination. - -Extra parameters: - -- outbound calls: `ALegUUID`, `ALegRequestUUID`. -- forwarded calls: `ForwardedFrom`, when the carrier supplies it. -- completed calls: `HangupCause`, `Duration`, `BillDuration`, `TotalCost`. -- SIP calls: custom headers with an `X-PH-` prefix. `sipHeaders="CustomId=123"` arrives as `X-PH-CustomId=123`. - -`CallStatus` values: `ringing`, `in-progress`, `completed`, `busy`, `failed`, `timeout`, `no-answer`. - -### Per element parameters - -The full lists are in the element reference above. In short: - -| Element | Action URL receives | Callback URL receives | -|---|---|---| -| `GetDigits` | `Digits` | none | -| `GetInput` | `InputType`, `Digits`, `Speech`, `SpeechConfidenceScore`, `BilledAmount` | interim speech: `StableSpeech`, `UnstableSpeech`, `Stability`, `SequenceNumber` | -| `Dial` | `DialStatus`, `DialRingStatus`, `DialHangupCause`, `DialALegUUID`, `DialBLegUUID` | `DialAction`, B leg status, duration, numbers, DTMF matches, hangup cause and source, `STIRVerification` | -| `Record` | `RecordUrl`, `RecordingID`, durations, `Digits` | the same without `Digits` | -| `Conference` | `ConferenceName`, `ConferenceUUID`, `ConferenceMemberID`, `RecordUrl`, `RecordingID` | `ConferenceAction` plus the same identifiers and recording fields | -| `MultiPartyCall` | `onExitActionUrl`: MPC and participant identifiers and times | `statusCallbackUrl`: `EventName` and the event's fields | - -### Securing the URLs - -Every request from Plivo carries `X-Plivo-Signature-V3`, `X-Plivo-Signature-Ma-V3` and `X-Plivo-Signature-V3-Nonce`. Validate the signature instead of protecting the URL with Basic auth, a bearer token or a secret in the query string. Plivo has no way to send your credentials, so an answer URL behind Basic auth or a bearer token is expected to reject Plivo's request and produce 7011. That is an inference, not a documented rule: no page states what Plivo does with a 401. - -Use your SDK's helper. Every Plivo server SDK has one, and the manual form is easy to get subtly wrong. - -How it is built (): Plivo takes the full request URL including scheme, port and query string; appends a `.`; appends the POST parameters sorted alphabetically by name with Unix style case sensitive sorting, as name then value with no separator between them; appends a second `.`; appends the nonce from `X-Plivo-Signature-V3-Nonce`; and signs the result with HMAC SHA256 using your Auth Token, Base64 encoded. On a GET the parameters are already in the query string, so the middle part is empty but both dots still stand. - -**The `.` separators are the part manual implementations miss.** The documented worked example, for URL `https://example.com/abcd?foo=bar` with POST parameters `CallUUID`, `Digits`, `From` and `To` and nonce `kjsdhfsd87sd7yisud2`, assembles to: - -```text -https://example.com/abcd?foo=bar.CallUuid4vbcpem8-0u46-x1ha-9af1-438vc92bf374Digits1234From+15551111111To+15555555555.kjsdhfsd87sd7yisud2 -``` - -A `.` between the URL and the sorted parameters, and a `.` before the nonce. Omit them and you compute a different string and reject every genuine request. Read the current page before shipping a hand written validator. - -If your account has more than one auth token, Plivo sends comma separated signatures and you must accept a match against any of them. - -V2 signatures are deprecated. - -If your server sits behind a proxy or load balancer, sign against the URL the client actually requested, not the internal one, or the signature will never match. - -### Timeouts, retries and edge region - -Plivo retries a webhook when it does not get a 200. You tune this with URL fragments appended to the callback URL, in the form `#key=value&key2=value2` (). - -| Key | Meaning | Allowed | Default | -|---|---|---|---| -| `ct` | connection timeout, milliseconds | 100 to 10000 | 2000 | -| `rt` | read timeout, milliseconds | 100 to 40000 | 40000 | -| `tt` | total timeout across retries, milliseconds | 100 to 55000 | 55000 | -| `rc` | retry count | 0 to 5 | 1 | -| `rp` | retry policy | `4xx`, `5xx`, `ct`, `rt`, `all`, comma separated | `ct,rt` | -| `er` | edge region | `nearest`, `local`, `n_california`, `n_virginia`, `frankfurt`, `singapore`, `mumbai` | `nearest` | - -Example: `https://example.com/answer?x=1#ct=2000&rt=3000&rc=3&rp=ct,rt`. - -The docs' "Applicable URLs" list is longer than most people expect. The fragments apply to: - -- **Voice application on the console:** Primary Answer URL, Fallback Answer URL, Hangup URL. -- **Make a call:** `answer_url`, `ring_url`, `hangup_url`, `fallback_url`, `machine_detection_url`. -- **Transfer a call:** `aleg_url`, `bleg_url`. -- **Recording and transcription:** `transcription_url` and `callback_url` on call and conference recording. -- **XML element URLs:** `action` and `callbackUrl` on the elements that take them, plus `confirmSound`, `dialMusic` and `waitSound`, and the interim speech results callback on `GetInput`. - -They do **not** apply to audio URLs used by `` or ``, which use fixed values: 2 second connection timeout, 120 second read timeout, retry count 1, retry policy `all`. A partial response is not retried. Read for the current list rather than assuming a URL is covered. - -### Idempotency - -Plivo may deliver the same callback more than once because of retries and network problems. Make every handler idempotent. - -Suggested keys: - -| Callback | Key | Note | -|---|---|---| -| `hangup_url` | `CallUUID` | the call ended, safe to process once | -| `ring_url` | `CallUUID` | can fire more than once when dialling several numbers | -| `action` URL | `CallUUID` plus the element context | the same action can be reached again after a retry | -| `recordingCallbackUrl` | `RecordingID` | recording ready | - -### Answer quickly, and answer with the right headers - -- Content-Type `application/xml` or `text/xml`. -- Under 100 KB. -- Under 15 seconds. -- HTTPS for every callback URL as a security rule (Plivo accepts `http://` too) and every audio URL. - -A note on the documented sample URL. `https://s3.amazonaws.com/static.plivo.com/answer.xml` is Plivo's own test answer URL. It is a static object, so it answers GET but rejects POST. Used as an answer URL with the default POST method, or reused as a hangup URL, it returns an HTTP 405 and an XML error document rather than Plivo XML. Point real applications at your own endpoint (commonly seen). - -## What breaks, and how to tell which - -The codes Plivo reports when a document is wrong or unreachable, and the shapes that cause them. Codes come from ; the shapes are commonly seen in practice. - -### The codes - -**XML errors, 8011 to 8014.** Your URL replied, but not with a document Plivo could use. - -| Code | Name | Which document | -|---|---|---| -| 8011 | Invalid Answer XML | the answer URL's document, the first one of the call | -| 8012 | Invalid Action XML | an `action` URL's document. Only fails the call when `redirect="true"` | -| 8013 | Invalid Transfer XML | a transfer URL's document | -| 8014 | Invalid Redirect XML | a `` URL's document | - -**URL errors, 7011 to 7034.** Plivo never got a usable HTTP response, so there is no document to inspect. - -| Code | Name | Meaning | -|---|---|---| -| 7011 | Error Reaching Answer URL | non 2xx from the answer URL | -| 7012 | Error Reaching Action URL | non 2xx from an action URL. Only fails when `redirect=true` | -| 7013 | Error Reaching Transfer URL | non 2xx from a transfer URL | -| 7014 | Error Reaching Redirect URL | non 2xx from a redirect URL | -| 7022, 7023, 7024 | Invalid Action, Transfer, Redirect URL | the URL is not a valid `http://` or `https://` URL | -| 7032, 7033, 7034 | Invalid Method | a method other than GET or POST | - -**Normal ends that people mistake for faults.** - -| Code | Name | Meaning | -|---|---|---| -| 4010 | End Of XML Instructions | the document ran out of elements. Normal for an XML controlled call | -| 3020 | Rejected | the hangup-causes table describes this as the called party rejecting. Observation, not documented: call records for a `` answer document carry 3020 with hangup source Answer XML, and the routing page documents only the rejection tone | -| 3010 | Busy Line | the hangup-causes table describes this as the destination being busy. Same observation for ``; the routing page documents only the busy signal | - -The difference between a 70xx and an 80xx is the difference between "your server did not answer" and "your server answered with the wrong thing". Do not debug the XML for a 7011. Read the HTTP status code, the method and the host. - -### The shapes that produce 8011 and 8012 - -All of these are commonly seen. Roughly in order of how often they turn up. - -**1. A JSON body where XML should be.** The single most common shape. Two sources. - -A number is attached to a **console flow application** rather than to your XML application. The flow answers the call instead of your answer URL, and if the flow itself cannot run you get a JSON body you did not write. Plivo publishes no schema for that body, so match on "this is JSON and I did not write it" rather than on any particular field. - -Or your own framework returns its JSON error envelope: - -```json -{"code":404,"message":"This webhook is not registered for POST requests. Did you mean to make a GET request?"} -``` - -Fix. Check what the number is actually attached to before you debug your code. Then make the endpoint answer POST, and make its error path return a valid document such as `We are sorry, we cannot take your call right now.` rather than a JSON error. - -**2. An HTML page.** Your web framework returned a page instead of a document. The parser has nothing to work with, because it read to the end without finding a Plivo root element. Plivo does not publish the parser messages, so match on the body you sent rather than on any particular error string. The body starts with something like ``. This is a 404 page, a login page, or a single page application catch all route swallowing the path. Large pages also break the 100 KB limit. - -Fix. Request the URL yourself with POST and look at the first 40 bytes of the body. If it starts with ` - - -``` - -Written with a bare `&`, Plivo's parser reports `not well-formed (invalid token)` at the column of the `&`. Write `&`, as above. Use your language's XML builder, or the Plivo server SDK, and this cannot happen. - -**5. A Twilio element.** - -```xml - - - -``` - -``, ``, ``, `` and `` are Twilio elements and none of them is in Plivo's element list, so a document built from them has no element Plivo can run. Answer documents with a top-level `` have been observed to fail with 8011; a top-level `` and a nested `` have been seen to be ignored instead, and the docs do not say what Plivo does with an unrecognised element. Fix all of them, and name a code only for ``. Plivo's equivalents are `` as shown, plus ``, `` and ``. Ported Twilio code also tends to keep Twilio's URL paths, which is a useful clue. - -**6. An empty or half filled template.** - -```xml - - Connecting you now. - - - - -``` - -An empty ``, an empty `sendDigits=""`, or a whole `` because the template rendered no branch. An empty `` is not a parse error, but it hangs the call up the moment it runs. Returned to a `callbackUrl` it is a perfectly good acknowledgement; returned to an answer URL it is a dropped call. - -Fix. Omit the element rather than emitting it empty, and give the template a default branch. - -**7. An unsupported language or voice.** - -```xml - - - Please say your order number. - - -``` - -The documented speech language list is printed as partial, so a code that is not on it is untested rather than known to be rejected: the docs do not say what happens to one. The same applies to `` outside the documented table unless you are using a `Polly.` voice that covers it. - -Fix. Use a code from the tables in the element reference above, or a Polly voice that covers the language. Test any code that is not listed before you rely on it. - -**8. A second document nobody ever tested.** 8012 usually arrives well into a call that was working. The answer document was correct; the `action` document was never exercised. Common instances: the `GetDigits` action URL that only handles the happy path, the `Dial` action URL that returns `OK`, the `Record` action URL that returns the recording as JSON. - -Fix. Test every URL your document names, not just the first one. For each, send a POST with the parameters that element sends and check the body is a valid ``. - -**9. Leading bytes before the root.** The docs make no Plivo specific claim here, so this is ordinary XML behaviour, checked against `xmllint` and Python's ElementTree. An XML declaration must be the first thing in the document: put a newline or any other whitespace in front of `` as the whole answer document.** This is deliberate screening: an allow list, a blocked caller, or out of hours. Call records for this shape show 3020 with hangup source Answer XML and zero duration, which is an observation rather than a documented mapping. It is only a bug when someone left it in as a placeholder. -- **`` returned to a `callbackUrl` or a hangup URL.** An acknowledgement. Correct. -- **A document with no `` and no `` at all.** Speak or play something, then hang up. That is a whole class of production traffic: out of hours messages, deflection announcements, notifications. -- **4010 End Of XML Instructions.** The document finished. Nothing is wrong. - -### How to debug one call - -1. Read the code first. 70xx means HTTP, 80xx means the body, 4010 means it worked and ended. -2. For 80xx, get the exact bytes. Console, Voice, Logs, Calls, the call, then the debug log. Plivo records its parser's own message there, for example `XML Parsing Error: Invalid XML Syntax: not well-formed (invalid token): line 1, column 271`. The line and column point straight at the problem. `plivo voice calls diagnose ` reads the same call for you. -3. Reproduce it yourself with the same method and the same parameters: - -```bash -curl -s -i -X POST https://example.com/plivo/answer \ - -d 'CallUUID=test&From=%2B10000000000&To=%2B10000000001&Direction=inbound&CallStatus=ringing' -``` +## Securing the URLs -Read the status line, the `Content-Type` and the first 40 bytes of the body. Most 8011s are visible in those three things. +- Validate `X-Plivo-Signature-V3`, with the nonce from `X-Plivo-Signature-V3-Nonce`, using the SDK helper: `plivo.utils.validate_v3_signature` (Python), `plivo.validateV3Signature` (Node), `Plivo::Utils.valid_signatureV3?` (Ruby). A hand-built string is easy to get subtly wrong. +- Sign the URL Plivo actually requested (scheme, host, port, path and query), not the one a proxy or load balancer forwards to your app. +- `X-Plivo-Signature-V3` uses the Auth Token of the account or subaccount that owns the request entity, such as the number; `X-Plivo-Signature-Ma-V3` always uses the main account's. With several active tokens Plivo sends comma-separated signatures: accept a match on any of them. +- Prefer this to a secret in the URL. Basic auth or a bearer token on the URL makes Plivo's request fail with a 401, which is a 7011. +- The worked example string on the docs page writes `CallUuid` and adds a `Caller` parameter; it is not a test vector. Full recipe: `plivo docs show voice/concepts/signature-validation`. -4. For 8012, run the same request against the `action` URL with that element's parameters, not against the answer URL. -5. If the body is right and the call still fails, check the size (100 KB), the content type (`application/xml` or `text/xml`) and the time to first byte (under 15 seconds). +## Where the docs disagree -Sources: the Voice XML, callbacks and troubleshooting sections of the Plivo docs (pages cited above). Where a rule says "commonly seen", it is an observation from practice with no docs page behind it: treat it as a thing to check, not as a Plivo commitment, and re-verify it if the platform changes. +- **`Speak voice`.** The audio-output attribute table allows only `WOMAN` and `MAN`; the same page's SSML section and the SSML page use `Polly.`. Follow the SSML pages. +- **Conference `stayAlone`.** The current row reads "End conference if only one member" with default `true`. Earlier versions of the page said `false` ends the conference when a member is alone (and once any member joins with `false`, it stays `false`), and the page's own two-caller bridge needs a lone first caller to stay. Follow that: the default `true` keeps a lone member in the room. MultiPartyCall's `stayAlone` is a different setting with a different default: `false`, described as "Stay if only participant". +- **MultiPartyCall role casing.** The roles table writes `Customer`, `Agent`, `Supervisor` and `ai-agent`; the page's first example writes `role="customer"`. Use the table's casing. +- **Timeouts and fallback.** The XML overview says Plivo waits 15 seconds for XML; the Calls API says `fallback_url` is used after 3 retries or a 60 second timeout; the callback-configurations page gives defaults of `rt=40000` ms, `rc=1`, `tt=55000` ms and `rp=ct,rt` (retry only on a connection failure or read timeout). Answer well inside 15 seconds, and set the fragment explicitly when failover timing matters. +- **Examples that break the rules above.** The routing page's sequential-dial example sets `action` with the default `redirect` and still expects the next `` and `` to run, and its custom-ringback example puts `` inside `` before a ``. Set `redirect="false"` and a finite `loop` when you copy them.