Phone lines for AI builders
You are building a CRM or SaaS on Lovable, Replit, Bolt, v0 or Cursor, and you want your customers to buy and activate phone lines — Brazilian landline numbers for activating WhatsApp Business accounts — without leaving your product. This page gives you four prompts. Paste them into your builder in order and you get the same flow as Phone lines in your app via the API.Rather start from working code? Download the MIT-licensed demo of this exact flow (a Node + TypeScript backend that holds the key and a React + Vite frontend): phone-lines-demo.zip. Drop it into your builder, or run it locally with
npm run dev.Why everything goes through your backend
There is no hosted page and no iframe here. Your page shows the numbers, the buy button and the activation code, but your backend is the one that talks to the Pilot Status API, for three reasons:- The
ps_key authorizes everything in your account — including buying lines, which charges money. - The activation code is a credential. Whoever has it can register a WhatsApp account on the line, and the call recording speaks it.
- The browser cannot call the API. CORS on the API is a strict allowlist of Pilot Status’s own domains, so a
fetchfrom*.lovable.appor*.replit.appis blocked — no key changes that.
Before you start
- A tenant-scoped API key (
ps_...) from the API tab of your profile (/profile). A number-scoped key gets 403NUMBER_SCOPE_NOT_ALLOWEDon every/v1/phone-linesendpoint. You do not need any WhatsApp number connected. - A verified account holder (CNPJ or CPF, the document and contact confirmation), done once before the first purchase. The simplest path is the dashboard: Phone lines → Buy a line. Prompt 2 only reads that state and sends the customer there when it is missing.
- Wallet credit or a saved card. A purchase has no checkout link: with no balance and no card, the number fails with
PAYMENT_FAILEDand nothing is charged. - A backend. Every builder listed above can run one: a Replit server, a Next.js route handler on v0, a Supabase Edge Function on Lovable or Bolt. For the webhook (Prompt 4) it needs a public
httpsURL.
How the pieces fit
ps_ key only ever exists on your backend’s arrow.
The four prompts
Paste them in order. Each one is self-contained — everything the AI needs is written into the prompt itself.Prompt 1 — Backend that holds the key and buys with an Idempotency-Key
Each number costs the full monthly price, taken first from the wallet and, for whatever is missing, from the saved card. A
PAYMENT_PROVIDER_ERROR means the card may have been charged: check the statement before trying again. All the rules are in Buy lines.Prompt 2 — Purchase screen with the verification check
Prompt 3 — Activation code, recording and cancellation
The request states, the
failureReason values and the timings (the line is checked every 15 seconds, up to 3 transcription attempts) are in Activation codes.Prompt 4 — Phone-line webhook, with signature
Mistakes the AI usually makes
These account for nearly every broken integration. If something does not work, check them in this order.The ps_ key ends up in the browser
The ps_ key ends up in the browser
The builder reads it from
VITE_PILOT_KEY or NEXT_PUBLIC_PILOT_KEY because that is the fastest way to make a fetch call compile. Those prefixes inline the value into the shipped bundle, and anyone who opens DevTools can buy lines on your account. The key belongs in a plain server-side variable such as PILOT_TENANT_KEY. If you find it in the bundle, rotate the key in the dashboard before fixing the code.A new idempotency key on every attempt
A new idempotency key on every attempt
The AI generates
crypto.randomUUID() inside the function that does the fetch, so every retry — and every repeated click — becomes a new purchase, charged again. The Idempotency-Key is born once per purchase, saved before sending, and resent unchanged on timeout, network error or 5xx. A new key only for a new purchase, after fixing the cause of a PAYMENT_FAILED or SUPPLIER_UNAVAILABLE. See Idempotency.Treating the purchase's 200 as success
Treating the purchase's 200 as success
POST /v1/phone-lines answers 200 when it processed the request, even if every number failed. Each number’s outcome is in results[i].ok. And a REQUEST_ALREADY_PROCESSED does not say whether the number was bought: compare with GET /v1/phone-lines by number.Telling the user to tap Call me before the request is open
Telling the user to tap Call me before the request is open
A call that arrives before
POST /v1/phone-lines/{id}/activations is not captured. The “tap Call me” instruction only appears after the request answers 201. And the line is a landline: waiting for an SMS only burns the 5-minute window.Stopping the poll at CAPTURED
Stopping the poll at CAPTURED
The AI stops at the first status other than
WAITING. But CAPTURED with failureReason: null is the intermediate state while the code is read, and it can last a few minutes. Keep polling until a final state, and check code — TRANSCRIBED does not guarantee a code. Every state: Statuses.Registering the webhook with POST /v1/webhooks
Registering the webhook with POST /v1/webhooks
POST /v1/webhooks is the registration for number webhooks and never receives phone_line.* events, not even with "*". The phone-line one is POST /v1/phone-lines/webhooks — and the plwh_ secret it returns appears only in that response. See Phone line webhooks.Verifying the signature over already-parsed JSON
Verifying the signature over already-parsed JSON
The framework parses the body before the handler, and the AI computes the HMAC over
JSON.stringify(req.body). It never matches. The signature is over the received bytes: read the raw body (express.raw, request.text()) on that route only, verify, and only then parse.Next steps
Phone lines via the API
The same flow step by step, with the sample purchase backend and the webhook receiver.
Phone Lines API reference
Every field, state and error code of
/v1/phone-lines.Account holder verification
To run verification inside your product instead of sending the customer to the dashboard.
Phone line webhooks
Events, payloads, signature and retries.