api.allternit.com and take a Clerk session or a compute-scoped API key, like the channel address routes. When the carrier is not configured they answer 503 {"error":"phone_not_configured"}.
Turn it on (operators)
Migration
024_phone_numbers.sql is applied by hand to the production database, like 020 and 021.
Numbers
GET /api/v1/phone/numbers/search?country=US&areaCode=415&locality=&type=local|toll_free&limit=10returns{carrier, numbers:[{e164, type, locality, monthlyCost}]}.POST /api/v1/phone/numberswith{e164, runtimeId, botId}buys the number, assigns it to the bot, and creates its relay address (providersms). Returns201 {number}. Phone numbers need a paid plan: Free gets402 phone_requires_plan. Each plan holds a limited number of numbers (PLAN_NUMBER_LIMITSinroutes/phone.rs: Plus 3, Super 5, Ultra 10), past which the route returns403 number_limit. Porting a number follows the same rules. Errors:402 phone_requires_plan,409 number_taken,403 number_limit,404 runtime_not_found.POST /api/v1/phone/numbers/portwith{e164, runtimeId, botId}starts a port-in (Telnyx). The number starts texting through Allternit when the port completes.GET /api/v1/phone/numbers/:id/portreturns{port:{orderId,state}, number}.GET /api/v1/phone/numberslists the caller’s numbers.DELETE /api/v1/phone/numbers/:idreleases the number and revokes its relay address.PATCH /api/v1/phone/numbers/:idwith{botId, runtimeId?, botName?}moves the number to another of your bots (and, withruntimeId, another of your computers). Returns200 {number, previousBotId}; the number’s runtime is told to resync at once. Invites still open on the number follow it; consent, opt-outs and registration stay with the number; past conversations stay with the old bot. Errors:400 bad_request(emptybotId),404 number_not_found,404 runtime_not_found,403 runtime_cannot_move_number. See Moving a number.
smsState is pending_registration, active, rejected or blocked. A new US number is pending_registration until the carrier approves its registration.
Registration
US carriers require registration before a number can text at volume.POST /api/v1/phone/numbers/:id/registrationtakes the business form (legal name, EIN, website, address, contact, use case, sample messages, optionaloptInPageUrl). Local numbers register for 10DLC, toll-free numbers for toll-free verification;kindoverrides. The stored copy masks the EIN.GET /api/v1/phone/numbers/:id/registrationreturns the state (pending,approved,rejected, withrejectionReason) and the number’ssmsState. It re-reads a pending registration from the carrier. A rejected registration can be submitted again.- The carrier message flow is generated, not taken from the form: one opt-in path, the person texts the number first after seeing it on the opt-in page, plus the STOP/START/HELP keywords and the terms and privacy links. Carriers reject flows that list several paths (signed forms, verbal consent) without proof for each.
optInPageUrlis the business’s own page; when it is empty the registration uses the hosted pageGET /sms/:numberId(public HTML: the number, the use case, the texting terms;404for a number with no registration or a released one). A free-textoptInWorkflowis still accepted and stored but never sent to the carrier. - 10DLC is two steps at the carrier: the business brand is verified first (usually hours to a few days), then the texting campaign is reviewed. The submit files the brand right away; if the brand is still being verified, the campaign is filed automatically as soon as the brand clears (on the carrier’s status webhook or the next
GET).campaignIdstaysnulluntil then. A campaign the carrier refuses outright turns the registrationrejectedwith arejectionReasonstartingcampaign:. Submitting the form again for a rejected 10DLC registration whose brand cleared corrects the existing campaign in place (PUT /10dlc/campaign/{id}at Telnyx, which puts it back into review) and keeps the same registration: no new brand or campaign is filed. A registration rejected at the brand step files a new one. - The campaign sent to the carrier includes the HELP, STOP and START replies (naming the business display name and contact email), and declares that messages may include links and phone numbers.
- Waiting, not rejected. When the carrier refuses for a reason on Allternit’s side (our carrier account’s balance or level, or a carrier outage), the registration stays
pendingwith arejectionReasonstartingwaiting:, and is retried automatically every 30 minutes. Only a refusal about the business’s own details turns itrejected. - Individuals (sole proprietors, no EIN). Send
entityType: "SOLE_PROPRIETOR",contactFirstName,contactLastNameandmobilePhone(instead of an EIN) anduseCase: "SOLE_PROPRIETOR". The carrier texts a code to that mobile;POST /api/v1/phone/numbers/:id/registration/otpwith{"pin":"123456"}checks it (400 when it doesn’t match) and files the campaign; an empty body sends a new code. Carriers allow one number per individual this way, with lower sending limits.
Texts in
The carrier posts to the number’s relay address. Before anything is queued the cloud verifies the carrier signature, drops duplicates, and handles keywords:STOP,STOPALL,UNSUBSCRIBE,CANCEL,END,QUIT: the sender is added to the number’s opt-out list, gets a confirmation, and nothing reaches the runtime.HELP: gets a help reply.START,UNSTOP: removes the opt-out and confirms.
/webhooks/channels/sms:
Texts out
POST /api/v1/channels/sms/send with {numberId, to, text} returns {ok:true, messageId, parts}. It refuses with:
POST /api/v1/phone/numbers/:id/consent with {e164, source, evidence} records explicit consent you hold (for example a web form). It allows calls, not texts: the carrier campaign registers texting-first opt-in only. It never lifts a STOP; only the person’s own START does.
Outbound calls
POST /api/v1/phone/calls/outbound with {numberId, to, botId, purpose} returns {consentRef, basis, expiresAt} when the callee texted or called the number first, or has an explicit consent record, and hasn’t opted out. Otherwise 403 {"error":"no_consent"}. The dial itself belongs to the voice service, which presents the consentRef.
Warm transfers
A warm transfer rings a third party, so the call worker refuses it unless the control carries aconsentRef. Cloud-api attaches one when it forwards POST /api/v1/voice/calls/{callId}/control with {"action":"transfer","to":"+1…","mode":"warm"}; cold transfers need none.
The reference lives 30 minutes in
call_consents with purpose: "transfer". STOP always wins over both bases.
Transfer targets
The owner approves who a bot may transfer to:PUT /api/v1/voice/bot-config/{botId} with {"transferTargets":[{"e164":"+14155550100","label":"Front desk"}]}. The list replaces the stored one (up to 20 E.164 numbers, deduplicated; [] clears it; leaving the field out keeps it). The response includes transferTargets. Each newly approved target logs an explicit consent row with source owner_transfer_target on the bot’s live numbers. Cloud-api migration 036 adds voice_bot_config.transfer_targets; apply it by hand to prod like 020 and 021.
The bot call settings screen in allternit-ai edits this list as “Transfer targets” (a small separate change on the app side).