Numbers
List your phone numbers, search carrier inventory, buy local or toll-free numbers, and route a number to or away from the AI agent.
A number is a real phone line owned by the workspace. Numbers you buy through the API are indistinguishable from numbers bought in the app: they show up in Settings → Numbers, they're billed on the same subscription, and a teammate can re-route them at any time.
#The number object
e164string — The number, for example+15125550100.type"local" | "toll_free"voiceModestring — Who answers inbound calls:agent(Astra),app(rings your team),voicemailorforward.agentIduuid | null — The AI agent, whenvoiceModeisagent.forwardTostring | null — The forwarding target, whenvoiceModeisforward.assignedUserIdstring | null — The teammate the line belongs to, if it's a personal line.statusstring —activeonce the line is live.
#Pricing
| Monthly | Notes | |
|---|---|---|
| First local number | Included in the plan | |
| Each extra local number | $5/mo add-on | Prorated on the subscription |
| Toll-free (+1 8xx) | $10/mo add-on | Calling only. Texting from toll-free isn't available yet |
| Texting on a local number | One-time $20 setup per number + $4/mo messaging add-on | Charged when you file 10DLC |
Ody works out the billed type from the number itself, so a +1 8xx number is always billed as toll-free whatever type you send.
#List numbers
/v1/api/numbersReturns every number in the workspace.
Scope: numbers:read
Returns { numbers: Number[] }.
curl https://api.ody.co/v1/api/numbers \
-H "Authorization: Bearer $ODY_API_KEY"const numbers = await ody.numbers.list();numbers = client.numbers.list(){
"numbers": [
{
"e164": "+15125550100",
"type": "local",
"voiceMode": "agent",
"agentId": "0d9c…",
"forwardTo": null,
"assignedUserId": null,
"status": "active"
}
]
}#Search available numbers
/v1/api/numbers/availableSearches live carrier inventory. Each result is quoted at Ody's retail monthly price: $5 for local (the first local number is included in the plan), $10 for toll-free.
Scope: numbers:manage
Query parameters
countrystring — ISO 3166 alpha-2. DefaultUS.type"local" | "toll_free" — Defaultlocal.startsWithstring — Leading digits, usually the area code (for example512). Local numbers only.containsstring — Digits anywhere in the number.localitystring — City.statestring — State or region (for exampleTX).limitinteger — 1–50, default 20.
Returns { available: [{ phone_number, phone_number_type, region_information, cost_information }] }. These fields are snake_case because they come straight from the carrier.
curl "https://api.ody.co/v1/api/numbers/available?startsWith=512&limit=5" \
-H "Authorization: Bearer $ODY_API_KEY"const available = await ody.numbers.searchAvailable({ startsWith: "512", limit: 5 });available = client.numbers.search_available(starts_with="512", limit=5){
"available": [
{
"phone_number": "+15125550188",
"phone_number_type": "local",
"region_information": [{ "region_type": "rate_center", "region_name": "AUSTIN" }],
"cost_information": { "monthly_cost": "5.00", "upfront_cost": "0.00", "currency": "USD" }
}
]
}#Buy a number
/v1/api/numbersOrders the number and bills the workspace exactly like a purchase in the app. The number is live as soon as the call returns. Inbound calls ring the team (voiceMode: "app") until you route it to the agent.
The same hard gates as the app apply. The workspace must have chosen a plan and completed identity and mobile verification where required. Workspaces billed through the Apple App Store can't add paid add-ons (a second number or a toll-free line). An agent can never buy past these gates.
With a test key nothing is ordered or charged; the response has simulated: true.
Scope: numbers:manage · supports Idempotency-Key
Body
phoneNumberstring required — An E.164 number from Search available numbers.type"local" | "toll_free" — A hint only. Ody works out the real type from the number.
Returns 201 with { number, simulated? }.
Errors (409): "Choose a plan before adding phone numbers", "Verify your identity before adding phone numbers", "Verify your mobile number before adding phone numbers", the App Store add-on block, or the carrier's message when the number was taken between search and order.
curl https://api.ody.co/v1/api/numbers \
-H "Authorization: Bearer $ODY_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: onboard-acme-main-line" \
-d '{"phoneNumber":"+15125550188"}'const { number } = await ody.numbers.buy({
phoneNumber: "+15125550188",
idempotencyKey: "onboard-acme-main-line",
});res = client.numbers.buy(phone_number="+15125550188", idempotency_key="onboard-acme-main-line")#Route a number to the agent
/v1/api/numbers/:e164/connectSends the number's inbound calls to the AI agent. The agent must already exist: create it with PATCH /agent first. Encode + as %2B in the path.
Scope: agent:manage
Returns { ok: true, voiceMode: "agent" }. Returns 404 if the number isn't in the workspace, or 409 "Create an agent first".
curl -X POST https://api.ody.co/v1/api/numbers/%2B15125550188/connect \
-H "Authorization: Bearer $ODY_API_KEY"await ody.numbers.connectAgent("+15125550188");client.numbers.connect_agent("+15125550188")#Route a number away from the agent
/v1/api/numbers/:e164/disconnectTakes the agent off the line and chooses who answers instead.
Scope: agent:manage
Body (optional)
mode"app" | "voicemail" | "forward" — Defaultapp(ring the team).forwardTostring — E.164. Required whenmodeisforward.
Returns { ok: true, voiceMode }.
curl -X POST https://api.ody.co/v1/api/numbers/%2B15125550188/disconnect \
-H "Authorization: Bearer $ODY_API_KEY" \
-H "Content-Type: application/json" \
-d '{"mode":"forward","forwardTo":"+15125559999"}'await ody.numbers.disconnectAgent("+15125550188", { mode: "forward", forwardTo: "+15125559999" });client.numbers.disconnect_agent("+15125550188", mode="forward", forward_to="+15125559999")