Skip to main content

Transparent checkout with Stripe Elements

With a transparent checkout, the card form is on your page: no redirect to a hosted page, no dashboard. Your customer types the card into fields rendered by Stripe; Pilot Status credits the wallet or saves the card. Use it to fund a workspace — for example, each end customer’s workspace when you resell — from inside your own product. The endpoints are described in Wallet and card via API. This guide shows how the pieces fit.

How it works

  1. Your server asks Pilot Status for an intent, with the API key.
  2. Your page loads Stripe.js with the publishableKey from the answer, mounts the Payment Element with the clientSecret, and confirms with Stripe.
  3. Your server calls sync, which reads the result from Stripe and credits the wallet (or saves the card) at once.
You do not need a Stripe account: the publishableKey and the intents are ours.
PCI: card data never reaches your server or ours. The card number, expiry and CVC are typed into Stripe’s own fields, inside an iframe served by Stripe, and go straight from the browser to Stripe. Your server only ever sees the clientSecret and the ids. Keep it that way:
  • load Stripe.js from https://js.stripe.com, never from a copy you host;
  • never build your own card inputs, and never send card data to any API, ours included;
  • keep the API key on your server only. The browser gets the clientSecret, never the key;
  • do not log the clientSecret: whoever holds it can confirm the intent.

1. Your server: create the intent

Expose one route on your backend that calls Pilot Status. With an account key, add x-workspace-id to fund a specific workspace.
server.js (Node 18+)
  • Idempotency-Key: generate one id per checkout attempt in the browser and reuse it on retries. A retry within 24 hours gets the same intent back instead of a second order.
  • 503 PAYMENTS_NOT_CONFIGURED: card payments are unavailable on our side. Show another option — on BRL workspaces, Pix.

2. Your page: mount the Payment Element and confirm

checkout.html

3. Your server: sync and react

sync answers with the result straight away:
  • orderStatus: "COMPLETED" — the wallet has the money. credited is true on the call that credited it, and balance is the new balance.
  • orderStatus: "PENDING" — Stripe has not confirmed yet. Do nothing: when Stripe confirms, we credit the wallet on our own.
  • orderStatus: "FAILED" — the payment was cancelled. Start a new attempt with a new Idempotency-Key.
The wallet is credited once, whether by your sync or by our own notification from Stripe. To learn about it on your side without polling, subscribe a workspace webhook to wallet.topup_paid: it fires once per credited top-up, for Pix and card alike.

Browser error but charged

stripe.confirmPayment can come back with a network error even when Stripe has already charged the card — the connection dropped after the charge and before the answer reached the browser. This happened in production on 2026-10-29. Never show “payment failed” on that error alone. For an error of type api_connection_error (or any error that is not a card decline), check with the server before telling the customer anything:
  1. Poll POST /v1/billing/payment-intents/{paymentIntentId}/sync — or GET /v1/billing/topups/{orderId} with the orderId from step 1 — every 2–3 seconds for about 30 seconds.
  2. Stop as soon as sync answers orderStatus: "COMPLETED" (or the top-up answers status: "paid"): the payment went through.
  3. If it is still pending after ~30 s, tell the customer the payment is being confirmed, not that it failed. sync answering orderStatus: "FAILED" (or the top-up status: "failed") is the only failure you show.
The source of truth is the wallet.topup_paid workspace webhook: it fires once per credited top-up, even when the browser never learned the payment succeeded. Reconcile your records from it, not from what the browser saw.

Saving a card without charging

The flow is the same with a setup intent:
  1. Server: POST /v1/billing/card/setup-intent with no body → clientSecret, setupIntentId, publishableKey.
  2. Page: stripe.elements({ clientSecret }), mount "payment", then stripe.confirmSetup({ elements, redirect: "if_required" }).
  3. Server: POST /v1/billing/card/setup-intent/{setupIntentId}/sync → saved: true and the card (brand, last 4 digits, expiry).
The card becomes the workspace’s saved card, replacing any previous one. From then on it backs the wallet on every charge: numbers, plan renewals and phone lines.

Testing

Use the Stripe test cards only in an environment whose publishableKey starts with pk_test_. In production (pk_live_) every confirmed payment is a real charge.