Internal Documentation internal
TalkIDE internal documentation

Authenticated user registers a credit/debit card as their default payment method via Stripe SetupIntent flow. Requires a valid JWT access token.

  • FE uses Stripe.js v3 + Stripe Elements (CardElement) to collect card data. Raw card numbers never reach TalkIDE servers.
  • BE lazily creates a Stripe Customer for the user on the first billing action (if stripe_customer_id is null in users table). The Customer ID is stored back to users.stripe_customer_id.
  • Flow: BE creates SetupIntent → FE confirms with stripe.confirmCardSetup() → Stripe attaches PaymentMethod to Customer → BE sets it as invoice_settings.default_payment_method on the Customer.
  • The FE polls / listens for the setup_intent.succeeded webhook or — for simplicity in MVP — calls GET /api/v1/users/me/payment-method after confirmation to verify the card is attached.
  • Only one default payment method per user. Registering a new card replaces the previous default.
  • Stripe test mode — use test card 4242 4242 4242 4242, any future expiry, any 3-digit CVC.
  • BE Foundation (be#111) prerequisite: stripe_customer_id VARCHAR(255) NULL column in users table (migration 0021-add-stripe-customer-id-to-users.xml).
sequenceDiagram
    actor User

    User->>+FE: clicks "Change" in Billing → Payment method section

    FE->>FE: mount Stripe Elements CardElement
    FE->>-User: show card input form (Stripe Elements iframe)

    User->>+FE: fills card details and clicks "Save card"

    FE->>FE: validate that CardElement is complete (Stripe.js client-side)
    alt CardElement incomplete
        FE-->>User: show "Please complete your card details" error
    end

    FE->>+BE: POST /api/v1/users/me/billing/setup-intent <br> Authorization: Bearer {accessToken}

    BE->>BE: validate JWT access token
    alt access token invalid or missing
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>BE: resolve stripe_customer_id for user
    alt stripe_customer_id is null (first billing action)
        BE->>Stripe: stripe.customers.create({ email, metadata: { userId } })
        Stripe-->>BE: Customer { id: "cus_..." }
        BE->>DB: UPDATE users SET stripe_customer_id = "cus_..." WHERE id = userId
    end

    BE->>Stripe: stripe.setupIntents.create({ customer: "cus_...", payment_method_types: ["card"] })
    Stripe-->>BE: SetupIntent { client_secret: "seti_...secret_..." }

    BE->>-FE: 200 OK <br> SetupIntentResponse

    FE->>Stripe: stripe.confirmCardSetup(clientSecret, { payment_method: { card: CardElement } })
    Stripe-->>FE: { setupIntent: { status: "succeeded", payment_method: "pm_..." } }

    alt Stripe confirmation error
        Stripe-->>FE: { error: { message: "..." } }
        FE-->>User: show Stripe error message (e.g. "Your card was declined.")
    end

    FE->>+BE: POST /api/v1/users/me/billing/payment-method <br> Authorization: Bearer {accessToken} <br> AttachPaymentMethodRequest

    BE->>BE: validate JWT access token
    alt access token invalid or missing
        BE-->>FE: 401 Unauthorized <br> ErrorResponse
    end

    BE->>Stripe: stripe.customers.update(customerId, { invoice_settings: { default_payment_method: paymentMethodId } })
    Stripe-->>BE: Customer (updated)

    BE->>-FE: 200 OK <br> PaymentMethodResponse

    FE->>-User: show updated card (brand + last4 + expiry), dismiss form

Step 1 — Create SetupIntent

POST /api/v1/users/me/billing/setup-intent (no request body; authentication via JWT Bearer token)

200 OK SetupIntentResponse:

{
  "clientSecret": "seti_1PqXyz...secret_..."
}

401 Unauthorized ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

502 Bad Gateway (Stripe API unreachable) ErrorResponse:

{
  "status": 502,
  "code": "STRIPE_UNAVAILABLE",
  "message": "Payment provider is temporarily unavailable. Please try again."
}

Step 2 — Attach Payment Method as Default

POST /api/v1/users/me/billing/payment-method AttachPaymentMethodRequest:

{
  "paymentMethodId": "pm_1PqXyz..."
}

200 OK PaymentMethodResponse:

{
  "brand": "visa",
  "last4": "4242",
  "expMonth": 12,
  "expYear": 2028,
  "billingEmail": "jane@example.com"
}

400 Bad Request (validation) ErrorResponse:

{
  "status": 400,
  "code": "VALIDATION_ERROR",
  "message": "Validation failed",
  "errors": [
    { "field": "paymentMethodId", "message": "must not be blank" }
  ]
}

401 Unauthorized ErrorResponse:

{
  "status": 401,
  "code": "AUTHENTICATION_FAILED",
  "message": "Access token is missing or invalid"
}

422 Unprocessable Entity (PaymentMethod not attached to this Customer) ErrorResponse:

{
  "status": 422,
  "code": "STRIPE_PAYMENT_METHOD_INVALID",
  "message": "The provided payment method could not be attached to your account."
}

502 Bad Gateway (Stripe API unreachable) ErrorResponse:

{
  "status": 502,
  "code": "STRIPE_UNAVAILABLE",
  "message": "Payment provider is temporarily unavailable. Please try again."
}

Frontend

Validations

Field / ControlConstraintsNote
CardElementStripe.js complete === true before submitValidated client-side via Stripe.js; not sent to BE — raw card data never leaves the browser
Submit buttondisabled while loading === true or CardElement incompletePrevents double-submit

UX Guidelines

Trigger: “Change” button in BillingSection.vue → payment method row.

Flow:

  1. Clicking “Change” reveals inline card form with Stripe Elements CardElement (iframe).
  2. “Save card” button is disabled until CardElement.on('change') emits { complete: true }.
  3. On submit: loading = true, button disabled, spinner shown.
  4. Step 1 call to POST /setup-intent, then stripe.confirmCardSetup(clientSecret, ...).
  5. On Stripe confirmation error: display Stripe-provided error.message inline below CardElement.
  6. On success: Step 2 call to POST /payment-method. On 200: update UI card display, dismiss form.
  7. On any BE error: show toast (rose, 5 s, dismissible) with error message.

Card display after registration:

Payment method      Visa •••• 4242   Expires 12/28   [Change]

Brand displayed as icon + text; last4 + expiry from PaymentMethodResponse.

Backend

DB Migration

New migration file: 0021-add-stripe-customer-id-to-users.xml

<changeSet id="0021-add-stripe-customer-id-to-users" author="system">
    <addColumn tableName="users">
        <column name="stripe_customer_id" type="VARCHAR(255)">
            <constraints nullable="true"/>
        </column>
    </addColumn>
</changeSet>

Stripe Customer Lifecycle

StripeCustomerService (new Spring @Service):

  • ensureCustomer(userId): String — returns existing stripe_customer_id or creates new Stripe Customer and persists to DB. Idempotent.
  • Called from all billing endpoints that require a Customer ID.

Validations

FieldConstraintsNote
JWT Authorization headernot_blank, valid signature, not expired401 if missing/invalid
paymentMethodId (Step 2)not_blank, matches pattern ^pm_[a-zA-Z0-9_]+$400 VALIDATION_ERROR if invalid format

Test Cases

GIVENWHENTHEN
Authenticated user with no existing stripe_customer_idPOST /setup-intent is calledNew Stripe Customer created in test mode; stripe_customer_id persisted to users table; 200 OK with valid clientSecret returned
Authenticated user with existing stripe_customer_idPOST /setup-intent is calledExisting Customer reused; no duplicate Customer created; 200 OK with clientSecret returned
No Authorization headerPOST /setup-intent is called401 AUTHENTICATION_FAILED returned
Stripe API returns error during Customer creationPOST /setup-intent is called502 STRIPE_UNAVAILABLE returned; no stripe_customer_id persisted
Authenticated user, valid paymentMethodId “pm_card_visa” (test)POST /payment-method is calledStripe Customer updated with new default payment method; 200 OK with brand, last4, expiry returned
paymentMethodId is blankPOST /payment-method is called400 VALIDATION_ERROR returned
paymentMethodId does not belong to this CustomerPOST /payment-method is called422 STRIPE_PAYMENT_METHOD_INVALID returned
No Authorization headerPOST /payment-method is called401 AUTHENTICATION_FAILED returned
Stripe API unreachable during attachPOST /payment-method is called502 STRIPE_UNAVAILABLE returned
User registers second card (replacing first)POST /payment-method is called with new pm_idNew card set as default; old payment method detached from Customer; 200 OK with new card details

Was this page helpful?

Thanks for the feedback.