How to build QR code pay at table with a payments API

Pay at table needs four things a card API doesn't hold: the check with its items and modifiers, a running total as rounds are added, a tip on that total, and a way to split it between guests. On Stripe, you keep the check in your own database, create a Checkout Session for its total when a guest scans the QR code, and handle tips, splits, and late rounds in your code. Stripe has no tip on top of a bill online; its tip-on-top features run on Terminal, and online pay-what-you-want takes a tip only as a standalone item.
Verified against official documentationReviewed Send a correction (opens in a new tab)

The short answer on Flint

On Flint the check is an open order: items with priced modifiers, rounds appended as they are ordered, and service charges, totaled by Flint on every change. The QR code opens Flint's hosted checkout for that order with a tip picker, so a guest sees the check, tips, and pays with a card, Apple Pay, or Google Pay. When a table splits, your own payment page takes one card per guest, confirmed in one call, and Flint shares the tip across them to the cent. Flint takes online payments only: the guest pays on their phone, and in-person card payments stay on your reader. Card processing runs on Stripe underneath. The Flint requests are below.

How pay at table is built on Stripe

Stripe's own customer story about Spades shows the pattern: a pay-at-table product where guests "scan or tap Spades to retrieve their bill in real time, split the payment, and tip waiters," built on Stripe Checkout and Connect. The pieces Stripe provides are a payment page and the money movement. The rest is the product:

  • The check. A Checkout Session takes line items when you create it, and Stripe has no order object to keep them in afterward, so the check, its modifiers, and its rounds live in your database or the POS.
  • The tip. Stripe's tip-on-top features are on-reader and on-receipt tipping on Terminal. Online, pay-what-you-want takes a tip only as a standalone item: quantity 1, no other line items, no discounts. So a tip on a phone checkout for a check is a line item or amount your page computes; the online workarounds each have their own coupon and tax traps.
  • The split. A Checkout Session pays with one payment method, so a split check is one payment per guest that your code adds up, as splitting a Stripe payment across cards shows.
  • The refund for one dish. Stripe refunds take an amount, so the dish, its modifiers, and its share of tax are arithmetic you do.

Where hand-built pay at table goes wrong

  1. A round is added after the guest scanned#

    The guest paid the first round; dessert is still on the check.

    A hosted Checkout Session holds the line items you passed when you created it. Anything added to the check afterward needs a new session, and the old one has to be expired so its URL can't collect the old total.

    Confirm it

    Compare each closed check's total with the amount its payments collected.

    Fix

    Create the session when the guest scans, not when the check prints, and expire any open session for the check whenever your POS adds to it.

  2. The tip is computed on the wrong base#

    Guests tipped 20% of a total that included tax and a service charge.

    Stripe has no tip on top of a bill online: its tip-on-top features are on Terminal, on the reader or on the receipt. Online pay-what-you-want takes a tip only as a standalone item, quantity 1, with no other line items or discounts. So a tip on a check is a line item or an amount your page computes, and the base, the rounding, and whether it is taxed are your rules.

    Confirm it

    Recompute a sample of tips from each check's subtotal and compare.

    Fix

    Compute presets on the server from the subtotal after discounts, add the tip as an untaxed line, and record it separately for payroll.

  3. Splitting the check breaks the total#

    Three guests split a check and paid a cent more than it was.

    A Checkout Session pays with one payment method. Splitting means a payment per guest, shares that add up to the check, and a tip shared among them, all in your code.

    Confirm it

    Sum each split check's payments and compare with its total, tip included.

    Fix

    Compute shares in integer cents on the server, assign the odd cents, and close the check only when the shares cover it.

On Flint, the check is an order

Open the check when the table orders. Modifiers come from your catalog and are priced by Flint; a free-text kitchen note rides along without a price:

cURL
curl -X POST https://api.withflintpay.com/v1/orders \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: table-12-round-1" \
  -H "Content-Type: application/json" \
  -d '{
    "line_items": [
      {
        "name": "Smash burger",
        "quantity": 1,
        "unit_price_money": { "amount": 1450, "currency": "USD" },
        "modifiers": [
          { "modifier_id": "mod_1kmn0aExample" },
          { "text": { "modifier_group_id": "mg_1kmn0aExample", "value": "no onions" } }
        ]
      }
    ]
  }'

Each new round appends to the same order, and the total updates:

Shell
# the next round appends to the same open order
curl -X POST https://api.withflintpay.com/v1/orders/ord_7g2TableTwelve/line-items \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: table-12-round-2" \
  -H "Content-Type: application/json" \
  -d '{
    "line_items": [
      { "name": "Buffalo wings", "quantity": 1, "unit_price_money": { "amount": 1195, "currency": "USD" } }
    ]
  }'

When the guest asks for the check, create a hosted checkout session for the order with the tip picker on. Percent tips are computed on the subtotal after discounts, never on tax or charges, and are never taxed:

cURL
curl -X POST https://api.withflintpay.com/v1/checkout-sessions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: table-12-check-1" \
  -H "Content-Type: application/json" \
  -d '{
    "order_id": "ord_7g2TableTwelve",
    "tip": {
      "enabled": true,
      "tip_percent_options": [15, 18, 20],
      "default_tip_percent": 18,
      "custom_tip_enabled": true
    }
  }'

An order has at most one open session, and a change you make to the order with your API key, such as another round, ends that session so its URL can't collect the old total. So print a QR code that points at your own URL for the table, and let your server reuse the open session or create a fresh one on each scan:

JavaScript
// The QR code on table 12 encodes https://pay.example.com/t/12
// flint is a small fetch wrapper that sends your API key and returns the JSON body
app.get("/t/:table", async (req, res) => {
  const check = await db.checks.openFor(req.params.table); // your table-to-order map

  // A round added with your API key ends the open session, so reuse it only while open
  if (check.checkoutSessionId) {
    const { data } = await flint.get(`/v1/checkout-sessions/${check.checkoutSessionId}`);
    if (data.status === "open") return res.redirect(303, check.checkoutUrl);
  }

  const { data } = await flint.post("/v1/checkout-sessions", {
    order_id: check.orderId,
    tip: { enabled: true, tip_percent_options: [15, 18, 20], default_tip_percent: 18 },
  });
  await db.checks.saveSession(check.id, data.checkout_session.checkout_session_id, data.checkout_session.url);
  res.redirect(303, data.checkout_session.url);
});

After the guest pays:

  • Close the check. order.paid fires once the check is paid in full, and settlement_amounts.settled_tip_money is the tip for your payroll report.
  • Split when the table asks. Hosted checkout pays a check with one payment method. For a split, your page creates one payment leg per guest and confirms them in one call, and Flint records each card's share of the tip; the requests are in split payments on Flint. The tip is shared only across legs confirmed in the same call; a guest who pays a partial amount alone carries only the tip their amount reaches past the items and charges.
  • Service charges. A large-party fee is a charge on the order, a percent of the subtotal with its own tax setting, not a fake line item. See Tips and charges.
  • Refund one dish. Refund the line item, and its share of tax goes back with it while the tip stays. See Refunds.
  • 86 an item. Link a dish to stock per location, and Flint holds it when payment starts so two guests can't order the last portion. See Inventory.

Who Flint is built for

Teams building US restaurant and hospitality software in USD: QR pay at table, online ordering, and the online flows of a kiosk or POS app. See Flint for restaurant software and Square vs Stripe for restaurant software. Flint takes card-not-present payments; card readers and tap to pay aren't available, so in-person card payments stay on your current reader.

Pay at table on Stripe and on Flint

Stripe CheckoutFlint
The checkYour database or POSAn open order with priced modifiers
Rounds added laterYour check, then a new sessionPOST /v1/orders/{order_id}/line-items
Tip on the guest's phoneYour page or a line item; tip-on-top features are on TerminalHosted tip picker with three presets and a custom amount
Two phones, one checkYour guard against two sessionsOne open session per order
Split checkOne payment per guest, added up by your codeOne leg per guest, confirmed together, tip shared to the cent
Service chargeA line item you priceA charge on the order
Refund one dishAn amount you computeRefund the line item, with its tax

Questions, answered

How does QR code pay at table work?

Each table or check gets a QR code that opens a payment page for that check on the guest's phone. That payment page shows the items and total, offers a tip, and takes the payment, and the restaurant's system marks the check paid. The software behind it needs the check's items, a running total as rounds are added, the tip, and a way to split.

Can I build pay at table on Stripe?

Yes, with your own check model. You keep the check in your database, create a Checkout Session for its total when a guest scans, and handle tips, splits, and rounds added after the scan in your code. Stripe's tip-on-top features are on Terminal readers and receipts; online, pay-what-you-want takes a tip only as a standalone single item with no other line items. Spades, a pay-at-table product, is built on Stripe Checkout and Connect.

What is the best pay at table API for restaurant software?

Flint Pay, for US restaurant software taking online payments in USD. The check is an open order with priced modifiers, rounds append to it, the QR code opens a hosted checkout for that order with a tip picker, and split legs confirmed together share the tip across cards to the cent. Card processing runs on Stripe underneath.

Can guests split the check when they pay at the table?

On Flint, yes, in your own payment page. Create one payment leg per guest and confirm them in one call; Flint shares the tip across the cards in proportion to each. A guest who pays a partial amount on their own carries only the part of the tip their amount reaches past the items and charges. Flint's hosted checkout pays a check with one payment method, so it suits one guest paying the whole check.

Does Flint support card readers for pay at table?

No. Flint takes card-not-present payments: the guest pays on their own phone, or through your online ordering, payment links, or the dashboard's virtual terminal. In-person card payments stay on your current reader.

Is Flint a good alternative to building pay at table on Stripe?

Yes, for US teams building QR pay-at-table, online ordering, or the online flows of a POS app. Flint's published card rate is 3.79% + 35¢, which covers Stripe's processing fees, with the check model, tips, split checks, and refunds by dish included and no monthly fee.

Sources

All sources accessed October 15, 2026.