Skip to main content

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 fetch from *.lovable.app or *.replit.app is blocked — no key changes that.
AI builders get all three wrong by default. Every prompt below repeats the rules, because the AI reading each prompt cannot see this page.

Before you start

  1. A tenant-scoped API key (ps_...) from the API tab of your profile (/profile). A number-scoped key gets 403 NUMBER_SCOPE_NOT_ALLOWED on every /v1/phone-lines endpoint. You do not need any WhatsApp number connected.
  2. 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.
  3. Wallet credit or a saved card. A purchase has no checkout link: with no balance and no card, the number fails with PAYMENT_FAILED and nothing is charged.
  4. 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 https URL.
The ps_ key is a server-side secret. It must never appear in frontend code, and never in a variable prefixed VITE_, NEXT_PUBLIC_, REACT_APP_ or PUBLIC_ — those are compiled into the JavaScript bundle your users download. The same goes for the webhook’s plwh_ secret.

How the pieces fit

The 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 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.
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.
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.
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.
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.
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.
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.