This is the bridge between a customer is at your counter and you can charge their card later for the rental, deposit, or damage. The flow uses Stripe's standard SetupIntent pattern: the customer's card details never touch our servers; they go straight from the browser/iPad to Stripe.
Where this fits
The walk-in POS wizard creates the booking and starts the rental, but doesn't yet collect a card as part of that flow (Stripe Elements integration is a roadmap item). For now, attach a card via this two-step flow before or after creating the booking, then any subsequent charge or refund uses that saved payment method.
The two-step flow
Step 1 — Operator initiates
The operator UI calls:
POST /api/customers/[customer_id]/payment-methods
{ "action": "create_setup_intent" }
Response:
{
"ok": true,
"clientSecret": "seti_xxxxx_secret_yyyyy",
"setupIntentId": "seti_xxxxx",
"stripeCustomerId": "cus_xxxxx"
}
The endpoint:
- Looks up the customer
- Creates a Stripe customer placeholder if the customer doesn't have one
- Creates a SetupIntent against that Stripe customer with
usage='off_session' - Returns the SetupIntent's client secret
Step 2 — Customer confirms (Stripe Elements)
The operator UI mounts Stripe Elements client-side using the
publishable key (test or live — the per-subaccount router auto-picks
based on is_demo). The customer enters card details on the iPad or
their phone. Stripe Elements confirms the SetupIntent client-side and
returns a paymentMethodId.
Step 3 — Operator attaches
POST /api/customers/[customer_id]/payment-methods
{
"action": "attach_payment_method",
"paymentMethodId": "pm_xxxxx",
"setAsDefault": true
}
The endpoint:
- Verifies the payment method belongs to the customer's Stripe customer
- Attaches it (idempotent if already attached)
- Sets it as default in Stripe and in our
payment_methodstable - Returns the persisted record
Why two steps
The split exists because the actual card details never touch our
backend. Stripe Elements collects them client-side and sends them
directly to Stripe. Our backend only knows the resulting paymentMethodId,
which is a token that lets us charge but doesn't reveal the card number.
This is PCI compliance Out Of The Box — you don't need to handle PAN data at any point.
What "isTestMode" means in practice
The endpoint reads subaccounts.is_demo. If the customer's subaccount is
demo-flagged AND STRIPE_SECRET_KEY_TEST is configured, the SetupIntent
runs in test mode. Otherwise live mode. This means demo-account testing
can use 4242 4242 4242 4242 while production accounts continue with
real cards.
The test_mode flag is also written to the SetupIntent metadata for
audit clarity.
Test card numbers
When you're testing on the demo subaccount:
| Card | Behavior |
|---|---|
4242 4242 4242 4242 | Succeeds |
4000 0027 6000 3184 | Requires 3DS authentication |
4000 0000 0000 9995 | Declines — insufficient funds |
Any future expiry, any CVC.
Error cases
| Error | Cause | Fix |
|---|---|---|
| "Customer not found" | Bad customer ID | Check the URL parameter |
| "Payment method belongs to a different Stripe customer" | The PM was created for a different customer | Re-create the SetupIntent for this customer |
| "paymentMethodId is required for attach" | Missing parameter on attach call | Frontend bug — make sure step 2's PM ID is passed to step 3 |
| Stripe API errors (rate limit, etc.) | Transient | Retry with exponential backoff |
What happens to the saved card
Once attached:
- The card appears on the customer detail page
- The walk-in wizard's customer search shows the customer has a card on file (no warning to add one)
- Any future deposit hold or off-session charge uses this card by default
- The customer can use the same card on their next visit without re-entering details
Removing a card
To detach a payment method, use the customer detail page's "Remove" button
or call Stripe's paymentMethods.detach directly via the API. Detaching
removes the PM from both Stripe and our payment_methods table.
Fully-built UI status
Today the SetupIntent + attach flow is API-only. The operator-facing "Add card" button on the customer detail page is on the roadmap. For now, trigger the flow via:
- Postman / curl with the endpoints above
- A custom internal page if you build one
- Eventually: a "Add card" modal on the walk-in wizard step 1
This is the largest remaining gap in end-to-end UI for shop rentals; we're prioritizing it as a near-term follow-on.
Running a fleet on this hardware?
Levy Fleets runs scooters, e-bikes, mopeds and golf carts on the IoT hardware you already have, with integration pages for 29 vendors including Segway, Okai, Omni, Queclink, Invers and Comodule. Keyless unlock, geofencing, rider payments and rider support are included, and the Managed plan is a share of revenue with no per-vehicle software fee.