Agent Phone: give an AI agent a phone line
Provision a real business phone line end to end through the API: buy a number, register 10DLC, configure and publish the AI agent, route calls to it, and place calls.
Agent Phone is the name for the part of the API that lets software (your backend, or an AI agent over MCP) set up a complete business phone line on its own: a real number, texting registration, an AI receptionist answering calls, and outbound AI calling.
The rule that makes it safe: everything an agent provisions appears in the customer's Ody account exactly as if they'd done it in the app. It's billed on the same subscription, held to the same gates, and shown on the same timeline. A human can take over at any time.
1. key ─► 2. buy number ─► 3. register 10DLC ─► 4. configure agent ─► 5. route number ─► 6. call & text
numbers:manage numbers:manage agent:manage agent:manage calls:place / messages:sendRun the whole guide with an ody_test_… key first. The purchase, 10DLC filing (mock mode), publishing and calls are all simulated, and the responses have the same shape as live ones.
#1. Get a key
An owner or admin creates it in Settings → Developer API with these scopes: numbers:read, numbers:manage, agent:manage, calls:place, messages:send, conversations:read. The workspace must already have a plan, and have completed identity verification where the app requires it. The API can't skip those steps, by design.
#2. Buy a number
Search local inventory by area code, then buy one:
curl "https://api.ody.co/v1/api/numbers/available?startsWith=512&limit=5" \
-H "Authorization: Bearer $ODY_API_KEY"
curl https://api.ody.co/v1/api/numbers \
-H "Authorization: Bearer $ODY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: acme-main-line" \
-d '{"phoneNumber":"+15125550188"}'const [pick] = await ody.numbers.searchAvailable({ startsWith: "512", limit: 5 });
const { number } = await ody.numbers.buy({ phoneNumber: pick.phone_number, idempotencyKey: "acme-main-line" });pick = client.numbers.search_available(starts_with="512", limit=5)[0]
res = client.numbers.buy(phone_number=pick["phone_number"], idempotency_key="acme-main-line")The first local number is included in the plan; extra local numbers are $5/mo. Toll-free numbers (type=toll_free) are $10/mo and calling-only for now, so skip step 3 for them.
#3. Register for texting (10DLC)
US carriers require this before a local number can text. It's a purchase: on a free trial it ends the trial and charges the plan plus $20 for the number right away. Tell the customer first, then pass acknowledge_trial_end.
curl https://api.ody.co/v1/api/messaging/registration \
-H "Authorization: Bearer $ODY_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"business": {
"displayName": "Acme Plumbing", "companyName": "Acme Plumbing LLC", "ein": "12-3456789",
"email": "[email protected]", "vertical": "PROFESSIONAL",
"street": "100 Congress Ave", "city": "Austin", "state": "TX", "postalCode": "78701"
},
"campaign": {
"useCase": "CUSTOMER_CARE",
"optIn": { "method": "web_form", "url": "https://acmeplumbing.com/contact",
"location": "the quote form at the bottom of the contact page", "formConfirmed": true }
},
"acknowledge_trial_end": true
}'Carrier review takes minutes to days. Poll until canText is true:
let status = await ody.messaging.refresh();
while (!status.canText && !status.step.endsWith("_failed")) {
await new Promise((r) => setTimeout(r, 15 * 60_000)); // every 15 min
status = await ody.messaging.refresh();
}Sole proprietors also get a PIN by text: submit it with POST /messaging/otp/verify. All the fields are in the 10DLC reference.
#4. Configure and publish the agent
await ody.agent.update({
name: "Astra",
greeting: "Thanks for calling Acme Plumbing, this is Astra. How can I help?",
voice: "luna",
timezone: "America/Chicago",
inHoursAction: "agent",
offHoursAction: "agent",
jobs: [
{ name: "Book appointments", instructions: "Collect name, address and the problem; offer the next two slots." },
{ name: "Emergencies", instructions: "For burst pipes or flooding, take the address and promise a callback within 15 minutes." },
],
});
await ody.agent.publish();#5. Route the number to the agent
curl -X POST https://api.ody.co/v1/api/numbers/%2B15125550188/connect \
-H "Authorization: Bearer $ODY_API_KEY"Inbound calls to that number are now answered by the agent. Recordings, transcripts and summaries land in the inbox, and you get them as call.completed and call.summary.completed webhooks.
#6. Call and text
await ody.calls.place({ to: "+15125550123" }); // the agent dials, talks, and logs the callawait ody.messages.send({ to: "+15125550123", body: "Hi! This is Acme Plumbing, confirming tomorrow's 2pm visit." });#Doing it from an AI agent (MCP)
Every step above has an MCP tool: search_numbers, buy_number, submit_10dlc_registration, get_messaging_status, configure_agent (with publish: true), connect_agent_to_number, place_call, send_message. Connect a client (setup) and ask:
"Set up a phone line for Acme Plumbing in Austin: get a 512 number, have the AI answer calls, book appointments and flag emergencies."
Keep approvals on for buy_number, submit_10dlc_registration and place_call. See Safety.
#Handing over to a human
Nothing here is hidden from the customer. They see the number in Settings → Numbers, the agent under Astra, and every call and text in the inbox. They can re-route the number, edit the agent or take over a conversation at any time, and your integration sees those changes on its next read.