Phone lines in your app via the API
Offer phone lines — Brazilian landline numbers for activating WhatsApp Business accounts — without sending your customer out of your product. Your page shows the numbers, the buy button and the activation code; your backend talks to/v1/phone-lines with your tenant key.
There is no hosted page and no iframe here: everything goes through your backend, because the activation code is a credential and the ps_ key must never reach the browser.
Prefer a running start? Download the MIT-licensed demo — a Node + TypeScript (Express) backend that holds the key and a React + Vite frontend, wired to exactly this flow: phone-lines-demo.zip. Unzip, put your
ps_ key in PILOT_TENANT_KEY (and, for the webhook, a public URL in PUBLIC_BASE_URL), npm run dev (or docker compose up) — frontend on :5175, backend on :8789.This page is the step-by-step: follow the five steps in order and you have a working integration. The reference — every field, state and error code — is the Phone Lines API. This page links there instead of restating it, so you always read one copy.
One credential
You need exactly one thing: a tenant-scopedps_ API key. Copy it from /profile, under the API tab.
A line belongs to the workspace, not to a WhatsApp number, so a number-scoped key gets 403 NUMBER_SCOPE_NOT_ALLOWED on every /v1/phone-lines endpoint. You do not need any WhatsApp number connected to buy a line.
Keep the ps_ key on your server — the browser never calls the Pilot Status API, it only calls your backend.
Architecture
/api/... routes above are the demo’s — the names are yours to pick; what matters is that each one is a call from your server to /v1/phone-lines.
The five steps
1
Check that the workspace can buy
Renting a line requires a verified account holder (CNPJ or CPF, the document, and contact confirmation), done once, before the first purchase. Before you show a buy button, read the state:Decide on
cURL
canPurchase: it is true for APPROVED and IN_REVIEW — under manual review you can already buy. With false, the purchase answers 403 VERIFICATION_REQUIRED (or VERIFICATION_REJECTED when it was rejected), so show your customer the way forward instead of the button:- In the dashboard — Phone lines (
/linhas) → Buy a line, which walks through the verification. This is what the demo does: it only reads the state and points to the dashboard. - From code — if you want to run the verification inside your product, it is in the API too: submit the details and the document, confirm the contact codes and read the decision in Account holder verification.
phone_line.verification_updated webhook (step 5).2
Search numbers by area code
cURL
display and keep the number — it is exactly the value the purchase takes. price and currency are what one line costs your workspace per month — your own phone line price included — so you can show the price before the purchase. A missing or malformed ddd answers 400 INVALID_AREA_CODE, without calling the carrier.- Search once and reuse it. Every search reaches our carrier and spends a request quota shared by the whole platform; when it runs out, the answer is 503
SUPPLIER_UNAVAILABLE. Do not poll this endpoint. - Searching is not reserving. A listed number can be bought by someone else before your purchase reaches it; it then comes back as
NUMBER_UNAVAILABLEin the purchase result.
3
Buy with an Idempotency-Key
POST /v1/phone-lines buys 1 to 10 numbers and moves money, so it requires the Idempotency-Key header. Generate the key on your backend, one per purchase, and store it with the order before sending — it is what you resend when the answer does not arrive.cURL
Response (200)
200 when it was processed — a 200 does not mean you bought anything: read results[i].ok for each number and keep the lineId of those that succeeded. The full table of per-number codes is in Buy lines.Payment. Each number costs the full monthly price, taken from the prepaid wallet first and, for whatever is left, from the saved card. There is no checkout link in this call: with no credits and no card, the item fails with PAYMENT_FAILED and nothing is charged. Before buying, top up the wallet — in the dashboard, or through the API with Pix, a hosted card page or a card form on your own page — or save a card. See Wallet and card via API. Price and billing cycle: Pricing and payment.The retry pattern:- On a timeout, a
5xxor a network error, resend the same body with the same key. A number the first attempt already handled comes back asREQUEST_ALREADY_PROCESSEDand is not charged again; the ones it never reached are processed now. - Reconcile with
GET /v1/phone-lines, matching bynumber.REQUEST_ALREADY_PROCESSEDdoes not say whether that number was bought or failed — the list does: a bought number is in it. - Retry the failures with a new key. After a
PAYMENT_FAILEDorSUPPLIER_UNAVAILABLE, fix the cause and send those numbers with a new key — with the same one they come back asREQUEST_ALREADY_PROCESSEDfor an hour.
4
Get the WhatsApp activation code
WhatsApp verifies a landline by calling it and speaking a 6-digit code. The order matters:The window is 5 minutes (
- Open the request first.
POST /v1/phone-lines/{id}/activations(no body), with thelineIdfrom the purchase. A call that arrives before the request exists is not captured. - Only then ask WhatsApp to call. In WhatsApp Business, choose to receive the code by phone call (Call me). The line is a landline and does not receive SMS — waiting for an SMS only burns the window.
cURL
Response (201)
pollDeadline). Show your customer a countdown and the instruction “tap Call me now”.Poll the request every few seconds with GET /v1/phone-lines/activations/{activationId} — the status only changes after our next check of the line, every 15 seconds. Keep going while status is WAITING, or CAPTURED with failureReason: null: CAPTURED is also the in-between state while the code is read, which can take a few minutes. Stop at a final state and check code — TRANSCRIBED does not guarantee a code. Every state: Statuses.The recording goes through your backend. When hasAudio is true, your customer can listen to the call — useful when the code could not be read. GET /v1/phone-lines/activations/{activationId}/audio requires your key, so your backend fetches the file and streams it to the browser (in the demo, GET /api/activations/:id/audio). Pass Cache-Control: private, no-store along too: the recording speaks the code. See Download the recording.Cancel the wait. There is one waiting request per line. If your customer wants to start over before the window closes, call POST /v1/phone-lines/{id}/activations/{activationId}/cancel and open a new request right away.Full reference: Activation codes.
5
Register the line webhook through the API
For your system to learn right away that a code arrived, a renewal failed or a line was suspended, register a line webhook. It is a registry separate from the number webhooks: the numbers’ Inside this registry,
POST /v1/webhooks does not receive phone_line.* events. The line registry is POST /v1/phone-lines/webhooks:cURL
Response (201)
"*" means every phone-line event. The URL must be https and public: localhost, a private IP or a name that resolves to one is refused with 400 WEBHOOK_URL_REFUSED — during development, use a tunnel with a public URL in PUBLIC_BASE_URL.Verify the signature and deduplicate. Every delivery carries x-pilot-status-signature: the HMAC-SHA256 of the raw body, hex-encoded, keyed with the whole secret (including plwh_). Compute it over the bytes exactly as received, before parsing the JSON, and compare in constant time:Backend — webhook receiver (Express)
- Deduplicate on
id. It is the same on every retry of a delivery (and also comes in theIdempotency-Keyheader). Delivery is at least once, order is not guaranteed, and the signature has no timestamp. - Answer
2xxwithin 10 seconds. Anything else counts as a failure; there are 6 attempts over about 15 minutes, and a3xxredirect is not followed. - Events are notifications, not the source of truth. A
phone_line.code_receivedsaves you the polling of step 4, but if your endpoint was down for longer than the retries, read the current state withGET /v1/phone-linesorGET /v1/phone-lines/activations/{activationId}.
active: false, deleting and reading the delivery log are in the API too — PATCH and DELETE /v1/phone-lines/webhooks/{id} and GET …/{id}/deliveries. Every event, payload and endpoint: Phone line webhooks.Example backend
The step that needs the most care is the purchase: it is the only one that moves money. Your backend generates and stores theIdempotency-Key before sending, repeats with the same key when the answer does not arrive, and reconciles against the list of lines.
Backend — Node.js / Express
PAYMENT_FAILED or SUPPLIER_UNAVAILABLE, your customer fixes the cause and makes a new purchase — a new purchaseId, and therefore a new key.
Cancel a line
DELETE /v1/phone-lines/{id} answers with the line as it is afterwards, and the result depends on its status:
cURL
Tell your customer which of the two cases happened: from the response’s
status and cancelAtPeriodEnd, not from the 200.
Other paths
The API is the path for putting lines inside your own product. Depending on your case, there are alternatives:- In the dashboard. To buy and activate a few lines without writing code — and to verify the account holder — follow Phone lines. Dashboard-only: naming a line, undoing a scheduled cancellation and paying overdue lines on the spot.
- Playground. Try each endpoint with your key before writing the backend: Playground → Phone Lines. Buying and cancelling there are real.
- AI builders. If you use Lovable, Replit, Bolt, v0 or Cursor, there are ready-made prompts that generate this same flow in Phone lines for AI builders.
- AI agents (MCP). The MCP server has the account holder verification tools (
phone_lines_verification_*) and the recording tool (phone_lines_activation_audio), under the same tenant-scoped credential rules.
Next steps
- Full reference: Phone Lines API — Overview
- Verify the holder from code: Account holder verification
- Purchase and idempotency in detail: Buy lines
- Code request states: Activation codes
- Webhook events, payloads and endpoints: Phone line webhooks
- Price and renewal: Phone line pricing
- Key scopes and headers: API authentication
- Prompts for AI builders: Phone lines for AI builders