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.

That is the model behind a payments API with orders, tax, and refunds built in.

Note:

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#

cURL
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#

cURL
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 '{
    "subscription_plan_id": "plan_xxx",
    "customer_id": "cus_xxx",
    "payment_method_id": "pm_xxx",
    "billing_start": { "type": "immediate" }
  }'

Move a subscriber Stripe already billed#

A new signup starts with "billing_start": {"type": "immediate"}. A subscriber Stripe charged for the period they are still in should not be charged again, so import the running period instead. Send period_started_at as the current_period_start from the Stripe subscription:

cURL
curl -X POST https://api.withflintpay.com/v1/subscriptions \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: import-cus-plan-001" \
  -d '{
    "subscription_plan_id": "plan_xxx",
    "customer_id": "cus_xxx",
    "payment_method_id": "pm_xxx",
    "billing_start": {
      "type": "imported",
      "period_started_at": "2026-07-01T00:00:00Z"
    }
  }'

Flint charges nothing, computes the period end from the plan interval, and takes over at the next renewal. Cancel the Stripe subscription once the Flint one exists, and import the period before it ends: a period that has already elapsed fails with SUBSCRIPTION_IMPORT_PERIOD_NOT_CURRENT. If you want to keep driving renewal dates from your own system during the cutover, add "billing_schedule": {"owner": "external"} and supply each date with the billing-schedule endpoint.

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 subscription_plan_id for app-driven hosted signup
  • POST /v1/payment-links with subscription_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_item or order_id
  • redirect the buyer to data.checkout_session.url

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

cURL
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_item": {
      "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 has an order and you want that order to stay authoritative through payment, refunds, and reporting:

cURL
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"
    }
  }'
Note:

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:

cURL
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 subscription_plan_id instead of line_items.

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

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

If you collected tips through a custom step before Stripe Checkout, or as a "Tip" line item, move them to the tip section on the payment link or checkout session. The hosted page shows the tip prompt and the order records the tip separately from the items. See Tips & fees.

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. Read payment_collection guidance from the order.
  3. Mount Stripe Elements without a PaymentIntent client secret.
  4. Create a one-time payment source with Stripe.js.
  5. Submit the source through Flint's order payment route.
  6. Verify the final order state from your backend.

Read Flint collection guidance#

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

The response includes:

  • data.payment_collection.stripe.account_id
  • data.payment_collection.stripe.publishable_key
  • data.payment_collection.stripe.elements

Mount Elements from that guidance:

JavaScript
const stripe = Stripe(paymentCollection.publishable_key, {
  stripeAccount: paymentCollection.account_id,
});

const guidance = paymentCollection.elements;
const elements = stripe.elements({
  mode: guidance.mode,
  amount: guidance.amount_money.amount,
  currency: guidance.amount_money.currency.toLowerCase(),
  paymentMethodCreation: guidance.payment_method_creation,
  paymentMethodTypes: guidance.payment_method_types,
});

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

Create a one-time source on the frontend:

JavaScript
const {error: submitError} = await elements.submit();
if (submitError) throw submitError;

const {error, paymentMethod} = await stripe.createPaymentMethod({
  elements,
});
if (error) throw error;

Submit that token from your backend. The expected value is the amount the buyer approved from payment_collection.stripe.elements.amount_money:

cURL
curl -X POST https://api.withflintpay.com/v1/orders/ord_xxx/pay \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: pay-order-001" \
  -d '{
    "action": "pay",
    "payment_source": {"token": "pm_xxx"},
    "expected_outstanding_money": {"amount": 2500, "currency": "USD"}
  }'

Flint creates and confirms the full-balance order payment leg in that call. Do not call stripe.confirmPayment for an order-owned payment. If payment_attempt.is_resumable is true, run the returned client_action and resume with order_payment_attempt_id only.

Then verify the final state from your backend:

cURL
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
  • issue with POST /v1/invoices/{invoice_id}/issue
  • 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:

cURL
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",
    "collection": {"mode": "buyer_initiated"},
    "payment_due": {"type": "absolute", "due_at": "2026-04-15T00:00:00Z"},
    "recipient_email": "billing@example.com",
    "reference": "PO-1048"
  }'

Issue it:

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

Create or reuse the invoice-owned hosted checkout flow:

cURL
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:

cURL
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.setup_intent.stripe_js_call (confirm_setup)
  • data.client_setup.stripe.setup_intent.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:

cURL
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.

If your Stripe integration computes an item's share of the discount and tax before each partial refund, send line_items with the order_line_item_id and a quantity and leave the amount out. Flint computes the amount and reverses the item's tax. Stripe partial refund: discount and tax reversal walks through one return on both APIs, and Refunds has the full request.

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:

cURL
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.paid",
      "subscription.payment_succeeded",
      "invoice.paid",
      "refund.updated"
    ]
  }'

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

If your Stripe handler fulfills from checkout.session.completed or payment_intent.succeeded, move that job to order.paid. Which Stripe webhook event should you trust? maps each Stripe event to the job it fits, and Choosing the right event covers the Flint side.

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.

Was this helpful?