Skip to content
flint

Payments for iOS apps

Accept payments in your iOS app, with the store already built.

Sell physical goods and real-world services from a Swift or SwiftUI app. The app collects the card with Stripe's iOS SDK, your backend pays a Flint order with one call, and every sale arrives as an order with its totals, an emailed receipt, refunds by line item, a buyer account, and a dashboard, plus US sales tax when you turn it on.

Stripe's SDK for the card

The app turns the card into a Stripe payment method. Card numbers go to Stripe and never touch your servers or Flint's.

One API from your backend

Your server creates the order and pays it with one Flint key, which never ships in the app binary.

Apple Pay, hosted

Open Flint's hosted checkout in SFSafariViewController and buyers can pay with Apple Pay, with no merchant ID to set up.

01App Store rules

Physical goods and real-world services pay outside In-App Purchase.

Apple's App Review Guidelines draw the line by what the buyer gets. Content or features unlocked inside the app go through In-App Purchase. Goods and services used outside the app go through another payment method, and that is the sale Flint is built for.

Physical goods and services used outside the app, such as products you ship or tickets to an event: Apple requires a payment method other than In-App Purchase, such as Apple Pay or credit card entry.

Guideline 3.1.3(e)

Subscriptions to in-app content, premium features, game currency, and unlocking a full version: Apple requires In-App Purchase. Flint isn't for these.

Guideline 3.1.1

Apple reviews every app against its own guidelines, so check your app's case against them before you submit.

App Review

reviewed 2026-10-30 against Apple's App Review Guidelines · corrections: Flint Help

Who Flint serves

Businesses based in the United States, charging in US dollars, for online payments. Buyers can pay with cards issued anywhere.

Two ways to take the payment

Collect the card on your own screen with Stripe’s iOS SDK, or open Flint’s hosted checkout, with Apple Pay, in a Safari sheet. Both pay the same Flint order, and both start from the same backend.

02Your own checkout screen

Four steps from a cart to a paid order.

The app keeps the card field it would use with Stripe, and the backend talks to Flint. Flint confirms the payment, so there is no PaymentIntent for the app to confirm and no payment sheet configuration to keep in step with your server.

  1. Create the order on your backend

    POST /v1/orders

    The app sends variant ids and quantities. Flint prices the cart from your catalog and computes the total, so a price is never the app's to choose. Read the order back and send its payment_collection.stripe to the app.

    From your server, with your secret key
    curl -X POST https://api.withflintpay.com/v1/orders \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -H "Idempotency-Key: ios-cart-7f3a" \
      -d '{
        "line_items": [
          {
            "type": "variant",
            "variant_id": "var_1kmn0aExample",
            "quantity": 1
          }
        ]
      }'

    Create the product once in the dashboard's catalog. The order is the record every later step settles against.

    GET /v1/orders/{order_id}
    {
      "data": {
        "order_id": "ord_1kmn0aExample",
        "version": 1790760000,
        "settlement_amounts": {
          "outstanding_money": { "amount": 3500, "currency": "USD" }
        },
        "payment_collection": {
          "stripe": {
            "account_id": "acct_1AbcPlaceholder",
            "publishable_key": "pk_test_51AbcPlaceholder",
            "elements": {
              "next_step": "collect_payment_source",
              "amount_money": { "amount": 3500, "currency": "USD" },
              "payment_method_types": ["card"]
            }
          }
        }
      }
    }

    The publishable key and account are for Stripe's SDK and hold no secrets. Keep the balance and version on your server: they are what the buyer approves.

  2. Collect the card in the app

    STPAPIClient.createPaymentMethod

    Stripe's card field takes the number, and one Stripe call turns it into a payment method on the account the order named. The app posts its id to your backend.

    Swift, with Stripe's iOS SDK
    // payment_collection.stripe, as your backend read it
    // from the order. It holds no secrets.
    let stripe = STPAPIClient(publishableKey: collection.publishableKey)
    stripe.stripeAccount = collection.accountId
    
    // cardField is an STPPaymentCardTextField on your checkout screen
    let paymentMethod = try await stripe.createPaymentMethod(
      with: cardField.paymentMethodParams
    )
    
    // your backend pays the Flint order with it
    let result = try await backend.pay(paymentMethodId: paymentMethod.stripeId)

    Card details go from the app to Stripe. Your server and Flint only see the pm_ id.

    Your screen, your layout. Stripe draws only the card field.
  3. Pay the order from your backend

    POST /v1/orders/{order_id}/pay

    Your backend sends the payment method as payment_source, with the balance and version the buyer approved. Flint charges it against the order and answers with the payment attempt, whatever the outcome.

    The route your app calls
    // the app posts the payment method id it got from Stripe
    app.post("/checkout/pay", async (req, res) => {
      const { paymentMethodId } = req.body;
      // the order id, balance, and version the buyer approved
      const quote = await loadQuote(req.user);
    
      const response = await fetch(
        `https://api.withflintpay.com/v1/orders/${quote.orderId}/pay`,
        {
          method: "POST",
          headers: {
            Authorization: `Bearer ${process.env.FLINT_API_KEY}`,
            "Content-Type": "application/json",
            "Idempotency-Key": `pay-${quote.orderId}-${paymentMethodId}`,
          },
          body: JSON.stringify({
            action: "pay",
            payment_source: { credential_type: "token", token: paymentMethodId },
            expected_outstanding_money: quote.outstandingMoney,
            expected_version: quote.version,
            buyer_contact: { email: quote.buyerEmail },
          }),
        },
      );
      const body = await response.json();
      if (!response.ok) return res.status(response.status).json(body.error);
    
      const attempt = body.data.order_payment_attempt;
      res.json({
        status: attempt.status,
        clientAction: attempt.pending_actions[0]?.client_action?.stripe ?? null,
      });
    });

    Deriving the idempotency key from the order and the payment method makes a retried request safe.

    The app branches on

    • succeeded
    • requires_action
    • failed
    • processing

    A decline comes back as a 200 with a failed attempt and a message you can show the buyer.

    last_payment_error

    On success the order closes as paid and the buyer gets an emailed receipt.

    order.paid

    Declines and payment attempts covers every status and the retry after a decline.

  4. Handle 3D Secure when the bank asks

    requires_action

    The attempt pauses with a pending action that carries a PaymentIntent client secret. The app hands it to Stripe's payment handler, then your backend resumes the same attempt with no new card.

    Swift, with Stripe's iOS SDK
    // result.clientAction is pending_actions[0].client_action.stripe,
    // decoded with snake_case keys converted to camelCase
    let action = result.clientAction!
    let actionClient = STPAPIClient(publishableKey: action.publishableKey)
    actionClient.stripeAccount = action.accountId
    
    let handler = STPPaymentHandler.shared()
    handler.apiClient = actionClient
    _ = await handler.handleNextAction(
      paymentIntentClientSecret: action.paymentIntent.clientSecret,
      authenticationContext: self,
      returnURL: nil
    )
    
    // resume even after an error: Flint reads the outcome from Stripe
    let resumed = try await backend.resume()

    Pass the client secret only to handleNextAction. Resume from your backend and read the outcome there.

    What the pay call returns
    {
      "order_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_1AbcPlaceholder",
                "publishable_key": "pk_test_51AbcPlaceholder",
                "payment_intent": {
                  "stripe_js_call": "handle_next_action",
                  "client_secret": "pi_3AbcPlaceholder_secret_XyzPlaceholder"
                }
              }
            }
          }
        ]
      }
    }

    Your backend sends client_action.stripe to the app.

Fulfill from your backend, on the order.paid webhook or a fresh read of the order. A buyer can close the app before it hears the result.

order.paid

In a sandbox, 4242 4242 4242 4242 succeeds, and 4000 0025 0000 3155 asks for 3D Secure, so both branches run before you go live.

sandbox

reviewed 2026-10-30 against the embedded payments guide · test cards · corrections: Flint Help

03Apple Pay and hosted checkout

Or open Flint's checkout in a Safari sheet.

Your backend creates a checkout session for the same order, and the app presents its URL in SFSafariViewController. Buyers pay with Apple Pay or a card on a page Flint hosts, with no card field in the app at all.

POST /v1/checkout-sessions
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ios-checkout-7f3a" \
  -d '{
    "type": "order",
    "order_id": "ord_1kmn0aExample",
    "redirects": { "success_url": "https://shop.example.com/thanks" }
  }'

The response's url is a Flint checkout for this order. success_url must be an absolute HTTPS URL on your domain.

Swift, with SafariServices
// url is the checkout session's url, from your backend
let checkout = SFSafariViewController(url: url)
checkout.delegate = self
present(checkout, animated: true)

func safariViewControllerDidFinish(_ controller: SFSafariViewController) {
  // the sheet closed: ask your backend whether the order is paid
  Task { await refreshOrder() }
}

When the buyer taps Done, the app asks your backend. The redirect is for the buyer and proves nothing about payment.

Flint's hosted checkout in the Safari sheet, with Apple Pay on top.

Apple Pay works on hosted checkout with no setup: checkout runs on Flint's domain, so there is no Apple merchant ID, certificate, or domain file for you to manage.

checkout.withflintpay.com

Apple supports Apple Pay on the web in SFSafariViewController, so buyers can pay with Apple Pay inside your app's Safari sheet.

SFSafariViewController

A payment link works the same way: one URL for a product, opened from a button in the app, with no backend call at all.

POST /v1/payment-links

Your backend code is the same for every mobile app. Building for Android too? See payments in an Android app. Sharing one codebase? See payments in a React Native app.

04What you skip

Your backend pays an order. The rest of the store comes with it.

A charge records an amount. A store also needs prices, tax, receipts, refunds, and order history, and in an app each one is backend code plus a screen that waits on App Review. On Flint, each one is already on the order the payment settled.

Around a charge, your backend builds

amount + currencywhat a charge records
  1. Prices

    A product table your backend prices carts from

    kept in step with what the app shows

  2. Totals

    An orders table and the code that sums it

    has to match the charged amount to the cent

  3. Sales tax

    A tax lookup, stored per line item

    recomputed on every edit and every refund

  4. Receipts

    An itemized receipt built from your order data

    plus an email service to send it

  5. Refunds

    Tooling that maps a charge back to items

    plus each item's share of the tax

  6. Order history

    Screens for past orders and saved cards

    built and shipped through App Review

  7. Subscriptions

    Renewals, retries, and failed-payment email

    a scheduler that runs every cycle

7 systems to build, test, and keep in sync

On Flint, your backend reads the order

ord_1kmn0aExampleGET /v1/orders/{order_id}
  1. Prices

    variant_idPOST /v1/products
  2. Totals

    pricing_amountsPOST /v1/orders
  3. Sales tax

    tax_moneypricing_amounts.tax_money
  4. Receipts

    order.paidFlint emails the buyer
  5. Refunds

    line_item_allocationsPOST /v1/refunds
  6. Order history

    customer_accountaccount.withflintpay.com
  7. Subscriptions

    subscription_plan_idPOST /v1/subscriptions

1 order · 1 API key · 1 webhook stream

Sales tax is calculated on the order when you turn it on, from the buyer’s location; sales tax covers locations and categories. Subscriptions suit goods and services delivered outside the app, such as a coffee club or a membership; see subscription billing.

05The back office

Support, refunds, and order history, with nothing to ship through App Review.

The first time a buyer asks where an order is, or wants one item refunded, you need a record and a tool. Every sale from the app is an order in your dashboard, and every buyer gets an account page that their receipt links to and your app never has to draw.

app.withflintpay.com
Flint dashboard order detail: line items, totals with a line-item refund, the payment attempt and an activity timeline on one $19.43 order record
One order: what sold, the totals, a refund against a single line item, the payment attempt, and a timeline.
The buyer's account at account.withflintpay.com: orders, subscriptions, and payment methods. Every receipt links to it.

Refund one item

Pick the line item and a quantity. Flint works out what it sold for, including its share of any discount and tax, and returns the money to the card that paid. Refunds

  • line_item_allocations
  • tax_money
  • refund.succeeded

Receipts that send themselves

The buyer gets a receipt when the order is paid, with a link to their account. You can switch any email family to send your own from the webhooks.

A buyer account you don't build

Buyers find past orders, manage subscriptions, and change a saved card, on Flint’s page or a subdomain of yours. Customer accounts

The only scopes your backend key needs

  • commerce.orders.write
  • checkouts.checkout_sessions.write

The key creates and pays orders and opens checkout sessions, and stays on your server. Anything shipped in an app binary is public, however it is obfuscated. Key security covers scopes and rotation.

Start

From a sandbox key to a paid test order.

Your first call
curl -X POST https://api.withflintpay.com/v1/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: ios-cart-7f3a" \
  -d '{
    "line_items": [
      {
        "type": "variant",
        "variant_id": "var_1kmn0aExample",
        "quantity": 1
      }
    ]
  }'

With a sandbox key, from your backend.

Create a product in your sandbox, create an order for it, and pay it from the app with 4242 4242 4242 4242, any future expiry, and any CVC. The paid order appears in your dashboard. Going live means verifying your business, creating your products and webhook endpoints again in live mode, and switching your backend to a live key. When the app gets product ids from your backend, it needs no new build.

3.79% + 35¢ on cards · no monthly fee · free sandbox · no credit card

Then read these

  1. 01
    Embedded paymentsOrder read, pay, 3D Secure, and every error.
  2. 02
    Checkout sessionsThe hosted path, redirects, and confirming payment.
  3. 03
    Apple Pay and Google Pay setupWallets on hosted checkout and on your own pages.
  4. 04
    WebhooksFulfill from order.paid and verify each delivery.
  5. 05
    Node and PHP SDKsTyped server calls for your backend.
  6. 06
    Going liveFrom the sandbox to real payments.

Or run npm install -g @flintpay/cli && flint signup to create your account and a sandbox key from the terminal. CLI guide

Sources

Where each Apple and Stripe fact comes from.

  • Apps that unlock features or functionality, such as subscriptions, in-game currencies, game levels, access to premium content, or a full version, must use In-App Purchase. App Review Guidelines, 3.1.1 In-App Purchase

    developer.apple.com
  • Apps that let people buy physical goods or services consumed outside the app must use purchase methods other than In-App Purchase, such as Apple Pay or traditional credit card entry. App Review Guidelines, 3.1.3(e) Goods and Services Outside of the App

    developer.apple.com
  • In iOS, Safari and SFSafariViewController support Apple Pay. Apple Pay on the Web

    developer.apple.com
  • Stripe's iOS SDK provides STPPaymentCardTextField, STPAPIClient.createPaymentMethod, and STPPaymentHandler.handleNextAction for payments confirmed on a server. stripe-ios on GitHub

    github.com

last reviewed 2026-10-30 · corrections: Flint Help

FAQ

Questions iOS developers ask first.

Can I use Stripe in my iOS app with Flint?

Yes. Flint processes cards through Stripe, and your app keeps Stripe's iOS SDK for card entry. The app turns the card into a Stripe payment method with STPAPIClient.createPaymentMethod, using the publishable key and account your backend reads from the Flint order, and your backend pays the order with that payment method's id. Flint confirms the payment itself, so the app doesn't call confirmPayment or present PaymentSheet. Card numbers go from the app to Stripe and never reach your servers or Flint's.

Do I have to use Apple In-App Purchase?

Apple’s App Review Guidelines require In-App Purchase to unlock features or content inside the app (3.1.1). For physical goods and services used outside the app, such as products you ship or tickets to an event, the guidelines require a payment method other than In-App Purchase, such as Apple Pay or credit card entry (3.1.3(e)). Flint is for that second kind of sale. Apple reviews each app against its own guidelines, so check your app’s case against them.

Does Flint have an iOS SDK?

No. Your app uses Stripe’s iOS SDK to collect the card, and your backend calls the Flint API over HTTPS or with Flint’s Node or PHP server SDK. Every Flint API key is a secret key that stays on your server, so no Flint code or key ships in the app binary.

Can buyers pay with Apple Pay in my iOS app?

Yes, through Flint’s hosted checkout. Your backend creates a checkout session and your app opens its URL in SFSafariViewController, which supports Apple Pay. Apple Pay works on hosted checkout with no setup, because checkout runs on Flint’s domain: there’s no Apple merchant ID, certificate, or domain file for you to manage. Flint doesn’t offer Apple Pay through a native payment sheet inside the app. Apple Pay and Google Pay setup has the details.

How does 3D Secure work in an iOS app?

When the card’s bank asks the buyer to authenticate, the pay call returns an attempt with status requires_action and a pending action whose client_action.stripe carries a PaymentIntent client secret, a publishable key, and an account. Your backend passes those to the app, the app calls STPPaymentHandler’s handleNextAction with the client secret, and your backend then resumes the attempt with action: "resume". Flint reads the outcome from Stripe and returns the same response shape as the first pay call. Handle 3D Secure covers every outcome.

How does my app know the payment went through?

The pay call answers with the attempt’s status, so the app can show a confirmation when it reads succeeded. Fulfill from your backend, on the order.paid webhook or a fresh read of the order, because a buyer can close the app before it hears the result. On the hosted path, the app asks your backend when the buyer closes the Safari sheet.

Can hosted checkout send the buyer back into my app?

A hosted session’s success_url, where the buyer lands after paying, must be an absolute HTTPS URL, so custom URL schemes such as myapp:// aren’t accepted. Point it at an HTTPS page on your domain. In SFSafariViewController the buyer then taps Done, and your app’s safariViewControllerDidFinish handler asks your backend whether the order is paid.

Which businesses can use Flint?

Businesses based in the United States, charging in US dollars, for online payments. Buyers can pay with cards issued anywhere.

What does it cost?

Card payments are 3.79% + 35¢, with no monthly fee and no setup fee. That price includes the order, the hosted checkout, emailed receipts, refunds by line item, the buyer account, and the dashboard. Subscription payments add 0.40%, and automatic sales tax adds 0.50% on payments where Flint collects tax. The sandbox is free and needs no credit card. Pricing lists every fee.