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
Thetwilio-to-vobiz agent is a Claude Code skill that reads your existing Twilio 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 verb and attribute renames. It walks every Twilio touchpoint in your repo — the Client(...) constructor, client.calls.create(...), the mono‑resource client.calls(sid).update(...) you use for live‑call control, the TwiML you return from your voice URL, and your RequestValidator signature checks — and emits the Vobiz equivalent that compiles and runs against https://api.vobiz.ai/api/v1. Where Twilio expressed something imperatively (mutating a live call through update()) or inline (twiml=), Vobiz has a clearer shape — a dedicated resource keyed by (auth_id, call_uuid), or a declarative <Redirect> returned from your answer URL — and the agent makes that translation explicitly rather than leaving a stub, so the call flow you had on Twilio 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 Twilio 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.| Twilio construct | Agent rewrites it to | Why the change is needed |
|---|---|---|
Client(ACCOUNT_SID, 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_…, sent as X-Auth-ID); the positional account SID becomes the keyword api_key. |
client.calls.create(to=, from_=, url=, method=) | client.calls.make_call(auth_id=…, from_=, to=, answer_url=, answer_method=) | Method rename plus auth_id becomes an explicit per‑call argument. url → answer_url and method → answer_method, so the answer webhook still fires. |
url= / method= on the call | answer_url= / answer_method= | Keyword rename for the answer webhook. The request/response model is identical — only the parameter names move. |
inline twiml="<Response>…" | serve VobizXML from your answer_url | Vobiz executes the XML your answer_url returns, so the agent lifts inline TwiML into the answer handler and returns it as application/xml. |
client.calls(sid).update(twiml="<Play>…") | client.play_audio.call(auth_id, call_uuid, urls=) | In‑call actions are dedicated resources in Vobiz, each keyed by (auth_id, call_uuid). One update() fans out into a purpose‑named method. |
client.calls(sid).update(twiml="<Say>…") | client.speak_text.call(auth_id, call_uuid, text=, voice=, language=) | Same fan‑out: the spoken‑text action gets its own resource, so the intent is on the method name instead of buried in inline XML. |
sendDigits at create time | client.dtmf.send_dtmf(auth_id, call_uuid, digits=) | Sending DTMF into a live leg becomes a first‑class call keyed by call_uuid. |
client.calls(sid).recordings.create() | client.record_calls.start_recording(auth_id, call_uuid) | Recording is its own resource (record_calls / recordings), not a sub‑collection of the call. |
client.calls(sid).update(status="completed") | client.live_calls.hangup_call(auth_id, call_uuid) | Ending a live leg is an explicit live_calls action rather than a status mutation. |
client.calls(sid).update(url=, method=) | return <Redirect> from your answer flow | Vobiz models a mid‑call redirect declaratively in VobizXML rather than an imperative REST verb — the agent moves the logic into the answer handler. |
client.calls(sid).fetch() / client.calls.list(status=…) | client.live_calls.get_live_call/list_live_calls(auth_id, …, status='live') + client.cdr.list_cdrs(auth_id, …) | Twilio’s one calls resource returns both live and completed legs; Vobiz separates in‑flight (live_calls, needs status='live') from completed history (cdr). |
VoiceResponse() · str(resp) | vobizxml.ResponseElement() · resp.to_string() | Same builder shape, different package. The <Response> wrapper and nesting rules carry over verbatim. |
response.say(...) → <Say> | add_speak(...) → <Speak> | Verb rename inside an otherwise‑identical document; voice/language/loop carry over. |
response.gather(input=, timeout=, speech_timeout=) | add_gather(input_type=, execution_timeout=, speech_end_timeout=) | Attribute renames. In VobizXML timeout belongs to <Dial>/<Number> (ring timeout), so the Gather collection window is named executionTimeout — reusing timeout would be wrong. |
<Pause> · <Client>/<Sip> | <Wait> · <User> | Verb renames. A single <User> reaches SIP endpoints and softphones and carries sendDigits / sipHeaders. |
available_phone_numbers('US').local.list() + incoming_phone_numbers.create(phone_number=) | phone_numbers.list_inventory_numbers(auth_id, country=) + purchase_from_inventory(auth_id, e164=) | Provisioning moves from a live carrier catalog to a Vobiz pre‑provisioned inventory with per‑action assignment. |
RequestValidator on X-Twilio-Signature (HMAC‑SHA1) | Vobiz X-Vobiz-Signature-V3 (HMAC‑SHA256 + nonce) validation | Auth model differs end to end; the algorithm, header, and signed string all change, so the verifier is rewritten — not aliased. 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.
Client(sid, token) args to keyword api_key= / auth_token= (where api_key is your Auth ID), create becomes make_call, url/method become answer_url/answer_method, and auth_id appears as a first‑class parameter. The agent applies all of these together so the call still fires instead of throwing on an unexpected keyword. Full method table in Voice Call API.
Before → after: IVR menu (TwiML Gather → vobizxml Gather)
The most common XML port. VoiceResponse becomes vobizxml.ResponseElement, and the agent renames the Gather attributes to their VobizXML names so a former TwiML menu keeps collecting keypad input.
VoiceResponse() and add_gather mirror TwiML’s builder, but the attribute names don’t carry over — input becomes inputType, and timeout on a TwiML <Gather> is a collection window, while Vobiz reserves timeout for ring duration on <Dial>/<Number> and names the Gather window executionTimeout (with digitEndTimeout for inter‑digit pacing and speechEndTimeout for speech). A blind rename would either drop the timeout or apply it to the wrong concept. The action‑URL payload stays compatible (Digits / SpeechResult map to Vobiz names), so your menu handler needs minimal changes. See the full verb table in TwiML → VobizXML.
Before → after: live‑call control (calls(sid).update → split resources)
Twilio funnels every mid‑call action back through client.calls(sid).update(...) — redirecting to fresh TwiML, inlining <Play>/<Say>, or setting status. Vobiz gives each action its own resource keyed by (auth_id, call_uuid), so the intent is on the method name. The agent maps each update() variant to its resource and threads both keys through.
update(twiml="<Play>…") and update(twiml="<Say>…") aren’t two calls on one object, they map to two different resources (play_audio, speak_text), and update(status="completed") becomes live_calls.hangup_call. 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 leg. An imperative update(url=…) redirect becomes a declarative <Redirect> returned from your answer flow — the same webhook‑driven pattern your TwiML app already uses. Full side‑by‑side examples for every in‑call action live in Voice Call API.
A migration strategy that works
A Twilio-to-Vobiz move is mostly mechanical, but “mostly” is where projects stall. Thetwilio-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 Twilio touchpoint:
from twilio.rest import Client / require('twilio'), twilio.twiml (VoiceResponse), client.calls(...), available_phone_numbers / incoming_phone_numbers, RequestValidator / validateRequest, and any hardcoded api.twilio.com host. It produces a surface map (REST calls, TwiML, 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. Your Account SID becomes the Auth ID (api_key) and your Auth Token stays the secret — Vobiz sends them as X-Auth-ID / X-Auth-Token headers instead of Twilio’s HTTP Basic.
Check: one authenticated read (e.g. client.account.retrieve_account(auth_id=AUTH_ID)) returns 200. See the migration overview.SDK calls
The agent rewrites each REST call:
calls.create(to, from_, url, method) → calls.make_call(auth_id, from_=, to=, answer_url=, answer_method=), threading an explicit auth_id through every account-scoped method and renaming url → answer_url. Twilio’s live control via client.calls(sid).update(...) splits into dedicated resources — play_audio, speak_text, dtmf, record_calls, and live_calls — keyed by (auth_id, call_uuid); live-call lookups take status='live'.
Check: every migrated call site passes auth_id; no client.calls(sid).update(...) survivors remain. Reference: Voice Call API.TwiML / answer URL
The agent converts TwiML to VobizXML:
VoiceResponse() → vobizxml.ResponseElement(), <Say> → <Speak>, <Pause> → <Wait>, <Dial><Client>/<Sip> → <Dial><User>, and on <Gather> input → inputType, timeout → executionTimeout, speechTimeout → speechEndTimeout (numDigits/finishOnKey keep their names). Documents keep the same <Response> container and are served as application/xml.
Check: answer-URL responses byte-compare against the converted reference (see checklist). <Record> carries an action, and every <Gather> uses executionTimeout. Reference: TwiML → VobizXML.Webhooks
Replace Twilio’s
RequestValidator (HMAC-SHA1 over the full URL plus alphabetically-sorted POST params) with the Vobiz HMAC-SHA256 validator: V3 signs baseURL + "." + nonce, V2 signs baseURL + nonce, query stripped, nonce read from X-Vobiz-Signature-V3-Nonce. Rename the params you read (CallSid → CallUUID, SpeechResult → Speech), branch on the Event field (Ring / StartApp / Hangup) instead of a separate StatusCallback URL, 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 Twilio’s live carrier catalog (
available_phone_numbers('US').local.list() + incoming_phone_numbers.create(...)) 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 search/buy/configure call to its phone_numbers equivalent and wires the answer URL onto the number.
Check: the numbers you depend on exist in inventory and are assigned to the right app/answer URL before cut-over. Reference: Phone numbers.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 (calls.create → calls.make_call), an added auth_id, and live control split into dedicated resources. Deterministic and easy to diff. |
| TwiML / answer XML | Low | High | Near drop-in. Only <Say> → <Speak>, <Client>/<Sip> → <User>, and a few <Gather> attribute renames; the converter applies them verbatim. |
| Webhooks | Medium | Medium | Signature scheme (SHA1 → SHA256 + nonce) 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(auth_id=AUTH_ID)) 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) — never Twilio’stimeout, which on VobizXML belongs to<Dial>/<Number>(ring duration) — 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 theX-Vobiz-Signature-V3-Nonceheader 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 Twilio 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 Twilio numbers, TwiML apps, and voice URLs configured and idle (do not release them) until Vobiz has run clean through a full business cycle. Because the switch is “which platform owns the live number’s voice URL,” reverting is repointing that URL back to Twilio — 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 Twilio voice 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 Twilio migration overview.Install
Paste this in your project root. It creates.claude/skills/twilio-to-vobiz/ and writes the skill file — that single file is the installable agent. Then reload Claude Code and the skill appears as /twilio-to-vobiz.
Install the skill (one command)
Working with the agent
Thetwilio-to-vobiz agent is a Claude Code skill that reads your Twilio 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 route, just the webhooks) and target (Python vs Node, SDK vs raw TwiML), the cleaner the diff. Treat it like a fast pair-programmer who already knows the full Voice Call API and TwiML → VobizXML mappings 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
/voicehandler that returns TwiML.” - Say what to leave alone. “Don’t touch the billing module” keeps the diff reviewable.
- Ask for the reasoning inline. The agent annotates each rename (
calls.create→calls.make_call,url→answer_url, threadedauth_id) so you can verify rather than trust.
Example session 1 — single-file SDK migration
api_key, which is your Auth ID), make_call, auth_id passed in, and url/method → answer_url/answer_method. The full method-by-method table is in Voice Call API.
Example session 2 — converting a Flask /voice handler + its TwiML
<Say> becomes <Speak> and VoiceResponse() becomes vobizxml.ResponseElement() — and moves input/timeout to input_type/executionTimeout. The action payload still POSTs Digits, so your menu handler needs no changes (numDigits and finishOnKey keep their names). See TwiML → VobizXML for the full verb table.
Advanced prompts
Once you trust the single-file flow, scale it up:- Whole-repo swap — “Migrate every Twilio 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
twilio/twilio.twiml, converts each, and threadsauth_ideverywhere. - Batch-convert all TwiML templates — “Convert every
*.xmlundertemplates/from TwiML to VobizXML. Apply<Say>→<Speak>,<Pause>→<Wait>,<Dial><Client>→<Dial><User>, andinput/timeout→inputType/executionTimeout.” Good when your answer URLs serve static XML instead of building it in code. - Migrate just webhooks — “Only touch the signature-verification code. Replace Twilio’s
RequestValidatorwith the Vobiz HMAC-SHA256 validator and branch on theEventfield.” The agent rewrites the verifier to signbaseURL + "." + nonceand readX-Vobiz-Signature-V3; details in webhooks. - Convert live call control — “Find every
client.calls(sid).update(...)and split it into the Vobiz resources.” Turnsupdate(twiml="<Play>…"),update(twiml="<Say>…"), andupdate(status='completed')intoplay_audio.call,speak_text.call, andlive_calls.hangup_call.
Review the diff
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. Twilio’sclient.calls(sid).fetch()andclient.calls.list(status='in-progress')becomeclient.live_calls.get_live_call(auth_id, call_uuid, status="live")andlist_live_calls(auth_id, status="live"). Confirmstatus="live"is present; completed-call history moves tocdr.list_cdrs(auth_id, …).url/methodbecameanswer_url/answer_methodonmake_call. Twilio inlines TwiML viatwiml=; on Vobiz you host that XML at youranswer_urland passanswer_methodexplicitly. If the agent left an inlinetwiml=, it should have moved those instructions to your answer route.- The webhook signature is fully rewritten, not aliased. Verify the validator now signs
baseURL + "." + noncewith HMAC-SHA256 (not Twilio’s HMAC-SHA1 over the URL plus sorted POST params), readsX-Vobiz-Signature-V3and its-Nonce, and compares withhmac.compare_digest/crypto.timingSafeEqual— never==. A leftoverRequestValidatorcall is a red flag.
to_ → to on make_call, application/xml still set on answer responses, str(resp) → resp.to_string() on the builder, and CallSid → CallUUID / SpeechResult → Speech where you read webhook params.
Troubleshooting & FAQ
Q: The agent left aclient.calls(sid).update(url=…) call in my code. Is that a bug?
Vobiz moves a live call by returning <Redirect> from your answer flow rather than imperatively updating the leg. Ask the agent: “rewrite the update(url=…) redirect to return a <Redirect> from the answer URL.” It will emit a vobizxml response pointing at the new flow — same outcome, driven from your answer handler.
Q: I’m getting “method not found” on client.calls(sid).update(...) after migrating.
Live-call actions are dedicated resources in Vobiz, keyed by (auth_id, call_uuid). Map each update intent to its resource: update(twiml="<Play>…") → client.play_audio.call(auth_id, call_uuid, urls=…), update(twiml="<Say>…") → client.speak_text.call(...), sendDigits → client.dtmf.send_dtmf(...), recording → client.record_calls.start_recording(...), and update(status='completed') → client.live_calls.hangup_call(...). Re-run with “split the calls(sid).update actions into their Vobiz resources.”
Q: My webhook signature check fails after migration even though the request is real.
Two usual causes: the validator is still hashing the URL plus sorted POST params (Twilio’s scheme), or the query string wasn’t stripped before hashing. Confirm the agent signs only baseURL + "." + nonce with SHA-256, strips the query, reads the nonce from X-Vobiz-Signature-V3-Nonce case-insensitively, 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. See webhooks.
Q: My IVR stopped collecting digits after the Gather conversion.
Check the attribute moves the agent should have applied: Twilio’s input → input_type, timeout → execution_timeout (the 5–60s window), and speechTimeout → speech_end_timeout; num_digits and finish_on_key keep their names. 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 TwiML → VobizXML reference.”