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.

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.