Skip to main content

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-scoped ps_ 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

Only your backend touches the Pilot Status API. The /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:
cURL
Decide on 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.
Both are the same verification: what you do through one shows up in the other. State changes also arrive as the phone_line.verification_updated webhook (step 5).
Buying under IN_REVIEW depends on the review. If it is rejected, the lines bought during the review are returned to the carrier 3 days later, with their charges refunded to the wallet — see identity verification.
2

Search numbers by area code

cURL
Show your customer the 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_UNAVAILABLE in the purchase result.
Reference: Search numbers.
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)
The numbers are independent. Each one is charged and acquired on its own, and one that fails does not undo the others. The request answers 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:
  1. On a timeout, a 5xx or a network error, resend the same body with the same key. A number the first attempt already handled comes back as REQUEST_ALREADY_PROCESSED and is not charged again; the ones it never reached are processed now.
  2. Reconcile with GET /v1/phone-lines, matching by number. REQUEST_ALREADY_PROCESSED does not say whether that number was bought or failed — the list does: a bought number is in it.
  3. Retry the failures with a new key. After a PAYMENT_FAILED or SUPPLIER_UNAVAILABLE, fix the cause and send those numbers with a new key — with the same one they come back as REQUEST_ALREADY_PROCESSED for an hour.
A 500 or a timeout does not prove that nothing happened. Numbers are processed one after the other, and a failure midway can come after earlier numbers were already charged. Never generate a new key to “retry” an answer that did not arrive — that can buy and charge the same order again. A PAYMENT_PROVIDER_ERROR asks for something else: the card may have been charged, so check the card statement before trying again. The exact rules are in Idempotency.
The full code of this step — persisted key, retry and reconciliation — is in the Example backend below.
4

Get the WhatsApp activation code

WhatsApp verifies a landline by calling it and speaking a 6-digit code. The order matters:
  1. Open the request first. POST /v1/phone-lines/{id}/activations (no body), with the lineId from the purchase. A call that arrives before the request exists is not captured.
  2. 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)
The window is 5 minutes (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.
The activation code is a credential. Whoever holds it can register a WhatsApp account on the line. Show it only to your authenticated customer, do not log it and do not cache it — API responses already carry Cache-Control: no-store.
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’ POST /v1/webhooks does not receive phone_line.* events. The line registry is POST /v1/phone-lines/webhooks:
cURL
Response (201)
Inside this registry, "*" 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.
The plwh_ secret appears once, in this response. Store it on your server right away: no read returns it later, and there is no rotation. To change it, create a new webhook, move your receiver to its secret and delete the old one. Creating is not idempotent: two POSTs create two webhooks with two secrets — list the existing ones with GET /v1/phone-lines/webhooks before creating another.
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 the Idempotency-Key header). Delivery is at least once, order is not guaranteed, and the signature has no timestamp.
  • Answer 2xx within 10 seconds. Anything else counts as a failure; there are 6 attempts over about 15 minutes, and a 3xx redirect is not followed.
  • Events are notifications, not the source of truth. A phone_line.code_received saves you the polling of step 4, but if your endpoint was down for longer than the retries, read the current state with GET /v1/phone-lines or GET /v1/phone-lines/activations/{activationId}.
Editing (name, URL, events), pausing with 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 the Idempotency-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
For the numbers that came back with PAYMENT_FAILED or SUPPLIER_UNAVAILABLE, your customer fixes the cause and makes a new purchase — a new purchaseId, and therefore a new key.
The ps_ key is a server secret. Never put it in a variable exposed to the browser (VITE_..., NEXT_PUBLIC_...) or return it in JSON the frontend can read. The same goes for the webhook’s plwh_ secret.

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.
A returned number stops being yours, and the history is erased. Nothing undoes a return, there is no guarantee of buying the same number back, and the WhatsApp account activated on it stays tied to it. When the line reaches RETURNED or CANCELED, its codes and call recordings are deleted. Undoing a scheduled cancellation is dashboard-only. See Cancel a line.

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