Prerequisite: Claude Code (CLI, desktop, or an IDE extension). The skill is a small Markdown file you drop into your project — install it in one command below.
What the agent does
Theplivo-to-vobiz agent is a Claude Code skill that reads your existing Plivo code and rewrites it to Vobiz in place — not a regex find-and-replace, but a structural port that understands the three things that actually change between the platforms: the auth model, the resource layout, and a handful of attribute renames. It walks every Plivo touchpoint in your repo — the SDK client constructor, client.calls.* calls, the PlivoXML you return from your answer URL, and your webhook-signature checks — and emits the Vobiz equivalent that compiles and runs against https://api.vobiz.ai/api/v1. Where Plivo did something in one call that Vobiz expresses as two (a split resource) or as XML (a redirect), the agent makes that translation explicitly rather than leaving a stub, so the call flow you had on Plivo behaves identically on Vobiz. Everything below is mechanical and verifiable, which is exactly why an agent can do it reliably across a whole codebase.
Capability map
The agent recognizes each Plivo construct and rewrites it to its Vobiz form. The “why” column is the load-bearing part — it’s the reason a blind rename would break, and the reason the agent threads state through your code instead of just swapping tokens.| Plivo construct | Agent rewrites it to | Why the change is needed |
|---|---|---|
plivo.RestClient(AUTH_ID, AUTH_TOKEN) | Vobiz(api_key=AUTH_ID, auth_token=AUTH_TOKEN) | Different SDK package and constructor. In the Vobiz SDK api_key is your Auth ID (MA_…); auth_token is the secret. |
| HTTP Basic auth on the wire | X-Auth-ID + X-Auth-Token request headers | Vobiz authenticates with explicit headers, not Basic. Any raw-HTTP or signature-verification code must change, not just the SDK calls. |
client.calls.create(...) | client.calls.make_call(auth_id=…, …) | Method rename plus auth_id becomes an explicit per-call argument — every account-scoped call carries it, so the agent threads auth_id through each call site. |
to_= keyword | to= | Vobiz drops the trailing underscore on to (but keeps from_ to avoid Python’s reserved word). A silent rename that breaks if missed. |
implicit answer_method | explicit answer_method='POST' | Plivo defaults the method; Vobiz make_call expects it passed. The agent adds it so the answer URL is fetched correctly. |
client.calls.play / speak / send_digits / record / hangup | client.play_audio.call, client.speak_text.call, client.dtmf.send_dtmf, client.record_calls.start_recording, client.live_calls.hangup_call | In-call actions are separate resources in Vobiz, each keyed by (auth_id, call_uuid). One Plivo namespace fans out into several dedicated ones. |
client.calls.transfer(uuid, …) | return <Redirect> from your answer XML | Vobiz models a transfer as a control-flow redirect in VobizXML rather than a REST verb — the agent moves the logic into the answer handler. |
plivoxml.ResponseElement() | vobizxml.ResponseElement() | Same builder shape, different package. The <Response> wrapper and nesting rules carry over verbatim. |
<GetDigits> / <GetInput> (add_get_digits / add_get_input) | <Gather> (add_gather, with input_type='dtmf') | Both Plivo input verbs unify into one Vobiz verb. The agent also adds input_type and remaps the timeout attributes below. |
timeout on GetDigits, digitTimeout | executionTimeout, digitEndTimeout | Attribute renames. In VobizXML timeout belongs to <Dial>/<Number> (ring timeout), so reusing it on <Gather> would be wrong — hence the distinct executionTimeout. |
<Conference maxMembers=…> | <Conference maxParticipants=…> | Attribute rename inside an otherwise-identical verb. |
<Record> without action | <Record action=…> | action is required in Vobiz so it can deliver RecordUrl — the agent fills in or flags the missing attribute. |
| Plivo webhook signature check | Vobiz header-based signature validation | Auth model differs end to end; the verification snippet changes alongside the client. See webhooks. |
Before → after: outbound call with auth_id threading
The headline transform. Beyond the method rename, the agent makes auth_id an explicit argument at the call site — the single biggest semantic shift in the migration, because Vobiz scopes every account operation to an Auth ID you pass in rather than one baked into the client.
api_key= / auth_token=, create becomes make_call, to_ loses its underscore, and auth_id appears as a first-class parameter. The agent applies all four edits together so the call still fires (message: "Call fired") instead of throwing on an unexpected keyword.
Before → after: IVR menu (PlivoXML GetDigits → vobizxml Gather)
The most common XML port. GetDigits and GetInput both collapse into <Gather>, which is why the agent adds input_type='dtmf' (so a former GetDigits keeps collecting keypad input) and renames the timeout attributes to their Gather-specific names.
add_get_digits is kept as an alias on the Vobiz builder, but the attribute names don’t carry over — timeout on a Plivo <GetDigits> is a collection window, while Vobiz reserves timeout for ring duration on <Dial>/<Number> and names the Gather window executionTimeout. A blind rename would either drop the timeout or apply it to the wrong concept. The action-URL payload stays parameter-compatible (both POST Digits), so your menu handler needs no changes. See the full verb table in call-control XML.
Before → after: in-call action (calls.play → play_audio.call)
Plivo’s live-call actions all hang off the calls namespace. Vobiz splits them into dedicated resources, each addressed by (auth_id, call_uuid). The agent maps each verb to its resource and threads both keys through.
calls.play and calls.speak don’t map to two methods on one object, they map to two different resources (play_audio, speak_text). The agent knows the full fan-out, attaches auth_id as the first positional argument on each, and preserves the call_uuid so the action lands on the right live call. Full side-by-side examples for every in-call verb live in code snippets.
A migration strategy that works
A Plivo-to-Vobiz move is mostly mechanical, but “mostly” is where projects stall. Theplivo-to-vobiz agent works best when you give it a phased plan, a clear definition of “done” for each surface, and a way to prove equivalence before you flip live traffic. The playbook below is how the agent sequences the work — and exactly what you should check at each gate.
The guiding principle: change one surface at a time, keep the old path runnable, and verify with bytes and signatures rather than vibes.
The phased plan
Each phase below is a unit of work the agent completes and you review before moving on. Run them in order — later phases assume earlier ones are green.Inventory
The agent greps your codebase for every Plivo touchpoint:
plivo.RestClient / new plivo.Client, plivoxml, calls.*, numbers.*, validate_v3_signature, and any hardcoded api.plivo.com host. It produces a surface map (REST calls, XML, webhooks, numbers) so nothing migrates by surprise.
Check: the inventory list matches your own mental model — no stray scripts, cron jobs, or Lambda handlers left out.Auth & host
Swap the client constructor to
Vobiz(api_key=AUTH_ID, auth_token=AUTH_TOKEN) (Node: new VobizClient({ apiKey, authToken })) and point raw-HTTP code at https://api.vobiz.ai/api/v1. Vobiz authenticates with X-Auth-ID / X-Auth-Token headers, not HTTP Basic.
Check: one authenticated read (e.g. client.account.retrieve_account()) returns 200. See auth & base URL.SDK calls
The agent rewrites each REST call:
calls.create → calls.make_call, threads an explicit auth_id through every account-scoped method, renames to_ → to, and always passes answer_method. In-call control splits into dedicated resources — play_audio, speak_text, dtmf, record_calls, and live_calls — keyed by (auth_id, call_uuid).
Check: every migrated call site passes auth_id; no client.calls.play/speak/send_digits survivors remain. Reference: code snippets.XML / answer URL
The agent converts PlivoXML to VobizXML:
<GetDigits> and <GetInput> both become <Gather>, timeout → executionTimeout, digitTimeout → digitEndTimeout, maxMembers → maxParticipants. Builders move from plivoxml to vobizxml with the same ResponseElement / add_* shape, served as application/xml.
Check: answer-URL responses byte-compare against the converted reference (see checklist). <Record action="…"> is present — Vobiz requires it.Webhooks
Replace
plivo.utils.validate_v3_signature with the Vobiz HMAC validator: V3 signs baseURL + "." + nonce, V2 signs baseURL + nonce, query stripped. Branch on the Event field (Ring / StartApp / Hangup) instead of separate ring/hangup URLs, and handle the parent-signed X-Vobiz-Signature-MA-V3 on sub-account callbacks.
Check: a replayed real callback validates true, and a tampered one validates false. Reference: webhooks.Numbers / provisioning
Search-and-buy moves from Plivo’s carrier catalog to a Vobiz pre-provisioned inventory:
client.phone_numbers.list_inventory_numbers(auth_id, country='US') then purchase_from_inventory(auth_id, e164='+1…'). The agent maps each numbers.search/buy/list/delete to its phone_numbers equivalent.
Check: the numbers you depend on exist in inventory and are assigned to the right app/answer URL before cut-over.Verify
The agent runs the full verification checklist below against staging — auth smoke test, XML byte-compare, signature round-trip, and a short parallel run — and reports any drift.
Check: every item is green. Treat a single red as a blocker, not a footnote.
Effort & confidence per surface
How much work each surface is, and how confident you can be in an automated port. “Higher” means more human review, never “harder to do in Vobiz.”| Surface | Effort | Confidence | Why |
|---|---|---|---|
| REST calls | Medium | High | Mechanical renames (create → make_call), an added auth_id, and split in-call resources. Deterministic and easy to diff. |
| Call-control XML | Low | High | Near drop-in. Only <GetDigits>/<GetInput> → <Gather> and a few attribute renames; the converter applies them verbatim. |
| Webhooks | Medium | Medium | Signature scheme and event model change. The code is short but security-critical, so it earns a real round-trip test, not just a compile. |
| Numbers / provisioning | Higher | Medium | Model shifts from live carrier search to a pre-provisioned inventory with per-action assignment — confirm availability and assignment by hand. |
Verify before cut-over
Do not flip traffic until all four pass. The agent can run each, but you sign off.- Auth smoke test. One authenticated read against
api.vobiz.ai/api/v1with the new headers (e.g.client.account.retrieve_account()) returns 200. Proves credentials, host, and header auth in a single call. - XML byte-compare. For each answer-URL route, capture the rendered VobizXML and diff it against the converter’s reference output. Confirm
<Gather>usesexecutionTimeout(5–60s, default 15) — nevertimeout, which belongs to<Dial>/<Number>— and that every<Record>carries anaction. - Signature check. Replay a captured real callback through the new validator: a genuine request must validate true, a byte-flipped copy must validate false. Compare with
hmac.compare_digest/crypto.timingSafeEqual, never==, and read nonce headers case-insensitively. Test theMAparent-signed header too if you use sub-accounts. - Parallel run. Point a small slice of traffic (or a synthetic test line) at the Vobiz path while Plivo still serves production. Compare call outcomes, recordings, and
Hangupcallbacks side by side for a fixed window before widening.
Safe rollback
Cut-over is a config change, not a code rewrite, so rollback is cheap — keep it that way. Leave your Plivo numbers, apps, and answer URLs configured and idle (do not delete them) until Vobiz has run clean through a full business cycle. Because the switch is “which platform owns the live number’s answer URL,” reverting is repointing that URL back — no redeploy required if you keep both code paths shipped behind a flag. Roll back the moment a verification gate that was green goes red in production: restore the Plivo answer URL, confirm the next inbound call hits the old path, then diagnose offline. Migrate one application or number pool at a time so a rollback affects a slice, never the whole estate. For the full per-surface mapping the agent applies, see the Plivo migration overview and the interactive converter.Install
Paste this in your project root. It creates.claude/skills/plivo-to-vobiz/ and writes the skill file — that single file is the installable agent. Then reload Claude Code and the skill appears as /plivo-to-vobiz.
Install the skill (one command)
Working with the agent
Theplivo-to-vobiz agent is a Claude Code skill that reads your Plivo code, rewrites it to the Vobiz SDK and VobizXML, and explains every change. You drive it with plain-language prompts. The more precise you are about scope (one file, one handler, just the webhooks) and target (Python vs Node, SDK vs raw XML), the cleaner the diff. Treat it like a fast pair-programmer who already knows the full Plivo to Vobiz mapping and the code-snippets playbook by heart.
How to prompt it
- Name the file or symbol. “Migrate
outbound.py” beats “migrate my code.” The agent works best when it can open a concrete file and show a focused diff. - State the language and surface. “Keep it Python, SDK only” or “this is the Flask
/answerhandler that returns PlivoXML.” - Say what to leave alone. “Don’t touch the billing module” keeps the diff reviewable.
- Ask for the reasoning inline. The agent will annotate each rename (
calls.create→calls.make_call,to_→to, threadedauth_id) so you can verify rather than trust.
Example session 1 — single-file SDK migration
make_call, auth_id passed in, and to_ → to. The full method-by-method table is in Voice Call API.
Example session 2 — converting a Flask /answer handler + its PlivoXML
<GetDigits>/<GetInput> both become <Gather> — and the timeout → executionTimeout attribute move. Everything else (Speak, the action payload that POSTs Digits) ports verbatim. See Call-control XML for the full verb table.
Advanced prompts
Once you trust the single-file flow, scale it up:- Whole-repo swap — “Migrate every Plivo call site in this repo to Vobiz. Walk the tree, convert each file, and give me one summary of renames per file.” The agent enumerates imports of
plivo/plivoxml, converts each, and threadsauth_ideverywhere. - Batch-convert all XML templates — “Convert every
*.xmlundertemplates/from PlivoXML to VobizXML. ApplyGetDigits/GetInput→Gather,timeout→executionTimeout,digitTimeout→digitEndTimeout,maxMembers→maxParticipants.” Good when your answer URLs serve static XML instead of building it in code. - Migrate just webhooks — “Only touch the signature-verification code. Replace
plivo.utils.validate_v3_signaturewith the Vobiz HMAC validator and branch on theEventfield.” The agent rewrites the verifier to signbaseURL + "." + nonceand readX-Vobiz-Signature-V3; details in webhooks. - Convert in-call control — “Find every
client.calls.play/speak/send_digits/recordand split them into the Vobiz resources.” Turns monolithiccalls.*calls intoplay_audio,speak_text,dtmf,record_calls, andlive_calls.
Review the agent’s output
The agent is fast and consistent, but you still own the diff. Check these four things on every migration — they’re the changes that silently break a call flow if missed:auth_idis threaded through every account-scoped call.make_call,play_audio.call,speak_text.call,dtmf.send_dtmf,record_calls.start_recording, and alllive_calls.*methods takeauth_idas the first argument. A converted line missing it is the most common slip.status="live"was added to live-call lookups.client.calls.get(uuid)andclient.calls.list(status='live')becomeclient.live_calls.get_live_call(auth_id, uuid, status="live")andlist_live_calls(auth_id, status="live"). Thestatus="live"is required — confirm it’s present.answer_methodis present onmake_call. Plivo defaults it toPOST; Vobiz wants it explicit. If the agent dropped it, the call won’t fetch your XML the way you expect.- The webhook signature is fully rewritten, not aliased. Verify the validator now signs the base URL + nonce (V3 uses the
.separator, V2 doesn’t), readsX-Vobiz-Signature-V3, and compares withhmac.compare_digest/crypto.timingSafeEqual— never==. A leftovervalidate_v3_signaturecall against Plivo’s lib is a red flag.
to_ → to on make_call, application/xml still set on answer responses, and add_record(action=...) carrying a required action in Vobiz.
Troubleshooting & FAQ
Q: The agent left aclient.calls.transfer(...) call in my code. Is that a bug?
Vobiz handles call transfer through call control rather than a REST verb. Ask the agent: “rewrite the transfer to return a <Redirect> from the answer URL.” It will replace the transfer call with a vobizxml response that emits <Redirect> pointing at the new flow — same outcome, driven from your answer handler.
Q: I’m getting “method not found” on client.calls.play (or .speak / .record) after migrating.
Those in-call actions are separate resources in Vobiz, not methods on calls. The fix is the resource split: client.calls.play → client.play_audio.call(auth_id, call_uuid, ...), speak → client.speak_text.call(...), send_digits → client.dtmf.send_dtmf(...), record → client.record_calls.start_recording(...), hangup → client.live_calls.hangup_call(...). Re-run with “split the calls.* in-call methods into their Vobiz resources.”
Q: My webhook signature check fails after migration even though the request is real.
Two usual causes: the validator is hashing the POST params (Plivo’s V3 does; Vobiz V2/V3 sign only baseURL + nonce), or the query string wasn’t stripped from the URL before hashing. Confirm the agent strips the query, picks the right separator (. for V3, none for V2), and keyed the HMAC with your Vobiz auth token. For sub-account callbacks, also validate X-Vobiz-Signature-MA-V3 with the parent account token.
Q: My IVR stopped collecting digits after the GetDigits → Gather conversion.
Check the attribute renames the agent should have applied: timeout → executionTimeout (5–60s window) and digitTimeout → digitEndTimeout, plus an explicit input_type="dtmf". In VobizXML timeout belongs to <Dial>/<Number> (ring timeout), so a stray timeout on <Gather> is ignored. Re-prompt: “remap the Gather timing attributes per the call-control XML reference.”