Wallet and card via API
Every charge of a workspace — numbers, plan, phone lines — comes out of its prepaid wallet first and, for whatever the wallet does not cover, from its saved card. These endpoints let your system fund both without sending your customer to the dashboard:
Nothing is charged by these calls. They only issue the charge: a person pays the Pix, fills in the hosted page, or confirms the card on your page. The wallet is credited when the payment is confirmed, and the
wallet.topup_paid webhook tells you.
Credentials and permissions
- A workspace-level credential: a workspace key, or an account key with
x-workspace-id. A number key, or a token limited to some numbers, gets 403NUMBER_SCOPE_NOT_ALLOWED. - Role, with an account key or an OAuth/MCP token: topping up and saving a card are Owner actions; reading a top-up status (
GET /v1/billing/topups/{id}) needs Owner or Admin. A lower role gets 403PERMISSION_DENIED. A workspace key has no user, so no role applies. - Currency: the amount is always in the workspace’s currency (
BRLorUSD). It cannot be chosen per request.
Top up with Pix
Pix is available on BRL workspaces.POST /v1/billing/checkout with purpose: "wallet_topup", method: "pix" and the amount:
- Show
pix.copyPaste(the copy-and-paste code) and/orpix.qrCode(the QR code image as adata:image/png;base64,…URI, ready for an<img src>; it can benull) to whoever pays. A Pix expires in 1 hour (pix.expiresAt). - Keep
orderId: it is how you follow the payment withGET /v1/billing/topups/{id}and how thewallet.topup_paidwebhook names it. - A USD workspace gets 400
PIX_NOT_ELIGIBLE. Use a card. successUrlandcancelUrlhave no meaning for a Pix: sending them gets 400FIELD_NOT_APPLICABLE.
Top up or save a card on a hosted Stripe page
The same endpoint, withmethod: "card" (the default), answers with a url to a page hosted by Stripe. Open it in the browser of whoever pays.
Response (200)
purpose: "add_card" the page saves a card on the workspace, with no charge. The response is { "url": "…", "purpose": "add_card" }. The card becomes the workspace’s saved card, on every plan: it backs the wallet for every charge — numbers, plan renewals and phone lines. method does not apply to add_card (400 FIELD_NOT_APPLICABLE).
string
required
wallet_topup or add_card. Anything else → 400 INVALID_PURPOSE.number
Required for
wallet_topup: a positive number in the workspace’s currency. Missing, zero, negative or not a number → 400 INVALID_AMOUNT.string
default:"card"
card or pix, only for wallet_topup. Anything else → 400 INVALID_METHOD.string
Card only. Where the hosted page sends the browser after paying or saving.
string
Card only. Where the hosted page sends the browser when the person gives up.
NO_BILLING_EMAIL. Pix does not.
Follow a top-up
GET https://pilotstatus.com.br/v1/billing/topups/{id}, with the orderId of any top-up — Pix, hosted page or Elements.
Response (200)
- It only reads. It calls no payment provider, so polling it is cheap. The order turns
paidwhen the payment provider confirms the payment to us. - Prefer the webhook to polling.
wallet.topup_paidarrives once per credited order. It is sent only after the order is completed, so when it arrives this endpoint already answerspaid. - An id from another workspace gets the same 404
TOPUP_NOT_FOUNDas an id that does not exist.
Card on your own page (Stripe Elements)
With these endpoints the card fields are on your page, built with Stripe’s Payment Element. Your server asks us for an intent; your page confirms it with Stripe.js; your server callssync.
All four endpoints:
- answer with
Cache-Control: no-store— aclientSecretlets whoever holds it confirm the intent, so do not cache or log it; - return the
publishableKeyto load Stripe.js with, so you do not need any Stripe account of your own; - answer 503
PAYMENTS_NOT_CONFIGUREDwhen card payments are not available on our side. Offer Pix (BRL) or try again later.
Save a card
POST https://pilotstatus.com.br/v1/billing/card/setup-intent, with no body (or {}). Any field gets 400 UNKNOWN_FIELDS.
Response (200)
clientSecret and call stripe.confirmSetup(...). When it resolves, call:
POST https://pilotstatus.com.br/v1/billing/card/setup-intent/{setupIntentId}/sync
Response (200)
syncreads the intent from Stripe; it never trusts your page. When Stripe sayssucceeded, the card becomes the workspace’s saved card, replacing the previous one.- Not succeeded yet (
processing,requires_action, …):saved: false,card: nulland Stripe’sstatus. Call again later, or rely on our side: Stripe also notifies us, and the card is saved then. - Idempotent. Calling it again, or racing our own Stripe notification, saves the card once.
- An id that is not a
seti_…→ 400INVALID_SETUP_INTENT_ID. An id we did not create for this workspace → 404SETUP_INTENT_NOT_FOUND.
Top up with a card
POST https://pilotstatus.com.br/v1/billing/payment-intents
string
required
wallet_topup, the only value today. Anything else → 400 INVALID_PURPOSE.number
required
A positive number in the workspace’s currency, rounded to cents. Not positive → 400
INVALID_AMOUNT. Below Stripe’s minimum card charge → 400 AMOUNT_TOO_SMALL.boolean
default:"false"
true also saves this card as the workspace’s card once the payment succeeds. Not a boolean → 400 INVALID_FIELD_TYPE.string
Optional. Up to 255 characters, no control characters (otherwise 400
IDEMPOTENCY_KEY_INVALID). See below.UNKNOWN_FIELDS.
stripe.confirmPayment(...) with clientSecret. When it resolves, call:
POST https://pilotstatus.com.br/v1/billing/payment-intents/{paymentIntentId}/sync
Response (200)
syncnever charges. It is not Stripe’s/confirm: it only reconciles what Stripe already decided. The wallet is credited exactly once, bysyncor by our Stripe notification, whichever comes first.- An id that is not a
pi_…→ 400INVALID_PAYMENT_INTENT_ID. An id we did not create as a top-up of this workspace → 404PAYMENT_INTENT_NOT_FOUND. - The
orderIdworks onGET /v1/billing/topups/{id}like any other top-up.
Idempotency-Key
Send it onPOST /v1/billing/payment-intents so that a retry after a timeout does not open a second order.
- Absent: every call creates a new order and a new payment intent.
- Same key and same body within 24 hours: you get the same
orderId,paymentIntentIdandclientSecret, with the headerIdempotent-Replayed: true. Nothing new is created. - Same key, different body: 409
IDEMPOTENCY_KEY_REUSED. Use a new key. - Same key while the first call is still running: 409
IDEMPOTENCY_KEY_IN_USE. Retry in a moment. - Keys are per workspace. A call that failed validation, or failed afterwards, does not spend the key.
Errors
Errors carry acode and an error message given as English | Português — on POST /v1/billing/checkout too since 2026-10-29 (INVALID_PURPOSE, INVALID_AMOUNT, NO_BILLING_EMAIL and CHECKOUT_FAILED used to be English only; codes and statuses did not change). Branch on code, never on the sentence. The Elements endpoints put extra detail in details (for example details.unknownFields).
The authentication errors (
401, 400 WORKSPACE_ID_REQUIRED, 403 WORKSPACE_MEMBERSHIP_REQUIRED) are described in Key types.
Reseller workspaces and the shared card
A reseller’s end-customer workspace may be linked to the reseller’s shared card (reserve card). The reseller controls the link per workspace:"shareCard": true on POST /v1/workspaces (absent or false: not shared), and PUT / DELETE /v1/workspaces/{id}/shared-card afterwards; the Pilot Status team only configures which of the reseller’s workspaces holds that card. Before 2026-10-29, workspaces created by a reseller inherited it automatically. That card only backs the charges the platform makes — renewals, numbers, lines — when the workspace’s wallet does not cover them and the workspace has no card of its own. It is never used by these endpoints: a top-up here is paid by whoever pays the Pix, the hosted page or your Elements form, and add_card or a setup intent saves a card on the workspace itself, which then takes precedence over the shared one. See Billing for resellers.