Migrating from Stripe

This guide is for teams moving an existing Stripe integration to Flint without rebuilding every payment flow at once.

The main shift is structural:

  • Stripe integrations often start from PaymentIntent, Checkout Session, Invoice, or Subscription.
  • Flint starts from the business object that should stay authoritative: order, invoice, payment_link, or subscription.

Flint still uses Stripe.js and Stripe Elements for card entry in embedded and saved-card flows. The migration is mostly about changing your backend resources, hosted entry points, and webhook handling.

Stripe to Flint Object Map#

StripeFlint
CustomerCustomer
SetupIntentPOST /v1/payment-methods
PaymentMethodPayment Method
PaymentIntentPayment Intent
Checkout SessionCheckout Session
Payment LinkPayment Link
Product + recurring PriceSubscription Plan
SubscriptionSubscription
InvoiceInvoice
RefundRefund
Webhook endpointWebhook endpoint

Pick the Right Flint Primitive#

Use:

  • checkout-sessions when your app creates one hosted payment flow for one buyer or one order
  • payment-links when you want one reusable public URL for many buyers
  • payment-intents when you keep checkout on your own site with Stripe Elements
  • subscription-plans plus subscriptions when you own recurring billing in your app
  • invoices when the problem is accounts receivable, due dates, reminders, PDFs, or manual payment recording

1. Migrate Stripe Billing Subscriptions#

Common Stripe shape:

  • Product
  • recurring Price
  • Customer
  • PaymentMethod
  • Subscription

Flint equivalent:

  • Subscription Plan
  • Customer
  • Payment Method
  • Subscription

If your current Stripe flow creates subscriptions directly from your backend for known customers, the Flint migration is:

  1. Create or map the Flint customer.
  2. Save a payment method for that customer.
  3. Confirm card setup on the frontend and wait for setup completion.
  4. Create a subscription plan.
  5. Create the subscription.

The payment_method_id returned by POST /v1/payment-methods starts in pending status. It is not ready for subscription billing until frontend setup confirmation completes and Flint processes Stripe's webhook, which promotes it to active.

If you set that finished payment method as the customer's default, later POST /v1/subscriptions calls can omit payment_method_id and Flint will use the customer's default payment method instead.

Create the Plan#

Bash
curl -X POST https://api.withflintpay.com/v1/subscription-plans \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: create-plan-pro-monthly-v1" \
  -d '{
    "name": "Pro Monthly",
    "billing_interval": "monthly",
    "billing_interval_count": 1,
    "currency": "USD",
    "line_items": [
      {
        "name": "Pro Plan",
        "unit_price_money": { "amount": 2999, "currency": "USD" },
        "quantity": 1
      }
    ],
    "trial_period_days": 14
  }'

Create the Subscription#

Bash
curl -X POST https://api.withflintpay.com/v1/subscriptions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: subscription-cus-plan-001" \
  -d '{
    "plan_id": "plan_xxx",
    "customer_id": "cus_xxx",
    "payment_method_id": "pm_xxx"
  }'

If your current Stripe flow uses hosted subscription signup, do not create subscriptions directly from your frontend. Use one of these Flint hosted entry points instead:

  • POST /v1/checkout-sessions with plan_id for app-driven hosted signup
  • POST /v1/payment-links with plan_id for a reusable public signup URL

2. Migrate Stripe One-Time Hosted Checkout#

Common Stripe shape:

  • create a Checkout Session with mode=payment
  • redirect the buyer to the hosted Stripe page

Flint equivalent:

  • create a Checkout Session with quick_pay or order_id
  • redirect the buyer to data.url

Use quick_pay when you just need a one-time amount and Flint can create the backing order for you:

Bash
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: checkout-session-consulting-001" \
  -d '{
    "quick_pay": {
      "name": "Consulting Session",
      "amount_money": { "amount": 15000, "currency": "USD" }
    },
    "customer": {
      "require_email": true
    },
    "redirects": {
      "success_redirect_url": "https://example.com/success",
      "cancel_redirect_url": "https://example.com/cancel"
    }
  }'

Use order_id when your app already knows the cart or order and you want that order to stay authoritative through payment, refunds, and reporting:

Bash
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: checkout-session-order-001" \
  -d '{
    "order_id": "ord_xxx",
    "customer": {
      "customer_id": "cus_xxx",
      "require_email": true
    },
    "redirects": {
      "success_redirect_url": "https://example.com/orders/success"
    }
  }'

If you currently reuse one Stripe-hosted URL across many buyers, that is not a checkout-session migration. That is a payment-link migration.

Common Stripe shape:

  • one reusable public URL
  • shared from a site, campaign, QR code, or sales email

Flint equivalent:

  • POST /v1/payment-links

Example one-time payment link:

Bash
curl -X POST https://api.withflintpay.com/v1/payment-links \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: payment-link-drop-001" \
  -d '{
    "name": "Spring merch drop",
    "description": "Limited release t-shirt",
    "line_items": [
      {
        "key": "shirt",
        "name": "Limited Tee",
        "quantity": 1,
        "amount_money": { "amount": 3500, "currency": "USD" },
        "allow_quantity_adjustment": true,
        "min_quantity": 1,
        "max_quantity": 5
      }
    ],
    "customer": {
      "require_email": true
    },
    "redirects": {
      "success_redirect_url": "https://example.com/orders/success"
    }
  }'

For recurring signup pages, use the same endpoint but send plan_id instead of line_items.

Flint payment links also support popular hosted flows beyond standard one-time payments:

  • recurring signup with plan_id
  • donation pages with mode: "donation"
  • event or ticketing pages with mode: "event"

4. Migrate Stripe PaymentIntents plus Elements#

Common Stripe shape:

  • create a PaymentIntent
  • mount Stripe Elements or the Payment Element
  • confirm payment on the frontend

Flint equivalent:

  1. Create an order.
  2. Create a Flint payment intent.
  3. Mount Stripe Elements with the returned Flint-managed client_secret.
  4. Confirm payment with Stripe.js on the frontend.
  5. Verify the final order state from your backend.

Create the Flint Payment Intent#

Bash
curl -X POST https://api.withflintpay.com/v1/payment-intents \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: pi-order-001" \
  -d '{
    "order_id": "ord_xxx"
  }'

The response includes:

  • data.payment_intent.payment_intent_id
  • data.client_confirmation.stripe.client_secret
  • data.client_confirmation.stripe.account_id

That means your frontend can still use Stripe.js:

JavaScript
const stripe = Stripe("pk_test_xxx", {
  stripeAccount: accountIdFromBackend,
});

const elements = stripe.elements({
  clientSecret: clientSecretFromBackend,
});

const paymentElement = elements.create("payment");
paymentElement.mount("#payment-element");

Confirm payment on the frontend:

JavaScript
const { error } = await stripe.confirmPayment({
  elements,
  confirmParams: {
    return_url: "https://example.com/orders/success",
  },
});

Then verify the final state from your backend:

Bash
curl -X GET https://api.withflintpay.com/v1/orders/ord_xxx \
  -H "Authorization: Bearer YOUR_API_KEY"

Use this migration when you want to keep checkout embedded on your own domain instead of moving to Flint-hosted checkout.

5. Migrate Stripe Invoicing#

Common Stripe shape:

  • create draft invoice
  • finalize and send invoice
  • collect payment from hosted invoice page

Flint equivalent:

  • create invoice draft with POST /v1/invoices
  • send with POST /v1/invoices/{invoice_id}/send
  • collect with POST /v1/invoices/{invoice_id}/checkout-session
  • optionally record offline payments with POST /v1/invoices/{invoice_id}/manual-payments after shutting down any active online collection path for the invoice

Example draft invoice:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: inv-order-001" \
  -d '{
    "order_id": "ord_xxx",
    "recipient_email": "billing@example.com",
    "due_at": "2026-04-15T00:00:00Z",
    "reference": "PO-1048"
  }'

Send it:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_xxx/send \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: send-inv-001" \
  -d '{}'

Create or reuse the invoice-owned hosted checkout flow:

Bash
curl -X POST https://api.withflintpay.com/v1/invoices/inv_xxx/checkout-session \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -d '{}'

Use Flint invoices when the job is receivables management, not just immediate checkout.

6. Migrate Saved Cards and Future Use Flows#

Common Stripe shape:

  • create a SetupIntent
  • confirm card setup with Stripe.js
  • reuse the saved payment method later

Flint equivalent:

  • POST /v1/payment-methods
  • confirm with Stripe.js using the returned client_secret and account_id
  • use the resulting Flint payment_method_id for subscriptions or payment intents

Create the Flint payment method setup:

Bash
curl -X POST https://api.withflintpay.com/v1/payment-methods \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: save-pm-customer-001" \
  -d '{
    "customer_id": "cus_xxx"
  }'

The response includes:

  • data.payment_method.payment_method_id
  • data.client_setup.stripe.client_secret
  • data.client_setup.stripe.account_id

Create Stripe.js with the returned connected account before confirming setup:

JavaScript
const stripe = Stripe("pk_test_xxx", {
  stripeAccount: accountIdFromBackend,
});

If you are using a card element flow, confirm setup like this:

JavaScript
await stripe.confirmCardSetup(clientSecretFromBackend, {
  payment_method: {
    card: cardElementFromYourUI,
  },
});

If your frontend uses the newer elements-based setup flow, use stripe.confirmSetup(...) instead.

After the webhook confirms the setup, reuse the returned Flint payment_method_id in:

  • POST /v1/subscriptions
  • POST /v1/payment-intents
  • POST /v1/payment-methods/{payment_method_id}/set-default

7. Migrate Refunds#

Common Stripe shape:

  • refund a charge or payment intent

Flint equivalent:

  • refund the order_id or payment_intent_id

Example:

Bash
curl -X POST https://api.withflintpay.com/v1/refunds \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: refund-order-001" \
  -d '{
    "order_id": "ord_xxx",
    "reason": "requested_by_customer",
    "reason_message": "Customer canceled before fulfillment."
  }'

Flint refunds stay attached to the commerce object that matters downstream instead of making your system reconcile a separate processor object graph.

8. Migrate Webhooks#

Common Stripe shape:

  • register a webhook endpoint
  • verify Stripe-Signature
  • deduplicate on the Stripe event ID

Flint equivalent:

  • register a Flint webhook endpoint
  • verify webhook-signature
  • deduplicate on webhook-id

Register the endpoint:

Bash
curl -X POST https://api.withflintpay.com/v1/webhook-endpoints \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: webhook-prod-001" \
  -d '{
    "url": "https://example.com/webhooks/flint",
    "enabled_events": [
      "payment_intent.succeeded",
      "order.payment_succeeded",
      "subscription.payment_succeeded",
      "invoice.paid",
      "refund.created"
    ]
  }'

Store data.secret immediately. Flint only returns it at creation time and when you rotate the secret.

Migration Checklist#

  • Create Flint customers for every active Stripe customer you still need to charge.
  • Store your old Stripe IDs in Flint metadata or merchant_customer_id for reconciliation during cutover.
  • Recreate recurring prices as Flint subscription plans.
  • Replace Stripe-hosted payment entry points with Flint checkout sessions or payment links.
  • Replace direct Stripe invoice flows with Flint invoices if you need due dates, reminders, PDFs, or manual payment recording.
  • Keep Stripe.js on the frontend for embedded payments and saved-card flows, but switch backend intent and setup creation to Flint.
  • Replace Stripe webhook verification and event routing with Flint webhook verification and Flint event names.
  • Use Flint idempotency keys on the write paths that support them during the migration window, and treat the Idempotency guide as the source of truth for the current route list.
Rate this doc