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.