Skip to content

Platform limits

Headless Shopify doesn't mean headless checkout.

Shopify's Storefront API will give you a fully custom storefront and cart. It will not give you a custom checkout. The Checkout API was shut off on April 1, 2025, the Cart API's last step is a checkoutUrl you send the buyer to, and the Payments Apps API is a processor-side interface open only to approved Payments Partners. If you need to render the payment step yourself, that is not a missing feature. It is the platform boundary.

Checkout API

Deprecated, then shut off on April 1, 2025. No API version still exposes it.

Cart API

The documented replacement. Its terminal field is a URL to a checkout Shopify renders.

Payments Apps API

Approved partners only, under a revenue share on volume. It resolves sessions; it renders nothing.

published 2026-07-25 · every claim below is sourced to shopify.dev or help.shopify.com

01Definitions

Four things people mean by headless.

Almost every argument about this question is two people using one word for different layers. Separate them and the answer stops being controversial.

LayerWho owns itWhat Shopify gives you
StorefrontYoursStorefront API, Hydrogen, or any framework you like, on any domain.
CartYoursCart API. Lines, discount codes, delivery addresses, buyer identity.
Checkout UIShopify'sCheckout API removed 2025-04-01. Extensions render inside Shopify's checkout, and the payment step needs Plus.
PaymentsPartner-onlyPayments Apps API, restricted to approved Payments Partners under a revenue share.

The first two rows are real, well documented, and genuinely good. You can build a storefront in any framework, host it anywhere, and drive a cart entirely through GraphQL. Shopify invested heavily here and it shows.

Two of those four layers are yours. The third is the one people were actually asking about. When a team says they want headless checkout, they almost never mean they want to restyle a page. They mean they want to own the payment step: its markup, its validation, its failure states, and where the money goes. That is row three, and row three is not available.

02The removal

The Checkout API is not deprecated. It is off.

Deprecated means discouraged. This is stronger, and the distinction matters if you are reading a tutorial written before 2025.

Shopify deprecated the Storefront checkout mutations and the REST checkout endpoints, then shut them off on April 1, 2025. The changelog is unambiguous: customers "will not be able to create or complete checkouts using the deprecated Checkout APIs after the deadline". There is no API version you can pin to keep them. checkoutCreate, checkoutLineItemsAdd, checkoutEmailUpdateV2, and the checkoutComplete family all went at once.

Shopify published a migration mapping, and it is worth reading for what is missing from it rather than what is in it.

Removed 2025-04-01Cart API replacement
checkoutCreatecartCreate
checkoutLineItemsReplacecartLinesUpdate
checkoutDiscountCodeApplyV2cartDiscountCodesUpdate
checkoutShippingAddressUpdateV2cartDeliveryAddressesAdd
checkoutCustomerAssociateV2cartBuyerIdentityUpdate
checkoutCompleteWithTokenizedPaymentV3No replacement

Every cart operation has an equivalent. Lines, discount codes, delivery addresses, buyer identity: all of it moved across, and the Cart API is a better API than the one it replaced. The one row with no right-hand side is the one that took money. That is not an oversight in the mapping. Nothing in the Cart API charges a card, and nothing was added later that does.

03The handoff

The Cart API's last field is a URL.

You can drive the entire cart from your own code. Then there is exactly one way forward, and it is not a mutation.

Storefront API
mutation CartCreate($lines: [CartLineInput!]!) {
  cartCreate(input: { lines: $lines }) {
    cart {
      id
      totalQuantity
      cost { subtotalAmount { amount currencyCode } }
      checkoutUrl
    }
    userErrors { field message }
  }
}

Everything here is yours: quantities, pricing display, discount codes, the whole cart.

200 OK
{
  "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": []
    }
  }
}

Every headless Shopify tutorial ends on checkoutUrl. It is not a step inside your checkout. It is where your checkout stops.

Shopify describes this plainly: the response includes a URL that "redirects customers through Shopify's web checkout," and the guidance is to retrieve it when the buyer is ready to go there. From that point the address, shipping selection, taxes, payment method, authentication, and order creation all happen on Shopify's page.

Shopify has also said why, in its own RFC on the transition. The stated reason is PCI scope, and a maintainer put the design intent directly: "circumventing the checkout flow for buyers, even for a free transaction, could result in critical business logic not running." Read that as a product decision rather than a gap, because that is what it is. The same thread carries the cost: an auction app noting the change "prevents us from charging the winning bidder/customer immediately," and an immersive-commerce developer describing buyers having to "drop your session" to pay.

One update people miss. Checkout Kit, the successor to Checkout Sheet Kit, now embeds that page rather than navigating to it: you pass a checkoutUrl and it presents Shopify's checkout inside your app. If you read an older article saying you must redirect, that is out of date. It is also a smaller change than it sounds. The redirect became an embed. Ownership did not move.

The ceiling, stated by ShopifyDeeper control over the embedded checkout, described as "custom layout changes and themes," is available to "select partners," with TikTok given as the example. So there is a way to substantially change Shopify's checkout. The requirement is being TikTok.

04The other door

The Payments Apps API is not a checkout API.

This is the surface people reach for next, usually after reading its name and nothing else. It solves a different problem than the one you have.

Payments Apps API
mutation {
  paymentSessionResolve(
    id: "gid://shopify/PaymentSession/1"
    networkTransactionId: "txn_from_your_processor"
  ) {
    paymentSession { id status { code } }
    userErrors { field message }
  }
}

This is the shape of the whole thing: Shopify opens a payment session, and you answer it. There is no field here through which you could render anything.

Eligibility
Approved Payments Partners only
Terms
Signed revenue share on payments volume across all merchants
Billing
Invoicing begins above $150,000 USD monthly volume
Other APIs
Not permitted beyond mandatory webhooks
Uptime
99.95%, with a 2 hour response during incidents

Payment methods a payments extension may not process:

  • Apple Pay
  • Google Pay
  • Shop Pay
  • PayPal
  • Alipay

Shopify's reference opens by saying the API is "available only to approved Payments Partners." Getting approved means applying to the payments platform program and signing a revenue share agreement calculated on total payments volume across all merchants who use you. What you get in return is the ability to resolve, pend, or reject payment sessions, and to answer captures, refunds, and voids.

Then there is the clause that ends the idea for most teams who find their way here. A payments extension may not "use any Shopify APIs other than the Payments Apps API and mandatory webhooks, or require merchants to install additional apps alongside the payments extension." If your product is a payments extension, it cannot also be an app that reads orders or products. It is a payments extension and nothing else.

It is worth being exact about what this API is for. It is how you become a payment method that appears inside Shopify's checkout, next to Shop Pay and the card fields. That is a real and valuable thing to be, and for a payment processor it is the right integration. It is not a way to build a checkout, and no amount of approval turns it into one.

05The real surface

What you can actually change.

It is a lot, it is well built, and for most stores it is enough.

Checkout Extensibility is the supported way to modify checkout, and it is three things. Checkout UI extensions render your components at fixed targets inside the page. Shopify Functions run your logic server-side for discounts, shipping, and payment method rules. The Branding API restyles the checkout through a design-system object rather than a stylesheet.

The gates are real and worth knowing before you scope anything. UI extensions on the information, shipping, and payment steps are available only to stores on Shopify Plus; other plans can extend the Thank you and Order status pages. Checkout branding is Plus or development stores only. And checkout.liquid is gone: removed for the information, shipping, and payment pages on August 13, 2024, and for Thank you and Order status on August 28, 2025. The era of injecting arbitrary HTML, CSS, and JavaScript into checkout is over, deliberately.

None of that is a criticism. Shopify's checkout is one of the most optimized purchase flows on the internet, it carries Shop Pay's vaulted-card network, and it absorbs PCI scope, tax, fraud, and accessibility work across a very large number of markets. A sandboxed extension model is how you keep a page like that fast and correct while thousands of apps modify it. If your requirements fit inside these surfaces, using them is not a compromise. It is the better engineering decision.

06The decision

Five questions that settle it.

Answer these before you read another migration guide. Five no's means stay, and mean it.

  1. 01

    Do you need to render the payment step?

    Not restyle it, render it. Your markup, your validation, your layout, your framework.

  2. 02

    Does money need to move somewhere Shopify does not move it?

    Splitting a single order across several payees, or settling to an account Shopify's payouts do not reach.

  3. 03

    Does checkout need to run somewhere Shopify does not ship to?

    A kiosk, a countertop POS, a voice or chat surface, an in-game store, an agent acting for a buyer.

  4. 04

    Is the transaction shape something other than retail?

    An auction charged at close, a deposit now and a balance later, pay-at-table, split tender, a tab held open.

  5. 05

    Does the order of record need to live outside Shopify?

    Because your product, not the storefront, is the system the business actually runs on.

Everything on Shopify's supported path answers the styling question. None of it answers these. A team that needs any one of them is not looking for a checkout extension, and spending a quarter discovering that through prototypes is a common and expensive way to learn it.

07The cost

If you leave, this is what you give up.

Anyone who tells you the migration is free is selling you the migration.

Shop Pay. It does not come with you. Shop Pay recognizes buyers who have already checked out at some other Shopify store, so a returning stranger pays without typing an address or a card number. No checkout you build starts with that network, and nothing you build will replicate it.

The parts of checkout you were not thinking about. Address autocomplete and validation, shipping rate selection, tax calculation across jurisdictions, discount stacking rules, fraud analysis, abandoned-cart recovery, and localized payment methods per market. Shopify's checkout is not one page. It is a great deal of accumulated correctness, and rebuilding the parts you need is the actual scope of the project.

Your app ecosystem. Every Shopify app that triggers on an order (subscriptions, loyalty, reviews, shipping labels, accounting sync) is watching a Shopify order. Orders paid outside Shopify do not appear there, and each of those integrations becomes yours to replace or re-point.

The fee structure, if you stay partly on Shopify. Processing through a provider other than Shopify Payments while still on Shopify incurs third-party transaction fees, at a rate that varies by plan. Shopify waives them on Plus only when Shopify Payments is the sole provider. Check the current numbers against Shopify's pricing page rather than against a blog, this one included.

If you need to render the payment step, you are not missing a Shopify feature. You are outside Shopify's checkout. That is the end of the road.

08The other side

What owning the checkout looks like.

Flint is a payments and commerce API. It has no storefront and no themes. What it has is the layer Shopify keeps for itself: the order, the payment step, and the record afterward.

Start with the order. You send line items and the API computes the totals, so nothing about the sale is reconstructed later from display values. Line items take a name and a unit price, which means the catalog can stay wherever it already lives.

curl
curl -X POST https://api.withflintpay.com/v1/orders \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: order-trail-runner-001" \
  -d '{
    "line_items": [
      { "name": "Trail Runner 2", "quantity": 1,
        "unit_price_money": { "amount": 12800, "currency": "USD" } },
      { "name": "Merino sock", "quantity": 2,
        "unit_price_money": { "amount": 1800, "currency": "USD" } }
    ]
  }'

Each line is a name, a quantity, and a unit price. The total is not in the request.

201 Created
{
  "data": {
    "order_id": "ord_1kmn0aExample",
    "status": "open",
    "payment_status": "unpaid",
    "pricing_amounts": {
      "subtotal_money": { "amount": 16400, "currency": "USD" },
      "tax_money":      { "amount": 1312,  "currency": "USD" },
      "total_money":    { "amount": 17712, "currency": "USD" }
    },
    "settlement_amounts": {
      "paid_money":        { "amount": 0,     "currency": "USD" },
      "outstanding_money": { "amount": 17712, "currency": "USD" }
    }
  }
}

Subtotal, tax, and total came back computed. The outstanding balance is what payments settle against.

  • POST /v1/orders
  • pricing_amounts.total_money
  • settlement_amounts.outstanding_money

Then the field that Shopify's cart does not have.

A payment leg against that order returns collection guidance rather than a destination. This is the exact inversion of checkoutUrl: instead of an address to send the buyer to, you get the configuration to render the payment step yourself.

curl
curl -X POST \
  https://api.withflintpay.com/v1/orders/ord_1kmn0aExample/payment-intents \
  -H "Content-Type: application/json" \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: pi-trail-runner-001" \
  -d '{
    "payment_source_selection": {
      "card": { "digital_wallets": ["apple_pay", "google_pay"] }
    }
  }'

Your backend. The API key never reaches the browser.

201 Created
{
  "data": {
    "payment_intent": {
      "payment_intent_id": "pi_1kmn0aExample",
      "order_id": "ord_1kmn0aExample",
      "status": "requires_payment_method",
      "amount_money": { "amount": 17712, "currency": "USD" },
      "capture_method": "automatic"
    },
    "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"
        }
      }
    }
  }
}

Shopify's cart hands you a URL. This hands you a publishable key and an Elements configuration, and stops.

  • POST /v1/orders/{order_id}/payment-intents
  • payment_collection.stripe.elements
  • payment_method_creation
your frontend
const stripe = Stripe(paymentCollection.stripe.publishable_key, {
  stripeAccount: paymentCollection.stripe.account_id,
});

const guidance = paymentCollection.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,
});

// Your markup. Your layout. Your domain.
elements.create("payment").mount("#payment-element");

Stripe renders the card fields, because card data must never touch your server. Everything around them is yours.

shop.yourdomain.com/checkout
Flint's own checkout components, shown on a page you host. In your build this is your markup and your layout; the Payment Element is the only part Stripe renders.

Payment is a call your backend makes, not a page you hand over.

The browser creates a one-time source token and your server submits it against the leg. Flint confirms, settles the order, and answers with both records.

curl
curl -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-trail-runner-attempt-1" \
  -d '{
    "payment_intents": [
      { "payment_intent_id": "pi_1kmn0aExample", "token": "pm_1kmn0aExample" }
    ],
    "expected_outstanding_money": { "amount": 17712, "currency": "USD" },
    "completion_behavior": "complete_order",
    "buyer_email": "ada@example.com"
  }'

expected_outstanding_money is the balance the buyer approved. If the order moved underneath them, this refuses to charge.

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": {
      "payment_attempt_id": "opat_1kmn0aExample",
      "status": "succeeded",
      "is_resumable": false,
      "payment_intents": [
        { "payment_intent_id": "pi_1kmn0aExample", "status": "succeeded" }
      ]
    }
  }
}

The order is paid and closed, and the attempt that paid it is part of the answer.

  • POST /v1/orders/{order_id}/pay
  • expected_outstanding_money
  • completion_behavior

Authentication is an answer, not an error.

The step that most makes people want a hosted checkout is 3D Secure, because it hands control to the card issuer in the middle of your flow. Here the attempt stays resumable and tells you exactly which Stripe.js call to run, then you resume by attempt id.

200 OK, authentication required
{
  "data": {
    "payment_attempt": {
      "payment_attempt_id": "opat_1kmn0aExample",
      "status": "requires_action",
      "is_resumable": true,
      "pending_actions": [{
        "pending_action_id": "pendact_1kmn0aExample",
        "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"
            }
          }
        }
      }]
    }
  }
}

is_resumable true, and a client action naming the call. Nothing is lost and nothing is charged twice.

POST your endpoint
{
  "webhook_event_id": "whev_1kmn0aExample",
  "event_type": "order.paid",
  "payload_version": 1,
  "mode": "test",
  "merchant_id": "mer_1kmn0aExample",
  "created_at": "2026-07-24T14:00:00Z",
  "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" }
  }
}

Fulfill on this or on a backend read of the order, never on the browser returning. A buyer can close the tab.

  • order.paid
  • order.partially_paid
  • order.payment_captured
  • payment_intent.requires_action
  • payment_intent.payment_failed
  • refund.created

You send line items, not a total, so the sale is computed rather than asserted.

POST /v1/orders

A payment leg returns Elements configuration for your own UI, not a redirect.

payment_collection.stripe.elements

Your server confirms the payment. The browser never holds confirmation authority.

POST /v1/orders/{order_id}/pay

Authentication returns a resumable attempt, not a failed one.

pending_actions[].client_action

Refunds target line items and quantities, so the math is not yours to repeat.

POST /v1/refunds

reviewed 2026-07-25 against the published API

09Limits

What Flint does not do.

Flint is not a Shopify replacement. There is no storefront, no themes, no CMS, no product pages, no blog, no app store, and no merchandising tools. If what you actually want is a store, this is the wrong category of product. Shopify is a store. Flint is an API.

Flint is not a payment processor. Stripe processes every card, and Flint provisions and operates the processing account. You cannot attach a Stripe account you already have, and Flint inherits Stripe underwriting: if Stripe declined your business, Flint cannot approve it.

There is no card-present support. Terminals, chip readers, and in-person capture are not part of the API today. If half your volume crosses a counter, this is the wrong tool for that half.

The client list is short. A Node SDK and a CLI. No Python, Go, Ruby, PHP, or Java SDK exists, and everything else talks to documented, stable REST.

And the gaps specific to leaving Shopify. There is no Shopify app, no order sync, and no inventory bridge: if Shopify stays your catalog, the bridge between it and Flint is code you own. Beyond that, global catalogs, faceted product search, localization, and B2B price lists are what commercetools-class platforms are for, and Flint has none of them. If those are hard requirements this quarter, Flint is the wrong answer.

10The other verdict

When Shopify is the right answer.

Most of the time.

If you sell physical goods through a conventional retail flow, if your checkout requirements are about branding and a few custom fields, if you want a theme ecosystem and an app store and someone else carrying PCI scope and tax registration, then Shopify's hosted checkout is not a limitation you are working around. It is most of the value you are buying, and Checkout Extensibility on Plus will cover far more than people assume before they try it.

The reason to leave is not that Shopify's checkout is bad. It is that your product needs to own the transaction, and Shopify's checkout is not built to be owned. Those are different complaints, and only the second one is a reason to go anywhere.

11Sources

Check the claims.

Every statement about Shopify on this page comes from one of these. If one has gone stale, tell us and we will date the correction.

last reviewed 2026-07-25 · corrections: support@withflintpay.com

FAQ

Questions worth asking first.

Can I build a custom checkout with Shopify's Storefront API?

No. The Storefront API covers the storefront and the cart. The mutations that created and completed a checkout were removed on April 1, 2025, and the Cart API that replaced them has no completion mutation. The cart's last useful field is checkoutUrl, which sends the buyer to a checkout Shopify renders.

Is the Shopify Checkout API deprecated?

It is past deprecated. Shopify deprecated the Storefront checkout mutations and the REST checkout endpoints, then shut them off on April 1, 2025. Shopify's changelog states that customers cannot create or complete checkouts through those APIs after that date. There is no version of the Storefront API that still exposes them.

What replaced checkoutCreate?

cartCreate, and every other checkout mutation has a cart equivalent: cartLinesUpdate, cartDiscountCodesUpdate, cartDeliveryAddressesAdd, cartBuyerIdentityUpdate. The one family with no replacement is the checkoutComplete mutations. Nothing in the Cart API takes payment, so the migration path moves your cart and stops before your checkout.

Can I use my own payment gateway in Shopify checkout?

Only as an approved Payments Partner. The Payments Apps API is restricted to partners Shopify has approved, and it requires a signed revenue share agreement calculated on payments volume across all merchants. A payments extension also may not call any Shopify API other than the Payments Apps API and its mandatory webhooks, and may not process Apple Pay, Google Pay, Shop Pay, PayPal, or Alipay.

Do checkout UI extensions require Shopify Plus?

For the parts that matter, yes. Shopify documents that UI extensions on the information, shipping, and payment steps are available only to stores on Shopify Plus. Stores on other plans can extend the Thank you and Order status pages. The checkout Branding API is also Plus or development stores only.

Can I embed Shopify checkout in my own app instead of redirecting?

Yes, with Checkout Kit, which takes a checkoutUrl and presents Shopify's checkout inside your app. It changes where the checkout appears, not who renders it. Customization inside it still runs through UI extensions and branding, both of which need Plus, and deeper layout or theme control is a select-partner arrangement.

Can I use my own domain for Shopify checkout?

Not for the checkout itself. The storefront can live on any domain you like, which is the whole point of headless, but checkoutUrl resolves to a Shopify-hosted checkout and the buyer completes payment there. Checkout Kit can hide the navigation inside an app, though the page is still served and rendered by Shopify.

Can I keep my products in Shopify and run checkout somewhere else?

Technically yes, and you would own the integration. Flint line items take a name and a unit price, so nothing forces you to move a catalog to sell through it. What Flint does not ship is a Shopify app, an order sync, or an inventory bridge, so keeping Shopify as the catalog means writing and operating that glue yourself, and orders paid outside Shopify will not appear in Shopify's admin unless you put them there.

Start

Ten minutes, no card required.

Free sandbox keys. Create an order, mount Elements on localhost, and pay it with a test card.

npm install @flintpay/node
brew install flintpay/tap/flint

Test keys run against an isolated sandbox on the same host as live. Test mode is a property of the key, not a flag on the request, so there is no environment switch to forget.

Then read these three.

  1. Embedded payments with Stripe ElementsThe flow above, end to end, including 3D Secure resume.
  2. Payment intents API referenceEvery field on the collection guidance, and the confirmation rules.
  3. The order-first payments APIWhy the order is the primitive here, and what that changes downstream.