intermediate
shop-rentals
payment-methods
stripe

Adding a Card to a Customer's Profile

Operator-driven SetupIntent flow for attaching a payment method to a customer at the counter — used for deposit holds and refundable charges

Levy Fleets TeamMay 7, 20265 min read

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_methods table
  • 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:

CardBehavior
4242 4242 4242 4242Succeeds
4000 0027 6000 3184Requires 3DS authentication
4000 0000 0000 9995Declines — insufficient funds

Any future expiry, any CVC.

Error cases

ErrorCauseFix
"Customer not found"Bad customer IDCheck the URL parameter
"Payment method belongs to a different Stripe customer"The PM was created for a different customerRe-create the SetupIntent for this customer
"paymentMethodId is required for attach"Missing parameter on attach callFrontend bug — make sure step 2's PM ID is passed to step 3
Stripe API errors (rate limit, etc.)TransientRetry 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.