---
title: "Setup Payment Method"
method: POST
path: "/billing/setup-payment-method"
tags: ["billing"]
---

# Setup Payment Method

`POST /billing/setup-payment-method`

Attach a payment method to the user's Stripe customer.

This endpoint:
1. Creates a Stripe customer if one doesn't exist
2. Attaches the payment method to the customer
3. Sets it as the default payment method
4. Checks card fingerprint for abuse (same card on too many teams)
5. Stores the fingerprint and marks team as card-verified

Rate-limited to 5 attempts per user per hour. Each attempt captures a
$1 PaymentIntent (real settled charge) for Stripe Radar fraud scoring,
so unbounded retries let a single bot account incinerate Pioneer's
Stripe fee budget and Radar reputation. The cap leaves plenty of
headroom for legitimate users to retry on declined cards or SCA hiccups
while denying bot farms unlimited attempts on the same compromised account.

Args:
    request: FastAPI request (required by SlowAPI rate limiter).
    body: Payment method ID from Stripe.js.
    auth: Authenticated user context.

Returns:
    Success status and customer ID.

## Request body

- SetupPaymentMethodRequest — Request model for setting up a payment method. On the initial attempt the frontend sends only ``payment_method_id`` and the backend creates a $1 pre-auth PaymentIntent to verify the card. If the issuer requires SCA the response carries ``requires_action=True`` and a ``client_secret`` / ``payment_intent_id`` pair. The frontend then drives the 3DS challenge via ``stripe.handleNextAction`` and re-calls this endpoint with BOTH ``payment_method_id`` AND ``payment_intent_id``; the backend retrieves and finalizes that existing PI (rather than creating a new one) so we read the post-3DS state instead of Stripe's idempotent replay of the original ``requires_action`` body.
  - `payment_method_id` string, required — Stripe payment method ID from Stripe.js
  - `payment_intent_id` string, nullable — Set only on the post-3DS retry to finalize the existing verification PaymentIntent. Must match the ``payment_intent_id`` returned in the previous ``requires_action`` response.

## Response `200`

Successful Response

- SetupPaymentMethodResponse — Response model for payment method setup. When the issuer requires SCA / 3DS on the verification PaymentIntent the response sets ``requires_action=True`` and ships ``client_secret`` so the frontend can complete the 3DS challenge via ``stripe.handleNextAction`` and re-invoke the same endpoint. The backend re-creates the PaymentIntent with the same idempotency key, Stripe returns the same (now-confirmed) intent, and we finalize.
  - `success` boolean, required
  - `message` string, required
  - `customer_id` string, nullable
  - `requires_action` boolean
  - `client_secret` string, nullable
  - `payment_intent_id` string, nullable

## Other responses

- `422` — Validation Error

---

[API](https://skmtc.net/pioneer/apis/brain-api.md) · [All operations](https://skmtc.net/pioneer/apis/brain-api/llms.txt) · [OpenAPI document](https://skmtc-service-staging.skmtc.workers.dev/v1/apis/pioneer/brain-api/revisions/31dfe831e079/schema)
