## Documentation Index

Fetch the complete documentation index at: [/llms.txt](https://docs.withflex.com/llms.txt)

Use this file to discover all available pages before exploring further.

This guide provides step-by-step instructions for implementing Flex as a Stripe custom payment method, with complete code examples based on a working implementation.

## Overview

This implementation enables a dual-payment system where:

- **Stripe** handles traditional payment methods (cards, Apple Pay, etc.) with embedded processing
- **Flex** handles HSA/FSA payments with a hosted checkout redirect flow

Both payment options appear in a single Stripe Payment Element, providing a unified customer experience.

## Implementation Steps

1. **Create Server Actions**  
   Server actions handle payment processing on the backend, ensuring API keys remain secure.

2. **Build the Checkout Form Component**  
   The checkout form integrates the Stripe Payment Element and handles payment routing.

3. **Create Success Page**  
   Display order confirmation after successful payment.

4. **Configure Validation Schemas**  
   Set up form validation using Zod for type safety and error handling.

## 1. Server Actions

### Stripe Payment Intent Creation

Create a server action to generate Stripe PaymentIntents:

```typescript
"use server";

import { stripe } from "@/lib/stripe";
import { getPlanDetails } from "@/lib/utils";
import { formatAmountToCents } from "@/lib/utils";
import { CURRENCY } from "@/lib/constants";
import type Stripe from "stripe";

export async function createPaymentIntent(
  plan: string
): Promise<{ client_secret: string }> {
  const planDetails = await getPlanDetails(plan);

const paymentIntent: Stripe.PaymentIntent = await stripe.paymentIntents.create({
    amount: formatAmountToCents(Number(planDetails.price), CURRENCY),
    automatic_payment_methods: {
      enabled: true
    },
    currency: CURRENCY,
  });

return {
    client_secret: paymentIntent.client_secret as string
  };
}
```

The `automatic_payment_methods` setting allows Stripe to automatically display all enabled payment methods, including cards, digital wallets, and bank transfers.

### Flex Checkout Session Creation

Create a server action to generate Flex checkout sessions:

```typescript
"use server";

import { redirect } from "next/navigation";
import { v4 as uuidv4 } from "uuid";
import { getPlanDetails } from "@/lib/utils";

const FLEX_BASE_URL = "https://api.withflex.com";

export async function createCheckoutSession({
  firstName,
  lastName,
  email,
  plan,
}: {
  firstName: string;
  lastName: string;
  email: string;
  plan: string;
}): Promise<void | { error: Error }> {
  try {
    const planDetails = await getPlanDetails(plan);
    const orderId = uuidv4();
    const baseUrl = process.env.NEXT_PUBLIC_BASE_URL || "http://localhost:3000";

const body = {
      line_items: [
        {
          price: planDetails.flex_price_id,
          quantity: 1,
        }
      ],
      mode: "payment",
      success_url: `${baseUrl}/success?client-reference-id=${orderId}&plan=${plan}&price=${planDetails.price}`,
      cancel_url: baseUrl,
      client_reference_id: orderId,
      defaults: {
        first_name: firstName,
        last_name: lastName,
        email: email,
      },
    };

const res = await fetch(`${FLEX_BASE_URL}/v1/checkout/sessions`, {
      method: "POST",
      body: JSON.stringify({ checkout_session: body }),
      headers: {
        "Content-Type": "application/json",
        Authorization: `Bearer ${process.env.FLEX_SECRET_KEY}`,
      },
    });

if (!res.ok) {
      throw new Error(`Failed to create Flex checkout session: ${res.statusText}`);
    }

const { checkout_session } = await res.json();
    redirect(checkout_session.redirect_url);
  } catch (error) {
    console.error("Error creating Flex checkout session:", error);
    return { error: error as Error };
  }
}
```

The Flex integration uses a **hosted checkout** model, meaning customers are redirected to Flex’s domain to complete payment. Ensure your `success_url` and `cancel_url` are properly configured.

## 2. Checkout Form Component

The checkout form is the heart of the integration, rendering the Payment Element and routing payments appropriately.

### Form Wrapper Component

```typescript
"use client";

import { Elements } from "@stripe/react-stripe-js";
import getStripe from "@/lib/get-stripejs";
import { CURRENCY, FLEX_CUSTOM_PAYMENT_METHOD_ID } from "@/lib/constants";
import CheckoutForm from "./checkout-form-inner";

export default function ElementsForm({ plan, price }: { plan: string; price: string }) {
  return (
    <Elements
      stripe={getStripe()}
      options={{
        appearance: {
          variables: {
            colorIcon: "#6772e5",
            fontFamily: "Roboto, Open Sans, Segoe UI, sans-serif",
          },
        },
        customPaymentMethods: [
          {
            id: FLEX_CUSTOM_PAYMENT_METHOD_ID,
            options: {
              type: "static",
            },
          },
        ],
        currency: CURRENCY,
        mode: "payment",
        amount: Number(price),
      }}
    >
      <CheckoutForm plan={plan} price={price} />
    </Elements>
  );
}
```

The `customPaymentMethods` configuration registers Flex as a payment option. The `type: "static"` setting displays Flex as a selectable option without embedded interactive elements.

### Main Checkout Form

```typescript
"use client";

import { useState } from "react";
import { useStripe, useElements, PaymentElement } from "@stripe/react-stripe-js";
import { useForm } from "react-hook-form";
import { zodResolver } from "@hookform/resolvers/zod";
import { z } from "zod";
import { createPaymentIntent } from "@/app/actions/stripe";
import { createCheckoutSession } from "@/app/actions/flex";
import { FLEX_CUSTOM_PAYMENT_METHOD_ID } from "@/lib/constants";
import { checkoutFormSchema } from "@/lib/schemas";

export default function CheckoutForm({
  plan,
  price
}: {
  plan: string;
  price: string;
}) {
  const stripe = useStripe();
  const elements = useElements();
  const [errorMessage, setErrorMessage] = useState<string>();
  const [isProcessing, setIsProcessing] = useState(false);

const form = useForm<z.infer<typeof checkoutFormSchema>>({
    resolver: zodResolver(checkoutFormSchema),
    defaultValues: {
      firstName: "",
      lastName: "",
      email: "",
    },
  });

async function onSubmit(values: z.infer<typeof checkoutFormSchema>) {
    if (!stripe || !elements) {
      return;
    }

setIsProcessing(true);
    setErrorMessage(undefined);

try {
      const { error: submitError, selectedPaymentMethod } = await elements.submit();

if (submitError) {
        setErrorMessage(submitError.message);
        setIsProcessing(false);
        return;
      }

if (selectedPaymentMethod === FLEX_CUSTOM_PAYMENT_METHOD_ID) {
        const result = await createCheckoutSession({
          firstName: values.firstName,
          lastName: values.lastName,
          email: values.email,
          plan,
        });

if (result?.error) {
          setErrorMessage(result.error.message);
          setIsProcessing(false);
        }
      } else {
        const { client_secret: clientSecret } = await createPaymentIntent(plan);

const { error: confirmError } = await stripe.confirmPayment({
          elements,
          clientSecret,
          confirmParams: {
            return_url: `${window.location.origin}/success`,
            payment_method_data: {
              billing_details: {
                name: `${values.firstName} ${values.lastName}`,
                email: values.email,
              },
            },
          },
        });

if (confirmError) {
          setErrorMessage(confirmError.message);
          setIsProcessing(false);
        }
      }
    } catch (error) {
      setErrorMessage("An unexpected error occurred");
      setIsProcessing(false);
    }
  }

return (
    <form onSubmit={form.handleSubmit(onSubmit)} className="space-y-6">
      <div className="grid gap-4">
        <div>
          <label htmlFor="firstName" className="block text-sm font-medium mb-2">
            First Name
          </label>
          <input
            id="firstName"
            {...form.register("firstName")}
            className="w-full px-3 py-2 border rounded-md"
          />
          {form.formState.errors.firstName && (
            <p className="text-red-600 text-sm mt-1">
              {form.formState.errors.firstName.message}
            </p>
          )}
        </div>

<div>
          <label htmlFor="lastName" className="block text-sm font-medium mb-2">
            Last Name
          </label>
          <input
            id="lastName"
            {...form.register("lastName")}
            className="w-full px-3 py-2 border rounded-md"
          />
          {form.formState.errors.lastName && (
            <p className="text-red-600 text-sm mt-1">
              {form.formState.errors.lastName.message}
            </p>
          )}
        </div>

<div>
          <label htmlFor="email" className="block text-sm font-medium mb-2">
            Email
          </label>
          <input
            id="email"
            type="email"
            {...form.register("email")}
            className="w-full px-3 py-2 border rounded-md"
          />
          {form.formState.errors.email && (
            <p className="text-red-600 text-sm mt-1">
              {form.formState.errors.email.message}
            </p>
          )}
        </div>
      </div>

<div>
        <PaymentElement />
      </div>

{errorMessage && (
        <div className="bg-red-50 border border-red-200 text-red-800 px-4 py-3 rounded">
          {errorMessage}
        </div>
      )}

<button
        type="submit"
        disabled={!stripe || isProcessing}
        className="w-full bg-blue-600 text-white py-3 rounded-md hover:bg-blue-700 disabled:bg-gray-300 disabled:cursor-not-allowed"
      >
        {isProcessing ? "Processing..." : "Pay Now"}
      </button>
    </form>
  );
}
```

### Key Implementation Details

**Payment Method Detection**:

```typescript
const { selectedPaymentMethod } = await elements.submit();
```

The `elements.submit()` method returns the selected payment method ID, allowing you to route to the appropriate processor.**Conditional Routing**:

```typescript
if (selectedPaymentMethod === FLEX_CUSTOM_PAYMENT_METHOD_ID) {
  // Create Flex checkout session and redirect
} else {
  // Create Stripe payment intent and confirm inline
}
```

This pattern keeps the payment logic clean and maintainable.

## 3. Validation Schemas

Use Zod for type-safe form validation:

```typescript
import { z } from "zod";

export const planSchema = z.enum(["monthly", "annual"]);

export const checkoutFormSchema = z.object({
  firstName: z.string().min(1, "First name is required"),
  lastName: z.string().min(1, "Last name is required"),
  email: z.string().email("Please enter a valid email address"),
});

export const flexCustomerDefaultSchema = checkoutFormSchema.extend({
  plan: planSchema,
});

export type CheckoutFormData = z.infer<typeof checkoutFormSchema>;
export type FlexCustomerDefaults = z.infer<typeof flexCustomerDefaultSchema>;
```

## 4. Utility Functions

### Amount Formatting

Convert amounts to cents for Stripe:

```typescript
export function formatAmountToCents(amount: number, currency: string): number {
  const numberFormat = new Intl.NumberFormat(["en-US"], {
    style: "currency",
    currency: currency,
    currencyDisplay: "symbol",
  });

const parts = numberFormat.formatToParts(amount);
  let zeroDecimalCurrency = true;

for (const part of parts) {
    if (part.type === "decimal") {
      zeroDecimalCurrency = false;
    }
  }

return zeroDecimalCurrency ? amount : Math.round(amount * 100);
}
```

### Plan Details Retrieval

Fetch plan information from your data source:

```typescript
import data from "@/data.json";

export async function getPlanDetails(plan: string) {
  const planDetails = data.plans.find((p) => p.name === plan);

if (!planDetails) {
    throw new Error(`Plan "${plan}" not found`);
  }

return planDetails;
}
```

In production, replace this with a database query or API call to fetch plan details dynamically.

## 5. Success Page

Display order confirmation after payment:

```typescript
import { Suspense } from "react";

export default function SuccessPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) {
  const orderId = searchParams["client-reference-id"];
  const plan = searchParams["plan"];
  const price = searchParams["price"];

return (
    <div className="min-h-screen flex items-center justify-center bg-gray-50">
      <div className="max-w-md w-full bg-white rounded-lg shadow-lg p-8">
        <div className="text-center">
          <div className="mb-4">
            <svg
              className="mx-auto h-12 w-12 text-green-600"
              fill="none"
              stroke="currentColor"
              viewBox="0 0 24 24"
            >
              <path
                strokeLinecap="round"
                strokeLinejoin="round"
                strokeWidth={2}
                d="M5 13l4 4L19 7"
              />
            </svg>
          </div>

<h1 className="text-2xl font-bold text-gray-900 mb-2">
            Payment Successful!
          </h1>

<p className="text-gray-600 mb-6">
            Thank you for your purchase. Your order has been confirmed.
          </p>

<div className="bg-gray-50 rounded-md p-4 mb-6">
            <dl className="space-y-2 text-sm">
              <div className="flex justify-between">
                <dt className="text-gray-600">Order ID:</dt>
                <dd className="font-medium text-gray-900">{orderId}</dd>
              </div>
              <div className="flex justify-between">
                <dt className="text-gray-600">Plan:</dt>
                <dd className="font-medium text-gray-900 capitalize">{plan}</dd>
              </div>
              <div className="flex justify-between">
                <dt className="text-gray-600">Amount:</dt>
                <dd className="font-medium text-gray-900">
                  ${Number(price) / 100}
                </dd>
              </div>
            </dl>
          </div>

<p className="text-sm text-gray-500">
            A confirmation email has been sent to your email address.
          </p>
        </div>
      </div>
    </div>
  );
}
```

**Security Note**: The success page currently displays order details from URL parameters. In production, you should verify the payment status by querying the appropriate payment provider’s API before displaying the confirmation.

```typescript
// TODO: Verify payment status
const isValid = await verifyPaymentStatus(orderId);
if (!isValid) {
  redirect("/checkout");
}
```

## 6. Checkout Page Integration

Create your checkout page that uses the form:

```typescript
import { redirect } from "next/navigation";
import { z } from "zod";
import ElementsForm from "@/components/checkout-form";

const CheckoutQueryParamsSchema = z.object({
  plan: z.enum(["monthly", "annual"]),
  price: z.string(),
});

export default function CheckoutPage({
  searchParams,
}: {
  searchParams: { [key: string]: string | string[] | undefined };
}) {
  const result = CheckoutQueryParamsSchema.safeParse(searchParams);

if (!result.success) {
    redirect("/");
  }

const { plan, price } = result.data;

return (
    <div className="min-h-screen bg-gray-50 py-12">
      <div className="max-w-7xl mx-auto px-4 sm:px-6 lg:px-8">
        <div className="max-w-3xl mx-auto">
          <h1 className="text-3xl font-bold text-gray-900 mb-8">
            Complete Your Purchase
          </h1>

<div className="bg-white rounded-lg shadow p-6">
            <ElementsForm plan={plan} price={price} />
          </div>
        </div>
      </div>
    </div>
  );
}
```

## Additional Resources

- [Stripe Custom Payment Methods Documentation](https://docs.stripe.com/payments/payment-element/custom-payment-methods)
- [Stripe Payment Element Documentation](https://stripe.com/docs/payments/payment-element)
- [Flex API Documentation](https://docs.withflex.com/api-reference)
- [Flex Checkout Sessions](https://docs.withflex.com/api-reference/checkout-sessions)

## Support

If you encounter issues:

1. Review this documentation thoroughly.
2. Check Stripe and Flex Dashboard logs.
3. Review browser console for errors.
4. Contact Flex support with specific error messages and request IDs.
