Headless Shopify · public alpha
A headless checkout for the storefront you built on Shopify.
Shopify's Cart API ends at checkoutUrl, and the mutations that completed a checkout were shut off on April 1, 2025. Keep the storefront you built. On Flint the cart is an open order, Stripe Elements collects the card on your own page, and your backend completes the purchase with one call. Tax, 3D Secure, wallets, and the paid order come back from the same API.
3.79% + 35¢ on cards · no monthly fee · free sandbox · no credit card
import { Client } from "@flintpay/node";
const flint = new Client({
baseUrl: "https://api.withflintpay.com",
apiKey: process.env.FLINT_API_KEY!,
});
// 1. The cart is an open order. Variants in, no prices and no totals.
const order = await flint.orders.create(
{
line_items: cartLines, // [{ variant_id, quantity }]
delivery_destination: { address: shippingAddress },
tax: {
enabled: true,
calculation: { mode: "automatic", price_mode: "additive" },
},
},
{ idempotencyKey: `order-${cartId}` },
);
// 2. What Stripe Elements needs to mount on your page. No secrets in it.
const { payment_collection } = await flint.orders.get(order.order_id);
// 3. The browser posts back a single-use credential. Charge the order.
const result = await flint.orders.pay(
{
order_id: order.order_id,
body: {
action: "pay",
payment_source: { token: paymentMethodId },
expected_outstanding_money: order.settlement_amounts.outstanding_money,
buyer_contact: { email },
},
},
{ idempotencyKey: `pay-${cartId}-1` },
);
console.log(result.order.payment_status); // "paid"The whole backend half of checkout, from a Hydrogen loader, a Next.js route handler, or any Node server. You never send a total: Flint prices the variants, adds the tax, and charges what the order still owes.
your domain · your markup · Stripe Elements · no plan tier, no partner program
01Where it stops
Every checkout mutation moved to the cart, except the payment.
The Checkout APIs were shut off on April 1, 2025, and Shopify published a mapping from each removed mutation to its Cart API replacement. Five rows have one. The last row is the payment, and on Flint it is a call your backend makes.
| Removed 2025-04-01 | Cart API replacement | On Flint |
|---|---|---|
| checkoutCreate | cartCreate | POST /v1/orders |
| checkoutLineItemsReplace | cartLinesUpdate | POST /v1/orders/{order_id}/line-items |
| checkoutDiscountCodeApplyV2 | cartDiscountCodesUpdate | discounts[].promotion.promotion_code |
| checkoutShippingAddressUpdateV2 | cartDeliveryAddressesAdd | delivery_destination.address |
| checkoutCustomerAssociateV2 | cartBuyerIdentityUpdate | customer_id |
| checkoutCompleteWithTokenizedPaymentV3 | No replacement | POST /v1/orders/{order_id}/pay |
- checkoutCreateCart API: cartCreatePOST /v1/orders
- checkoutLineItemsReplaceCart API: cartLinesUpdatePOST /v1/orders/{order_id}/line-items
- checkoutDiscountCodeApplyV2Cart API: cartDiscountCodesUpdatediscounts[].promotion.promotion_code
- checkoutShippingAddressUpdateV2Cart API: cartDeliveryAddressesAdddelivery_destination.address
- checkoutCustomerAssociateV2Cart API: cartBuyerIdentityUpdatecustomer_id
- checkoutCompleteWithTokenizedPaymentV3No replacementPOST /v1/orders/{order_id}/pay
{
"data": {
"cartCreate": {
"cart": {
"id": "gid://shopify/Cart/c1-a1b2",
"totalQuantity": 3,
"cost": {
"subtotalAmount": {
"amount": "164.00",
"currencyCode": "USD"
}
},
"checkoutUrl": "https://shop.myshopify.com/cart/c/c1-a1b2"
},
"userErrors": []
}
}
}The cart's last field is an address. Shopify's guide says it redirects customers through Shopify's web checkout, where the address, the shipping choice, the payment, and the order all happen.
{
"data": {
"order_id": "ord_1kmn0aExample",
"payment_collection": {
"stripe": {
"account_id": "acct_1kmn0aExample",
"publishable_key": "pk_test_1kmn0aExample",
"elements": {
"next_step": "collect_payment_source",
"submit_to": "pay_order",
"mode": "payment",
"amount_money": { "amount": 17712, "currency": "USD" },
"payment_method_types": ["card"],
"digital_wallets": ["apple_pay", "google_pay"],
"payment_method_creation": "manual"
}
}
}
}
}The order answers with what your page needs to collect the card: the Stripe account, a publishable key, the amount, and the wallets to offer. No secrets, and nowhere to send the buyer.
02The other ways in
Each route into Shopify's checkout has a gate. Flint has an API key.
Teams that reach checkoutUrl usually try these four next. Each one is documented by Shopify with a plan, a partner program, or a term attached. On Flint the same job is a field on the order.
On Shopify's documented surfaces
Changing the payment step
Checkout UI extensions, inside the checkout Shopify renders
information, shipping, and payment steps: Shopify Plus only
Checkout inside a mobile app
Checkout Kit presents Shopify's checkout from a checkoutUrl
the same checkout buyers see on the web, in a sheet
Charging the card with your own code
Payments Apps API, for approved Payments Partners
signed revenue share, and no other Shopify API allowed
A second checkout beside Shopify's
API terms 2.3.18: no alternative to Shopify Checkout
or registering those transactions through the Shopify API
4 routes · a plan tier, a partner agreement, or written authorization
On Flint, you read the order
Changing the payment step
payment_collectionyour markup, on any planCheckout inside a mobile app
pending_actions[].client_actionweb, app, or kiosk: one order APICharging the card with your own code
POST /v1/orders/{order_id}/paya sandbox key at signupA second checkout beside Shopify's
GET /v1/orders/{order_id}the order, its payments, and its refunds
1 order · 1 API key · 1 dated API version
each Shopify row restates Shopify's own documentation or API terms · sources, checked 2026-09-20
03How it works
From the cart to a paid order, on your domain.
Your storefront keeps its routes, components, and design. At checkout your backend makes three Flint calls, and one webhook tells it the order is paid.
The cart is an open order
POST /v1/ordersSend variants and quantities, plus the shipping address. Flint prices each line from your catalog, computes the tax for that address, and holds the balance every payment settles against. Add a line or a discount code later and every figure is recomputed on the same order.
curlcurl -X POST https://api.withflintpay.com/v1/orders \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Idempotency-Key: order-cart-7f3a" \ -d '{ "line_items": [ { "variant_id": "var_1kmn0aRunner", "quantity": 1 }, { "variant_id": "var_1kmn0aSock", "quantity": 2 } ], "delivery_destination": { "address": { "line1": "540 Broad Street", "city": "Augusta", "state": "GA", "postal_code": "30901", "country": "US" } }, "tax": { "enabled": true, "calculation": { "mode": "automatic", "price_mode": "additive" } } }'what Flint computed Trail Runner 2, quantity 1line_items[0] $128.00 Merino sock, quantity 2line_items[1] $36.00 Subtotalpricing_amounts.subtotal_money $164.00 Tax, Augusta GApricing_amounts.tax_money $13.12 Totalpricing_amounts.total_money $177.12 Still owedsettlement_amounts.outstanding_money $177.12 Your storefront displays these amounts as returned. There is no cart total of your own to keep in step with the charge. Mount Stripe Elements on your page
GET /v1/orders/{order_id}Read the order and pass its payment collection block to the browser as returned. Apple Pay and Google Pay appear in the same element once your hostname is registered. Everything around the card fields is your markup.
your checkout pageconst stripe = Stripe(collection.stripe.publishable_key, { stripeAccount: collection.stripe.account_id, }); const guidance = collection.stripe.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, }); elements.create("payment").mount("#payment-element"); // on submit: a single-use credential, posted to your backend await elements.submit(); const { paymentMethod } = await stripe.createPaymentMethod({ elements }); await fetch("/checkout/pay", { method: "POST", body: JSON.stringify({ token: paymentMethod.id }), });Card details go from the buyer's browser to Stripe. They never reach your server or Flint's.
shop.example.com/checkoutThe id in the mount call is the dashed box. The rest of the checkout is yours to design. Pay the order from your backend
POST /v1/orders/{order_id}/paySend the credential the browser created and the balance the buyer approved. Flint confirms the payment and settles the order in the same request. If the order changed after the buyer saw the total, Flint refuses before charging.
curlcurl -X POST https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/pay \ -H "Content-Type: application/json" \ -H "Authorization: Bearer YOUR_API_KEY" \ -H "Idempotency-Key: pay-cart-7f3a-1" \ -d '{ "action": "pay", "payment_source": { "token": "pm_1kmn0aExample" }, "expected_outstanding_money": { "amount": 17712, "currency": "USD" }, "buyer_contact": { "email": "ada@example.com" } }'200 OK{ "data": { "order": { "order_id": "ord_1kmn0aExample", "status": "closed", "payment_status": "paid", "settlement_amounts": { "paid_money": { "amount": 17712, "currency": "USD" }, "outstanding_money": { "amount": 0, "currency": "USD" } } }, "payment_attempt": { "order_payment_attempt_id": "opat_1kmn0aExample", "status": "succeeded", "is_resumable": false } } }A decline arrives in this same response as one of 18 codes, and the order stays open for another card.
Run 3D Secure on your own page
POST /v1/orders/{order_id}/pay · action "resume"When the issuer asks for authentication, the pay call returns a typed action. Your page hands it to Stripe.js, the buyer authenticates there, and your backend resumes the same attempt with no new credential.
200 OK, authentication required{ "data": { "payment_attempt": { "order_payment_attempt_id": "opat_1kmn0aExample", "status": "requires_action", "is_resumable": true, "pending_actions": [ { "action_type": "payment_authentication", "client_action": { "stripe": { "account_id": "acct_1kmn0aExample", "publishable_key": "pk_test_1kmn0aExample", "payment_intent": { "stripe_js_call": "handle_next_action", "client_secret": "pi_1kmn0aExample_secret_1kmn0aExample" } } } } ] } } }browser, then backend// in the browser, on your checkout page const action = attempt.pending_actions[0].client_action.stripe; await stripe.handleNextAction({ clientSecret: action.payment_intent.client_secret, }); // then your backend resumes the same attempt. No new credential. await flint.orders.pay({ order_id: orderId, body: { action: "resume", order_payment_attempt_id: attemptId }, });One attempt, start to finishThe whole challenge happens on shop.example.com. The order is paid once, by the attempt that started it. Fulfill from the webhook
order.paidFulfill from the event, never from the browser returning, which a buyer can skip by closing the tab. Retries reuse the same event id, which makes it your deduplication key. The paid order is already in the dashboard your team works from.
POST /webhooks/flint{ "webhook_event_id": "whev_1kmn0aExample", "event_type": "order.paid", "payload_version": 1, "mode": "live", "merchant_id": "mer_1kmn0aExample", "created_at": "2026-09-20T14:00:21Z", "data": { "order_id": "ord_1kmn0aExample", "status": "closed", "payment_status": "paid", "total_money": { "amount": 17712, "currency": "USD" }, "paid_money": { "amount": 17712, "currency": "USD" }, "outstanding_money": { "amount": 0, "currency": "USD" } } }app.withflintpay.comThe same order in the Flint dashboard, which comes with the keys. All 20 of its sections are there the first time you sign in.
Events your handler subscribes to
- order.paid
- order.partially_paid
- order.refunded
- payment_intent.requires_action
Each event is a fact about one object, with a snapshot of that object in the body. The SDK verifies the signature against the raw request body. Webhooks guide →
Receive them on localhost
No tunnel and no public URL while you build. The CLI mints a signing secret for the session, so you test the same signature check you will run in production.
reviewed 2026-09-20 against the published API · corrections: Flint Help
04Checkout details
Tax, shipping, discounts, and wallets come with the order.
Rendering the checkout yourself does not mean rebuilding what runs behind it. Each of these is a field on the order or one call beside it.
Tax from the shipping address
Set the delivery destination and Flint calculates US sales tax for it, on the items and on the shipping charge. The figure the buyer sees is the figure that settles.
- delivery_destination.address
- tax.calculation.mode
- pricing_amounts.tax_money
Shipping options
Open an embedded checkout session and Flint quotes the buyer's address against your delivery methods, records the choice, and adds the charge to the order. Methods use fixed prices, rate tables, or live rates from your own endpoint.
- POST /v1/delivery-methods
- POST /v1/checkout-sessions/{checkout_session_id}/delivery-quotes
- POST /v1/checkout-sessions/{checkout_session_id}/delivery-selections
Discount codes
Create promotions with codes, automatic rules, and combination limits. Apply a code to the order and it lands in the totals, with tax calculated on what is left.
- POST /v1/promotions
- discounts[].promotion.promotion_code
- pricing_amounts.discount_money
Apple Pay and Google Pay
Register your storefront's hostname once. The wallets then render in the same Payment Element as the card fields and settle into the same order.
- POST /v1/payment-method-domains
- elements.digital_wallets
Signed-in buyers and saved cards
Put the customer on the order and their saved cards are available at checkout. A returning buyer pays with a saved payment method and never retypes a card number.
- customer_id
- payment_source.payment_method_id
- GET /v1/me/payment-methods
Risk rules you write
Allow, block, review, or require 3D Secure by amount, card, email, IP address, and risk level, with lists your rules share. Every payment attempt is evaluated, and a preview validates a rule before you turn it on.
- POST /v1/risk-rules
- POST /v1/risk-previews
05Your payment logic
Hold a card, take a deposit, split the total, refund one item.
Because your backend completes the purchase, it also decides how the money moves. Every shape below settles against the same order, so the balance, the refunds, and the webhooks stay correct.
Hold now, capture later
Authorize the card when the buyer checks out and capture some or all of it when the auction closes, the pre-order ships, or the review clears. Cancel to release the hold.
- capture_method
- POST /v1/orders/{order_id}/payment-intents/{payment_intent_id}/capture
- capturable_money
A deposit now, the balance later
Charge part of an order today. The order reports what is paid and what is still owed, and the next payment settles against the same balance.
- POST /v1/orders/{order_id}/payment-intents
- order.partially_paid
- settlement_amounts.outstanding_money
Subscriptions that renew as orders
Plans, trials, pause, resume, and dunning, billed on a schedule Flint runs. Every renewal is an order, so refunds and reporting work the same way. Subscriptions API
- POST /v1/subscription-plans
- POST /v1/subscriptions
- POST /v1/subscriptions/{subscription_id}/pause
curl -X POST https://api.withflintpay.com/v1/refunds \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Idempotency-Key: refund-ord-1042-001" \
-d '{
"order_id": "ord_1kmn0aExample",
"line_items": [
{ "order_line_item_id": "li_1kmn0aExample", "quantity": 1 }
]
}'You send a line item id and a quantity, and no amounts.
| what Flint refunded | ||
|---|---|---|
| Merino sock, quantity 1line_item_allocations[].amount_money | $18.00 | |
| Tax on that lineline_item_allocations[].tax_money | $1.44 | |
| Refundedamount_money | $19.44 | |
Refund by amount, by line item, or by charge
The refund event tells your backend, and the dashboard shows the same split your support team quotes to the buyer.
- POST /v1/refunds
- line_items[].order_line_item_id
- line_item_allocations
06The catalog
Your products move across from the CSV Shopify exports.
Export products from Shopify admin, then create each one with a single call. Options become options, variants keep their SKU and price, and stock becomes an inventory item per variant. Your storefront reads them from your own backend.
curl -X POST https://api.withflintpay.com/v1/products \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Flint-Version: 2026-09-07" \
-H "Idempotency-Key: import-trail-runner-2" \
-d '{
"name": "Trail Runner 2",
"product_type": "physical",
"metadata": { "shopify_handle": "trail-runner-2" },
"options": [
{
"client_option_key": "size",
"name": "Size",
"values": [
{ "client_value_key": "10", "value": "10" },
{ "client_value_key": "10-5", "value": "10.5" }
]
},
{
"client_option_key": "color",
"name": "Color",
"values": [{ "client_value_key": "slate", "value": "Slate" }]
}
],
"variants": [
{
"sku": "TR2-SLATE-10",
"unit_price_money": { "amount": 12800, "currency": "USD" },
"line_item_tax_category": "clothing",
"inventory_item": {
"name": "Trail Runner 2, Slate, 10",
"sku": "TR2-SLATE-10"
},
"selected_option_values": [
{ "client_option_key": "size", "client_value_key": "10" },
{
"client_option_key": "color",
"client_value_key": "slate"
}
]
}
]
}'Metadata carries the Shopify handle across, so your existing product URLs keep a key to resolve by.
curl 'https://api.withflintpay.com/v1/products?sku=TR2-SLATE-10' \
-H "Authorization: Bearer YOUR_API_KEY"Variants keep their SKU, so an incremental import finds the product to update without a mapping table.
Catalog
Products, options, variants, categories, bundles, and images. Variants carry SKU, barcode, tax category, and whether stock is tracked.
- POST /v1/products
- variants[].sku
- GET /v1/products?sku={sku}
Inventory
Stock per location, with reservations that hold units for an order and release them when it closes.
- POST /v1/inventory-items
- GET /v1/inventory-levels
- POST /v1/inventory-reservations
Fulfillment and returns
Fulfillments, shipments, and packages on the order that was paid, and returns that refund the right lines and restock them.
- POST /v1/orders/{order_id}/fulfillments
- POST /v1/shipments
- POST /v1/returns
Customer accounts
Order history, subscriptions, returns, and saved cards for a signed-in buyer, rendered in your own account pages over a customer session. Customer accounts
- POST /v1/customer-sessions
- GET /v1/me/orders
- GET /v1/me/subscriptions
What comes with the keys
Stripe processes every card. You sign up once and hold one set of keys.
Card data goes straight from your checkout page to Stripe, with the same PCI scope, fraud detection, and dispute handling as a direct integration. Flint provisions and runs the processing account, and you pay one processing fee per payment.
API counts come from the published OpenAPI spec · every code sample is checked against it at build time
Public alpha
Alpha accounts get new features first.
Merchants take real payments on Flint today, and new features reach alpha accounts before general rollout. The order you create this week is the record you keep.
shipped 2026-09-29 · Codes for saved details default to the auto channel
Building now?
Create an account from your terminal with flint signup. The sandbox is free, with no credit card.
07Start
From a sandbox key to a paid test order in ten minutes.
Node
CLI
Test keys are prefixed flint_test_ and bound to a sandbox with its own data, so nothing you do there touches live money. Live keys are prefixed flint_live_, on the same host and the same paths. Pay with 4242 4242 4242 4242, any future expiry, any CVC.
Then read these, in order
- 01
- 02
- 03
- 04
- 05
FAQ
Before you sign up.
Can I build a custom checkout with Shopify's Storefront API?
The Storefront API covers the storefront and the cart. The mutations that created and completed a checkout were shut off on April 1, 2025, and the Cart API that replaced them has no completion mutation. Its last field is checkoutUrl, which sends the buyer through Shopify's web checkout. On Flint the cart is an open order, and your backend completes it with POST /v1/orders/{order_id}/pay while Stripe Elements collects the card on your own page.
Is the Shopify Checkout API deprecated?
It is shut off. Shopify deprecated the Storefront checkout mutations and the REST checkout endpoints, then turned them off on April 1, 2025, and no API version still exposes them. If you are reading a tutorial that calls checkoutCreate or checkoutCompleteWithTokenizedPaymentV3, it predates the shutoff. The same flow on Flint is three calls: create the order, read its payment collection block, and pay it.
What replaced checkoutCreate?
cartCreate, and every other checkout mutation has a cart equivalent: cartLinesUpdate, cartDiscountCodesUpdate, cartDeliveryAddressesAdd, and cartBuyerIdentityUpdate. The one family with no replacement is checkoutComplete, the mutations that took the payment. On Flint that step is POST /v1/orders/{order_id}/pay, called from your backend.
Can I keep Shopify for products and run my own checkout beside it?
Shopify’s API terms cover this. Clause 2.3.18 says an API user may not, without Shopify’s express written authorization, use an alternative to Shopify Checkout for checkout or payment processing for Shopify merchants, or register those transactions through the Shopify API. Flint gives the storefront you already built its own backend: products, variants, stock, orders, and checkout on one API key, so there is nothing to sync back.
What happens to the storefront I already built?
It stays. Flint has no requirements for your frontend, so your Hydrogen, Next.js, or Remix routes, components, and design keep working. What changes is the data layer behind them: product and cart reads move from the Storefront API to your own backend, which talks to Flint with the API key. The browser talks to Stripe.js and to your backend.
How do I move my products?
Export them from Shopify admin as a CSV, which is a standard merchant feature, and create each product with POST /v1/products. Options become options, variants keep their SKU and price, and stock becomes an inventory item per variant. Put the Shopify handle in metadata so your existing URLs keep a key to resolve by, and find any product again with GET /v1/products?sku={sku}.
Do Apple Pay and Google Pay work on my own checkout page?
Yes. Register each checkout hostname once with POST /v1/payment-method-domains and serve checkout over HTTPS. The wallets then appear in the same Stripe Payment Element as the card fields and settle into the same order. Embedded payments guide
How does 3D Secure work on a checkout I render?
When the issuer asks for authentication, the pay call returns a typed pending action with a client secret scoped to that one step. Your checkout page passes it to the Stripe.js handleNextAction call, the buyer authenticates there, and your backend resumes the same payment attempt. The buyer never leaves your domain.
How is sales tax calculated?
Flint calculates US sales tax on the order from the delivery destination you send, on the line items and on shipping charges, and returns it in the order's pricing amounts. If you already run a tax engine, send its jurisdiction components in external mode and Flint records them on the order and reverses them on refunds.
Does Flint calculate shipping rates?
Flint evaluates the delivery methods assigned to the checkout. A method can use fixed pricing, a rate table, or a live rate from your own endpoint. Flint presents the available options, records the buyer's selection, applies the selected charge to the order, and taxes it.
Where does my team manage orders?
In the Flint dashboard, which comes with the keys. Orders, payments, refunds, products, inventory, fulfillment, returns, subscriptions, and payouts are there the first time you sign in, 20 sections in all, and everything in it is also in the API.
Which payment methods can buyers use?
Cards, Apple Pay, and Google Pay on your own form. Flint's hosted checkout page adds ACH debit and Affirm. Every method settles into the same kind of order, so refunds, webhooks, and reporting work the same way however the money arrived.
Who processes the cards?
Stripe processes every card. Card data goes directly from your checkout page to Stripe, with the same PCI scope, fraud detection, and dispute handling as a direct Stripe integration. Flint provisions and operates the processing account, so there is one signup, one set of keys, and one fee per payment.
What does it cost?
Cards cost 3.79% + 35¢, with no monthly fee. The sandbox is free and needs no credit card. One processing fee per payment covers Flint and the card processing underneath, so there is no separate processor bill. Automatic tax, invoices and subscriptions are optional add-ons with published rates.
How do I get started?
Request access and we email you a link to create your account, or create one now from your terminal with flint signup. The sandbox is free, with no credit card and no sales call. Flint is in public alpha and onboards US businesses today; buyers can pay from anywhere. Install the Node SDK, create an order, and pay it with the 4242 test card in about ten minutes. Quickstart
Sources
Where the Shopify facts come from.
The Checkout APIs were shut off on April 1, 2025, and the Cart API replaced them. Checkout APIs will be shut down April 1, 2025
shopify.devEach removed checkout mutation and its cart replacement. Migrate your app to the Cart API
shopify.devcheckoutUrl sends the buyer through Shopify's web checkout. Create and update a cart with the Storefront API
shopify.devUI extensions on the information, shipping, and payment steps need Shopify Plus. Checkout UI extensions
shopify.devCheckout Kit presents Shopify's checkout inside an app from a checkoutUrl. Checkout Kit
shopify.devPayments extensions: approved partners, a revenue share, and no other Shopify API. Requirements for payments extensions
shopify.devClause 2.3.18, on alternatives to Shopify Checkout. Shopify API License and Terms of Use
www.shopify.com
last reviewed 2026-09-20 · corrections: Flint Help
Render your own checkout this week.
Free sandbox, no credit card, no sales call. Your storefront stays as you built it, and your first paid test order takes about ten minutes.
Skip the wait: npm install -g @flintpay/cli && flint signup creates your account and a sandbox key from the terminal. CLI guide
POST /v1/orders · create your first order