Skip to main content

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.
To build a checkout inside your own app — card fields on your page, no redirect — follow the guide Transparent checkout with Stripe Elements.

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 403 NUMBER_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 403 PERMISSION_DENIED. A workspace key has no user, so no role applies.
  • Currency: the amount is always in the workspace’s currency (BRL or USD). 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/or pix.qrCode (the QR code image as a data:image/png;base64,… URI, ready for an <img src>; it can be null) to whoever pays. A Pix expires in 1 hour (pix.expiresAt).
  • Keep orderId: it is how you follow the payment with GET /v1/billing/topups/{id} and how the wallet.topup_paid webhook names it.
  • A USD workspace gets 400 PIX_NOT_ELIGIBLE. Use a card.
  • successUrl and cancelUrl have no meaning for a Pix: sending them gets 400 FIELD_NOT_APPLICABLE.

Top up or save a card on a hosted Stripe page

The same endpoint, with method: "card" (the default), answers with a url to a page hosted by Stripe. Open it in the browser of whoever pays.
Response (200)
With 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.
The hosted card rails need a billing e-mail for the workspace; without one the answer is 400 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 paid when the payment provider confirms the payment to us.
  • Prefer the webhook to polling. wallet.topup_paid arrives once per credited order. It is sent only after the order is completed, so when it arrives this endpoint already answers paid.
  • An id from another workspace gets the same 404 TOPUP_NOT_FOUND as 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 calls sync.
Card data never reaches Pilot Status, and must never reach your server either. The card number goes from the payer’s browser straight to Stripe, inside Stripe’s own fields. Your API key stays on your server. This keeps card data out of your systems and out of ours, which is what keeps your PCI DSS scope small. Never collect card numbers in your own inputs and never send them to any API.
All four endpoints:
  • answer with Cache-Control: no-store — a clientSecret lets whoever holds it confirm the intent, so do not cache or log it;
  • return the publishableKey to load Stripe.js with, so you do not need any Stripe account of your own;
  • answer 503 PAYMENTS_NOT_CONFIGURED when 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)
On your page, mount the Payment Element with clientSecret and call stripe.confirmSetup(...). When it resolves, call: POST https://pilotstatus.com.br/v1/billing/card/setup-intent/{setupIntentId}/sync
Response (200)
  • sync reads the intent from Stripe; it never trusts your page. When Stripe says succeeded, the card becomes the workspace’s saved card, replacing the previous one.
  • Not succeeded yet (processing, requires_action, …): saved: false, card: null and Stripe’s status. 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_… → 400 INVALID_SETUP_INTENT_ID. An id we did not create for this workspace → 404 SETUP_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.
Any other body field gets 400 UNKNOWN_FIELDS.
On your page, call stripe.confirmPayment(...) with clientSecret. When it resolves, call: POST https://pilotstatus.com.br/v1/billing/payment-intents/{paymentIntentId}/sync
Response (200)
  • sync never charges. It is not Stripe’s /confirm: it only reconciles what Stripe already decided. The wallet is credited exactly once, by sync or by our Stripe notification, whichever comes first.
  • An id that is not a pi_… → 400 INVALID_PAYMENT_INTENT_ID. An id we did not create as a top-up of this workspace → 404 PAYMENT_INTENT_NOT_FOUND.
  • The orderId works on GET /v1/billing/topups/{id} like any other top-up.

Idempotency-Key

Send it on POST /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, paymentIntentId and clientSecret, with the header Idempotent-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 a code 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.