Stripe Payments — Frontend Integration Guide

Overview

Card payments use a two-step flow:

  1. Backend creates a PaymentIntent — returns a stripe_client_secret
  2. Frontend confirms the payment — uses the Stripe SDK with the stripe_client_secret to collect card details and process the charge

The backend handles Stripe communication server-side. The frontend only needs the publishable key and the stripe_client_secret from the create payment response.


Publishable Keys

Environment Key
Development pk_test_3XxXqkTDvgkh8MxL8OdliMYq00KuYHGdTu
Staging Contact backend team
Production Contact backend team

Never use the secret key (sk_*) on the frontend.


API Flow

1. Create a Payment

Request

POST /api/v1/rides/:ride_id/payments
Authorization: Bearer <jwt_token>
Idempotency-Key: <uuid>
Content-Type: application/json

{ "payment_method": "card" }

Response 201 Created

{
  "data": {
    "attributes": {
      "id": 1182,
      "ride_id": 2544,
      "stripe_payment_intent_id": "pi_3T80f1IShcAMEstE2sdDxrwL",
      "stripe_client_secret": "pi_3T80f1IShcAMEstE2sdDxrwL_secret_Qr9ybne3az7Cv8uHdxnUvziLT",
      "amount_cents": 2363,
      "amount_dollars": 23.63,
      "status": "requires_payment_method",
      "payment_method": "card"
    }
  }
}

If a payment already exists for the ride, 200 OK is returned with the existing payment. Check status before presenting the payment sheet — if already succeeded, skip to step 3.

Store id (payment ID) and stripe_client_secret from this response.


2. Confirm the Payment (Stripe SDK)

Use the stripe_client_secret to present the payment sheet via the Stripe Flutter SDK:

// Initialize Stripe once at app startup
Stripe.publishableKey = 'pk_test_3XxXqkTDvgkh8MxL8OdliMYq00KuYHGdTu';

// In the payment screen:
Future<void> confirmPayment(String clientSecret) async {
  await Stripe.instance.initPaymentSheet(
    paymentSheetParameters: SetupPaymentSheetParameters(
      paymentIntentClientSecret: clientSecret,
      merchantDisplayName: 'SafeRide',
    ),
  );

  await Stripe.instance.presentPaymentSheet();
  // If no exception is thrown, the payment was confirmed by Stripe
}

Do not pass the full stripe_client_secret to the backend confirm endpoint — the SDK handles confirmation directly with Stripe.


3. Notify the Backend

After the Stripe SDK confirms the payment, call the backend confirm endpoint to sync the payment status:

Request

POST /api/v1/payments/:payment_id/confirm
Authorization: Bearer <jwt_token>
Idempotency-Key: <uuid>
Content-Type: application/json

{ "payment_method": "<payment_method_id_from_stripe_sdk>" }

The payment_method is the Stripe payment method ID returned by the SDK after confirmation (e.g. pm_...). If unavailable from the SDK result, the backend will also sync via webhook automatically — see Real-Time Updates below.

Response 200 OK — returns the updated payment with status: "succeeded".


Payment Status Reference

Status Meaning
requires_payment_method PaymentIntent created, awaiting card details
requires_confirmation Card attached, awaiting confirmation
requires_action 3DS or other action required from the user
processing Stripe is processing the charge
succeeded Payment complete — ride can be completed
failed Charge failed — prompt user to retry
canceled Payment canceled or refunded

Ride Fields

The ride object includes two payment-related fields:

{
  "requires_payment": true,
  "payment_completed": false
}
  • requires_paymenttrue when the ride has payment_method: "card" and the fare is non-zero
  • payment_completedtrue when the associated payment has status: "succeeded"

The driver’s Complete Ride button must be disabled when requires_payment == true && payment_completed != true.


Real-Time Updates

The backend broadcasts a ride_updated ActionCable event when payment status changes. Subscribe to RideChannel and listen for payment_completed: true rather than polling:

// On the ride detail screen, listen for ActionCable ride_updated events
// The payload includes: payment_completed, requires_payment, status
// When payment_completed flips to true, re-enable the Complete Ride button
// and dismiss any payment-pending UI

If ActionCable is not yet integrated, poll GET /api/v1/rides/:id every 2–3 seconds for up to 30 seconds after the SDK confirms, stopping when payment_completed is true.


Error Handling

HTTP Status Error Action
402 insufficient_funds Show “Your card has insufficient funds”
402 payment_error Show “Payment failed, please try a different card”
402 requires_action Trigger 3DS flow via Stripe SDK
422 invalid_request Payment intent issue — create a new payment
503 stripe_error Stripe temporarily unavailable — retry later

Testing Without a Real Card

Use Stripe test payment method tokens in development. With the Stripe Flutter SDK in test mode, use test card numbers directly in the payment sheet:

Card Number Scenario
4242 4242 4242 4242 Success
4000 0000 0000 9995 Insufficient funds
4000 0025 0000 3155 3DS required
4000 0000 0000 0002 Generic decline

Use any future expiry, any 3-digit CVC, and any billing ZIP.

Full list: https://stripe.com/docs/testing#cards