REST API reference
Base URL: https://api.callme.olorin.ai. This reference is generated from the API's own route table — every endpoint below exists in the service today, and the documentation build fails if the two drift apart.
Conventions
- Auth.
Authorization: Bearer cm_live_… (or cm_test_… for the sandbox tenant). Each key carries scopes; a route names the scope it needs, and a key without it gets a 403 insufficient_scope. - Errors. One shape everywhere:
{"error": {"code": "…", "message": "…"}} — 401 unauthenticated, 400 bad request, 404 not found, 409 for typed refusals (a refused call carries the refusal's own code and text). - Isolation. Another account's object is always a 404, never a 403 — the API does not confirm that other tenants' things exist.
- Phone numbers. Your own purchased numbers are shown in full: in
GET /v1/numbers, as the account's caller_number, and in the events delivered to your own signed webhook (caller ID included). Third-party and destination numbers are masked to the last four digits in the views the API serves: your verified destination (destination_last4) and the other party on a call or text, in call views, message views and the verification inbox. Message views and inbound call views carry your own number in full and mask only the other party (number_id says which of your numbers it was): on an inbound call the caller is masked and the line that rang is whole, and a call you placed carries no numbers. Message views are the list, reading one message (GET /v1/messages/{message_id}), the answer to sending one and a conversation's messages; a conversation (GET /v1/threads) names its remote party by its last four digits. The MCP tools mask the other party the same way and show your own number whole; an agent's transfer numbers are dial destinations and are masked the same way (see Agent transfer numbers below). - Agent transfer numbers. A hosted agent's
transfer tool may hand a call only to the numbers in the agent's transfer_numbers (at most 10 entries). These transfer numbers are dial destinations, so no answer ever shows one whole: every agent view (create, read, list, update, delete and a replayed create) serves each as an entry {"id": "tn_…", "number": "…1234"}, the number masked to its last four digits and the id stable across reads and edits. A write names the roster whole, entry by entry: an entry with an id keeps the stored entry (send the id alone, or the entry exactly as the agent showed it, so a client can read an agent, change it and write it back without ever holding a full number), a full number is a new entry (a valid number in a country enabled on the deployment, never repeated), and an entry you leave out is removed; [] clears the roster. An unknown or repeated id, an id on create, or an id sent with a number other than the masked one it was served with is a 400 that changes nothing. Over REST, changing the roster needs the admin scope on a live key: adding, removing or reordering an entry is a change, while sending every entry back by id in the order it was served needs only agents. A key without admin that sends a full number gets 403 insufficient_scope, whether or not the number is already on the agent, so an answer never says whether a guess was right. A test key cannot set transfer numbers at all, because a sandbox agent has no live call to transfer: a roster with entries is 409 sandbox_unsupported, and only clearing it is accepted. The MCP create_agent and update_agent tools take the same entries. - The sandbox. A
cm_test_… key acts on a sandbox tenant with fake numbers, fake sends and fake money, separate from your account, and it touches no carrier. Sandbox calls, messages, recordings, delivery logs and the sandbox wallet balance are ephemeral and per instance; sandbox configuration (numbers, webhook, agents, registrations) persists. Do not build on a sandbox call or message surviving a restart or being visible from a second instance: recreate them in each run. - Playing the caller in the sandbox. A sandbox number has no carrier behind it, so nothing outside can ring or text it. A test key plays the caller and the provider through the Sandbox driver routes in the table below, entirely in process: the
calls scope rings a sandbox number (POST /v1/sandbox/inbound), leaves a voicemail, speaks into a live conversation and hangs up; the messages scope delivers an inbound text (POST /v1/sandbox/inbound-sms) and reports a delivery state for one of your sandbox sends. These routes are for test keys only: a live key gets 409 sandbox_only, because a live key places real calls. The text routes exist where messaging is enabled. - Idempotency. A call's
request_key is its identity: repeating a create returns the original call (200) instead of dialling again (201). Every other paid or destructive operation (buying or releasing a number, a wallet top-up, sending a text, creating or deleting an agent, submitting a campaign, creating or closing a child account) takes an optional Idempotency-Key header: the same key replays the first answer (with an Idempotent-Replay: true header) and never runs the operation twice; the same key with a different request is a 409 idempotency_conflict. The MCP tools take the same key as request_key, in the same key space. - Live calls.
POST /v1/calls on a live key rings the owner, opens by saying who is calling, asks the question and waits for the reply, exactly as the MCP start_call tool does; the reply is the response's turn. Keep the conversation going with POST /v1/calls/{call_key}/turns, /listen and /end. - Test keys and scopes. A test key never carries the admin scope (account configuration needs a live key). The routes that configure the sandbox take the resource's own scope from a test key instead:
numbers for a sandbox number's inbound routing, usage for a sandbox wallet top-up, messages for sandbox 10DLC registration, calls for deleting a sandbox call's recording and for the sandbox webhook (its own URL, secret, delivery log and health, separate from the live webhook). A live key needs admin for all of them, and the Scope column below shows both. Everything else that needs admin is live-key only: recording settings, wallet settings, the call window and reseller children. A test key that reaches one gets 409 sandbox_unsupported.
Endpoints
Account & usage
Account & usage| Method | Path | Scope | Purpose |
|---|
GET | /v1/account | usage | The account: plan, status, and its provisioned caller number (destination shown as last four only). A test key reads the sandbox view: the email, plan and overage fields are null. |
GET | /v1/usage | usage | Calls and minutes used today and this period, against the plan's caps. On a wallet add-on line seconds_left is the plan's seconds left and is never negative: it stops at 0 while seconds keeps counting, and period.wallet_pays_past_plan is true (past the plan's minutes the wallet pays; a call or text is refused balance_exhausted only when the wallet cannot). On a plan line seconds_left is the plan's seconds left. A customer without a plan reads this too. |
Calls
Calls| Method | Path | Scope | Purpose |
|---|
GET | /v1/calls | calls | List the account's calls, newest first. On an inbound call your own number (the line that rang) is whole and the caller is masked to its last four digits; number_id says which of your numbers it was. A call you placed carries no numbers. A customer without a plan reads this too (no 409 no_plan): it is their own history. |
POST | /v1/calls | calls | Place a call. The audit record is written before the provider is asked; the same request key replays the same call, never a second dial (201 new, 200 replay). The optional mode field is "connected" (the default: you drive the call turn by turn) or "webhook": your account's own turn webhook drives the call — the placement returns as soon as the call is dialled (turn is null) and the platform speaks the opening when the phone connects. A replay must repeat the placement's mode; the other mode is 409 mode_conflict. A webhook placement with no configured, unpaused webhook is 409 webhook_not_configured. If the call cannot be recorded with its mode it is closed before any dial: 503 record_failed. A live call opens by saying who is calling, then asks the question, and the response waits for the owner's reply (the turn field), exactly as the MCP start_call tool does. |
GET | /v1/calls/{call_key} | calls | One call: state, outcome, duration, transcript and recording when they exist. On an inbound call your own number is whole and the caller is masked to its last four digits (number_id says which of your numbers rang); a call you placed carries no numbers. Another account's call is a 404, never a 403. A customer without a plan reads this too (no 409 no_plan): it is their own history. |
POST | /v1/calls/{call_key}/turns | calls | Speak the next question in a live call and wait for the owner's reply (the MCP continue_call tool). Replays of the same request key return the same answer. On a call your account's agent drives (a webhook placement, or an inbound call a webhook or hosted agent answered) this is 409 agent_driven — the agent is the one listening. |
POST | /v1/calls/{call_key}/listen | calls | Wait for the owner's next reply without speaking (the MCP listen tool). Use a new request key for each wait. On a call your account's agent drives this is 409 agent_driven — listening would take the replies the agent waits for. |
POST | /v1/calls/{call_key}/end | calls | Finish your side of the call (the MCP end_call tool). A plain end never hangs up a connected call; hang_up true is only for a call the owner dismissed in speech, and the response says whether the provider confirmed the end. A plain end on a call your account's agent drives is 409 agent_driven; hang_up true ends such a call. |
Recording
Recording| Method | Path | Scope | Purpose |
|---|
GET | /v1/calls/{call_key}/recording | calls | The call's recording, via a short-lived signed URL (at most 15 minutes) minted at read time. A customer without a plan still reads their own recordings; a held, suspended or closed account is refused (409 call_refused) and no URL is minted. |
DELETE | /v1/calls/{call_key}/recording | admin live key
calls test key | Delete the call's recording and transcript now, ahead of the retention schedule. The answer carries provider_copy: removed, none (there was no provider copy) or not_removed (the provider's own copy could not be removed and is still owed to you; the deletion of our copy stands). A customer whose plan has lapsed may still delete their own recording. |
GET | /v1/recording | admin live key only | The account's recording mode (off / on / announced) and retention in days (default 30, 7–30). A period stored above the maximum reads as the maximum. A closed account is refused 409 call_refused (the message says the account is closed); a hold or suspension still reads it. |
PUT | /v1/recording | admin live key only | Set the recording mode and retention (7–30 days). A period outside that range is a 400 that names it; nothing is clamped silently. |
Numbers
Numbers| Method | Path | Scope | Purpose |
|---|
GET | /v1/numbers | numbers | List the account's numbers, shown in full (they are your own). The primary caller number counts toward the plan cap (5 per account by default). Every number carries monthly_price_cents: 0 for a number within the plan's included numbers (Starter 1, Plus 3, Pro 5; the primary is the first), the deployment's extra-number price for each one beyond them (up to the plan's cap). It is derived from the number's place in the live list (primary first, then in the order bought), never stored, so a release or a plan change moves it at once. |
POST | /v1/numbers | numbers | Buy a number (US, CA or IL; Israel needs an approved regulatory bundle first; an area code is tried first, and when it has none the number comes from elsewhere in the country). Every number carries monthly_price_cents: 0 for a number within the plan's included numbers (Starter 1, Plus 3, Pro 5; the primary is the first), the deployment's extra-number price for each one beyond them (up to the plan's cap). It is derived from the number's place in the live list (primary first, then in the order bought), never stored, so a release or a plan change moves it at once. |
DELETE | /v1/numbers/{number_id} | numbers | Release a number. The primary number cannot be released. |
PUT | /v1/numbers/{number_id} | admin live key
numbers test key | Set the number's inbound routing: hosted, webhook, voicemail or reject, plus its agent and voicemail greeting. |
Agents
Agents| Method | Path | Scope | Purpose |
|---|
GET | /v1/agents | agents | List the account's agents. Transfer numbers are shown masked to their last four digits. |
POST | /v1/agents | agents | Create an agent: name, system prompt, greeting, voice, language, tools, transfer_numbers and limits. Transfer numbers need the admin scope on a live key and are live-only: a test key that sends entries gets 409 sandbox_unsupported. Every answer shows each one masked to its last four digits under an entry id. A transfer entry may carry a label, the customer's own name for the destination (at most 40 characters, at most 2 digits, never a phone number); an agent view shows it beside the id and the last four digits. The hosted agent's model is told each entry's id, label and last four and names an entry by id: a number never reaches it, and a refused or unknown destination is never echoed back. Hosted-mode agents are limited by the account's plan (GET /v1/account shows the cap and the count as limits.hosted_agents): creating one past the cap is refused 409 plan_agent_cap_reached. |
GET | /v1/agents/{agent_id} | agents | Read one agent (transfer numbers masked to their last four digits, under stable entry ids). |
PUT | /v1/agents/{agent_id} | agents | Update an agent. transfer_numbers names the whole roster: an entry's id keeps it, a full number adds one (admin scope, live key) and an entry left out is removed. An entry with an id may carry a new label (a null clears it); a label is at most 40 characters and at most 2 digits. Switching an agent to hosted mode meets the plan's hosted-agent cap, exactly like creating one: past it the update is refused 409 plan_agent_cap_reached. |
DELETE | /v1/agents/{agent_id} | agents | Delete an agent. An agent a number still routes to cannot be deleted. |
Messaging
Messaging| Method | Path | Scope | Purpose |
|---|
GET | /v1/messages | messages | List messages. Every message view masks the other party to its last four digits; your own number is whole. |
POST | /v1/messages | messages | Send a text from the account's own number. The record is written before the provider call; opted-out destinations and unregistered US sending refuse with a typed reason. The answer is the stored message with the recipient masked to its last four digits and your own sending number whole. Without an active plan it is a 409 no_plan. The plan includes a number of texts per day; past them a wallet add-on pays per segment at the account's own rate, and a wallet that cannot pay for the next segment is a 409 balance_exhausted (a plan-only account past its daily texts is a 409 message_cap_reached). The daily cap counts the texts the carrier accepted: a text charged past the cap stays charged if an earlier included text later fails delivery (judged at send). |
GET | /v1/messages/{message_id} | messages | Read one message, with its delivery status. The other party is masked to its last four digits and your own number is whole, as in the list. |
GET | /v1/threads | messages | List text conversations (one of your numbers and one remote party), newest activity first. Paginated with limit (1-100, default 50) and an opaque cursor; the remote party is shown as its last four digits. |
GET | /v1/threads/{thread_id}/messages | messages | List one conversation's messages, newest first, with the same limit and cursor. Another account's thread id is a 404. |
GET | /v1/verification-codes | messages | The verification inbox: the latest sign-in codes texted to the account's numbers in the last 10 minutes, newest first. |
Webhooks
Webhooks| Method | Path | Scope | Purpose |
|---|
GET | /v1/webhooks | admin live key
calls test key | Read the account's webhook configuration (URL and transport). |
PUT | /v1/webhooks | admin live key
calls test key | Set the webhook URL and transport (HTTP or websocket), or pause delivery. A live key needs the admin scope; a test key with the calls scope sets its own sandbox webhook (its own URL, secret and delivery log), never the live one. |
POST | /v1/webhooks/test | admin live key
calls test key | Send a webhook.test event through the real delivery engine. |
POST | /v1/webhooks/secret | admin live key
calls test key | Rotate the signing secret. The new secret is shown exactly once. |
GET | /v1/webhooks/deliveries | admin live key
calls test key | The delivery log: every event, its attempts and its last status. |
Wallet
Wallet| Method | Path | Scope | Purpose |
|---|
GET | /v1/wallet | usage | Wallet state: balance, currency, auto-recharge settings (wallet-priced accounts). The wallet is an add-on to a paid plan, but reading it needs no plan: a customer without a plan still reads the balance their ledger kept. |
PUT | /v1/wallet | admin live key only | Set auto-recharge (threshold and amount; off by default). The wallet is an add-on to a paid plan (it pays only past the plan's minutes and daily texts): choosing it (pricing_model wallet) without an active plan is a 409 plan_required, and nothing is saved; a record that already chose it may still change its other settings. |
GET | /v1/wallet/ledger | usage | Every wallet movement: top-ups, per-call and per-segment charges, refunds — each tied to the call or message it belongs to. Reading the ledger needs no plan: a customer without a plan still reads every movement. |
POST | /v1/wallet/topups | admin live key
usage test key | Start a top-up. With a live key the balance is credited only when the signed payment webhook confirms it; a test key credits fake money at once. The wallet is an add-on to a paid plan: a live top-up without an active plan is a 409 plan_required and opens no checkout (a test key's fake-money top-up needs no plan), and a top-up (or a PUT /v1/wallet) on a closed account is refused 409 account_closed, the message saying the account is closed. |
Messaging registration (10DLC)
Messaging registration (10DLC)| Method | Path | Scope | Purpose |
|---|
GET | /v1/registration/brand | admin live key
messages test key | The account's 10DLC brand registration and its carrier status. |
PUT | /v1/registration/brand | admin live key
messages test key | Submit the brand (legal name, EIN, address). The EIN is stored sealed and never shown again. |
GET | /v1/registration/campaigns | admin live key
messages test key | List messaging campaigns and their statuses. |
POST | /v1/registration/campaigns | admin live key
messages test key | Register a campaign: use case, opt-in flow and sample messages. |
GET | /v1/registration/campaigns/{campaign_id} | admin live key
messages test key | Read one campaign, including a rejection's carrier reason. |
PUT | /v1/registration/campaigns/{campaign_id} | admin live key
messages test key | Correct and resubmit a rejected campaign. |
Reseller sub-accounts
Reseller sub-accounts| Method | Path | Scope | Purpose |
|---|
GET | /v1/children | admin live key only | List child accounts (reseller accounts): their state and rolled-up usage. Every child row carries effective_rates ({voice_cents_per_minute, sms_cents_per_segment}): what the child is actually charged, a stored rate override under the deployment's card being charged the card. period_start and period_end name the child's current period. |
POST | /v1/children | admin live key only | Invite a child account. The answer carries the invite link (invite_url, shown once): send it to the owner, who opens it and signs in with the invited Google account (which must not already have an account here) to claim the child. effective_rates is on the answer. The wallet is an add-on that needs a plan: pricing_model wallet without a plan is a 400 plan_required. A reseller whose approval was revoked gets 409 reseller_not_approved. |
GET | /v1/children/{child_id} | admin live key only | Read one child account. effective_rates says what the child is charged (read it, not rate_overrides, which is what was set). |
PUT | /v1/children/{child_id} | admin live key only | Update a child: plan assignment and per-child rate overrides. effective_rates is on the answer. Moving a child to the wallet without a plan, or taking its plan away from a wallet child, is a 409 plan_required. |
DELETE | /v1/children/{child_id} | admin live key only | Close a child account. Closing is permanent. The answer is the child plus numbers_released (the numbers released at the carrier by this closure; they cannot be recovered), numbers_still_held (non-zero only where this deployment cannot release the child's numbers, and then closing again changes nothing; a release that fails is a 502 provider_error and closing again retries it) and notice (the plain-words statement; null while a number is still held). |
POST | /v1/children/{child_id}/activate | admin live key only | Activate a claimed child (verified phone + an assigned plan). A wallet child with no plan is a 409 child_no_plan. |
POST | /v1/children/{child_id}/suspend | admin live key only | Suspend a child. Holds propagate down the chain, never up. |
POST | /v1/children/{child_id}/resume | admin live key only | Resume a suspended child. |
POST | /v1/children/{child_id}/invite | admin live key only | Reissue the invite of a child account that has not been claimed yet (parent only). The answer carries a new single-use invite link, shown once; the previous link stops working. Refused: 409 child_already_claimed (nothing left to reissue), 409 child_closed, 409 child_invite_exists (that address has a live invite for another child) and 409 reseller_not_approved (a reseller whose approval was revoked hands out no more invites). Both SDKs have reissue_invite / reissueInvite. |
Call window
Call window| Method | Path | Scope | Purpose |
|---|
GET | /v1/call-window | admin live key only | The account's own call window (IANA time zone, weekdays, start and end — nulls when unset) and the effective window its calls are judged by: its own window, else the deployment's days and hours in the zone its verified phone's country names, narrowed by the operator's fleet window. A closed account is refused 409 call_refused (the message says the account is closed); a hold or suspension still reads it. |
PUT | /v1/call-window | admin live key only | Set the account's call window (tz, days, start and end together) or clear it back to the fallback. |
Sandbox driver
Sandbox driver| Method | Path | Scope | Purpose |
|---|
POST | /v1/sandbox/inbound | calls | Ring one of your sandbox numbers as the provider would (number_id, the E.164 caller in from, optional call_sid). The number's routing answers it; the 201 carries the call_key (null when the edge refused the call) and the TwiML it would have played. |
POST | /v1/sandbox/calls/{call_key}/voicemail | calls | Leave the voicemail on a sandbox call that is waiting for one (optional transcript and duration_seconds); the call closes as the provider's callbacks would close it. A call that is not waiting for one is a 409 call_not_live. |
POST | /v1/sandbox/calls/{call_key}/utterances | calls | Say something as the caller into a live webhook or hosted sandbox conversation (text). The answer carries what the platform said back (spoken), whether it ended the conversation (ended) and the call. |
POST | /v1/sandbox/calls/{call_key}/hangup | calls | Hang up a sandbox call as the caller: the call closes and is metered as the provider's final status callback would close it. Hanging up a finished call changes nothing. |
POST | /v1/sandbox/inbound-sms | messages | Text one of your sandbox numbers as the provider would (number_id, the E.164 sender in from, body and optional media_urls and message_sid). It takes the path a signed provider text takes. The 200 carries message_id and message, the stored record with the sender shown as its last four digits, or null when only a refusal row was left (the per-number hourly cap). |
POST | /v1/sandbox/messages/{message_id}/status | messages | Report a delivery state for one of your own sandbox sends, as the provider's status callback would: status is queued, sending, sent, delivered, failed or undelivered, with an optional numeric error_code. The answer is the record as it now stands, and a final state never goes back. An inbound text, another account's message and an unknown id are all a 404. |
Browser routes
One link in this API is opened in a browser, not called with a key. It is a browser route, not a REST endpoint: it takes no API key and answers with a redirect or a page.
Browser routes| Method | Path | Purpose |
|---|
GET | /account/invite/{token} | A child account's invite link. POST /v1/children and the MCP create_child tool return it once as invite_url; send it to the child's owner, who opens it in a browser. It redirects to Google's sign-in; the link's single-use token is kept on the service, not in the redirect. The child is claimed only when the owner signs in with the invited Google account (the verified email you named, which must not already have an account here) before the link expires, seven days after it was created by default. Opening the link only starts a sign-in and never uses the invite up: a different Google account claims nothing (it opens an ordinary account of its own) and leaves the invite waiting for its owner, and an expired or already-used link claims nothing. A malformed link is refused with a sign-in error page (403). Each link is single use. |
The other door
Every resource here is also an MCP tool — one implementation, two doors — and the Python and TypeScript SDKs wrap this API one-to-one.