Express checkout puts wallet buttons, such as Apple Pay and Google Pay, on a store's product and cart pages. The shopper taps a button, picks an address and a delivery option in the wallet sheet, confirms, and is charged; the order is created without the checkout page. A payment app offers it by declaring it. The same signed charge request starts the attempt, and the platform reads the provider through the app’s status endpoint to decide whether the order is paid.
How a purchase flows#
The page shows the buttons when the store's Show express buttons switch is on, a ready connection of your app is enabled for the shopper's market, its currency is in your declaration, and the purchase is simple (see When buttons do not show). The shopper's browser then checks that the device has the wallet.
The shopper taps a button. The adapter opens the wallet sheet with the total Tringify priced. As the shopper picks an address (the wallet shares only country, region, city and postal code at this stage) and a delivery option, Tringify re-prices and the sheet updates. Nothing is stored and no order exists yet.
The shopper confirms. The wallet gives the adapter a one-time confirmation token approved for the sheet's exact amount. Tringify re-prices with the full address and re-checks stock. If the total is not exactly what the sheet showed, nothing is charged: the sheet fails and the shopper continues at checkout with their details filled in.
Tringify creates the order and opens a payment session, then sends your charge endpoint the usual charge request with an
expressblock carrying the token.Your app charges the token for exactly the session amount and answers with
express_status. The shopper goes to the order page, or to your provider's authentication page first for 3-D Secure.Tringify asks your status endpoint for the live provider outcome. A paid answer lists every capture with its transaction ID, exact amount, currency and captured_at. A verified provider notification only sends an empty wake for that attempt.
Declare express#
Add express to the payment declaration of your app's draft. It needs the charge operation, is frozen with the release like the rest of the declaration, and can be changed only by a new release. Without it, your app never receives an express charge and its connections never show buttons.
{
"express": {
"adapter": "fakepay_express",
"wallets": [
"apple_pay",
"google_pay"
],
"currencies": [
"AUD",
"USD"
]
},
"operations": [
"charge",
"refund"
],
"service_type": "payment"
}
Field |
Rule |
|---|---|
|
A reviewed Tringify adapter for your provider (below). Adapters are added by Tringify after review; ask for one before you build. |
|
The wallets you offer, a non-empty subset of the adapter's: |
|
Upper-case ISO 4217 codes your provider's wallet sheet accepts, a non-empty subset of the adapter's. A market priced in another currency shows no buttons. |
Saving a declaration refuses: express on a non-payment declaration (not_supported), express without charge (charge_required), an unknown express.adapter (unknown), a wallet the adapter cannot show (express.wallets unsupported), a currency that is not an ISO code or that the adapter does not accept (express.currencies invalid_currency, unsupported_by_adapter), and empty wallets or currencies (required).
Reviewed adapters#
Adapter |
Wallets |
Currencies |
Totals it charges |
Provider script |
|---|---|---|---|---|
|
|
AED, AUD, EUR, HKD, INR, USD |
AED 2.00–999999.99; AUD 0.50–999999.99; EUR 0.50–999999.99; HKD 4.00–999999.99; INR 1.00–999999.99; USD 0.50–999999.99 |
|
|
|
AED, AFN, ALL, AMD, ANG, AOA, ARS, AUD, AWG, AZN, BAM, BBD, BDT, BGN, BIF, BMD, BND, BOB, BRL, BSD, BWP, BYN, BZD, CAD, CDF, CHF, CLP, CNY, COP, CRC, CVE, CZK, DJF, DKK, DOP, DZD, EGP, ETB, EUR, FJD, FKP, GBP, GEL, GIP, GMD, GNF, GTQ, GYD, HKD, HNL, HTG, HUF, IDR, ILS, INR, JMD, JPY, KES, KGS, KHR, KMF, KRW, KYD, KZT, LAK, LBP, LKR, LRD, LSL, MAD, MDL, MGA, MKD, MMK, MNT, MOP, MUR, MVR, MWK, MXN, MYR, MZN, NAD, NGN, NIO, NOK, NPR, NZD, PAB, PEN, PGK, PHP, PKR, PLN, PYG, QAR, RON, RSD, SAR, SBD, SCR, SEK, SGD, SHP, SOS, SRD, SZL, THB, TJS, TOP, TRY, TTD, TWD, TZS, UAH, USD, UYU, UZS, VND, VUV, WST, XAF, XCD, XOF, XPF, YER, ZAR, ZMW |
AED 2.00–999999.99; AFN 0.00–999999.99; ALL 0.00–999999.99; AMD 0.00–999999.99; ANG 0.00–999999.99; AOA 0.00–999999.99; ARS 0.00–999999.99; AUD 0.50–999999.99; AWG 0.00–999999.99; AZN 0.00–999999.99; BAM 0.00–999999.99; BBD 0.00–999999.99; BDT 0.00–999999.99; BGN 1.00–999999.99; BIF 0–99999999; BMD 0.00–999999.99; BND 0.00–999999.99; BOB 0.00–999999.99; BRL 0.50–999999.99; BSD 0.00–999999.99; BWP 0.00–999999.99; BYN 0.00–999999.99; BZD 0.00–999999.99; CAD 0.50–999999.99; CDF 0.00–999999.99; CHF 0.50–999999.99; CLP 0–99999999; CNY 0.00–999999.99; COP 0.00–999999.99; CRC 0.00–999999.99; CVE 0.00–999999.99; CZK 15.00–999999.99; DJF 0–99999999; DKK 2.50–999999.99; DOP 0.00–999999.99; DZD 0.00–999999.99; EGP 0.00–999999.99; ETB 0.00–999999.99; EUR 0.50–999999.99; FJD 0.00–999999.99; FKP 0.00–999999.99; GBP 0.30–999999.99; GEL 0.00–999999.99; GIP 0.00–999999.99; GMD 0.00–999999.99; GNF 0–99999999; GTQ 0.00–999999.99; GYD 0.00–999999.99; HKD 4.00–999999.99; HNL 0.00–999999.99; HTG 0.00–999999.99; HUF 175.00–999999.99; IDR 0.00–999999.99; ILS 0.00–999999.99; INR 0.50–999999.99; JMD 0.00–999999.99; JPY 50–99999999; KES 0.00–999999.99; KGS 0.00–999999.99; KHR 0.00–999999.99; KMF 0–99999999; KRW 0–99999999; KYD 0.00–999999.99; KZT 0.00–999999.99; LAK 0.00–999999.99; LBP 0.00–999999.99; LKR 0.00–999999.99; LRD 0.00–999999.99; LSL 0.00–999999.99; MAD 0.00–999999.99; MDL 0.00–999999.99; MGA 0–99999999; MKD 0.00–999999.99; MMK 0.00–999999.99; MNT 0.00–999999.99; MOP 0.00–999999.99; MUR 0.00–999999.99; MVR 0.00–999999.99; MWK 0.00–999999.99; MXN 10.00–999999.99; MYR 2.00–999999.99; MZN 0.00–999999.99; NAD 0.00–999999.99; NGN 0.00–999999.99; NIO 0.00–999999.99; NOK 3.00–999999.99; NPR 0.00–999999.99; NZD 0.50–999999.99; PAB 0.00–999999.99; PEN 0.00–999999.99; PGK 0.00–999999.99; PHP 0.00–999999.99; PKR 0.00–999999.99; PLN 2.00–999999.99; PYG 0–99999999; QAR 0.00–999999.99; RON 2.00–999999.99; RSD 0.00–999999.99; SAR 0.00–999999.99; SBD 0.00–999999.99; SCR 0.00–999999.99; SEK 3.00–999999.99; SGD 0.50–999999.99; SHP 0.00–999999.99; SOS 0.00–999999.99; SRD 0.00–999999.99; SZL 0.00–999999.99; THB 10.00–999999.99; TJS 0.00–999999.99; TOP 0.00–999999.99; TRY 0.00–999999.99; TTD 0.00–999999.99; TWD 0.00–999999.99; TZS 0.00–999999.99; UAH 0.00–999999.99; USD 0.50–999999.99; UYU 0.00–999999.99; UZS 0.00–999999.99; VND 0–99999999; VUV 0–99999999; WST 0.00–999999.99; XAF 0–99999999; XCD 0.00–999999.99; XOF 0–99999999; XPF 0–99999999; YER 0.00–999999.99; ZAR 0.00–999999.99; ZMW 0.00–999999.99 |
|
A purchase whose total is outside the adapter's limits shows no buttons. Wallet sheet amounts are whole minor units; Tringify converts the exact order total and never rounds.
Report the public configuration#
For each ready connection, add express_config to the connection check response: the public configuration the adapter needs for that merchant account. Its shape belongs to the adapter. fakepay_express takes exactly {"publishable_key": "pk_test_…"} (or pk_live_…), and the key's mode must match the reported mode.
{
"status": "ready",
"mode": "test",
"health": "healthy",
"account_country": "AU",
"account_hint": "sk_test_…4242",
"account_name": "Acme Goods",
"presentment_currencies": [
"AUD",
"USD"
],
"express_config": {
"publishable_key": "pk_test_…"
}
}
Never a secret: unknown fields, secret keys, scripts and URLs make Tringify reject the whole connection check. The limit is 2048 bytes.
It is kept only with
statusready, and refreshed on every connection check.express_configfrom an app whose declaration has noexpressis rejected (express_not_declared).Leave it out and the connection keeps taking normal payments; express buttons stay off for it and the merchant reads that the payment provider has not finished setting up express checkout.
Charge the wallet's token#
An express charge is the signed request to your declared charge endpoint described in Accept a payment, with the same session_id idempotency, the same amount and currency_code (the order's outstanding total as an exact decimal string) and the same return_url (the order page). It adds express. shipping_address is absent when nothing ships; billing_address is the wallet's billing identity when the wallet gave one, else the shipping address.
{
"session_id": "session_…",
"protocol_version": "1",
"declaration_revision_id": "revision_…",
"store_id": "store_…",
"binding_id": "binding_…",
"connection_key": "default",
"market_id": "market_…",
"store_name": "Acme Goods",
"order_id": "order_…",
"order_number": "1042",
"amount": "64.90",
"currency_code": "AUD",
"test": false,
"customer_email": "buyer@example.com",
"return_url": "https://shop.example/checkout/return",
"expires_at": "2026-07-13T12:30:00Z",
"express": {
"adapter": "fakepay_express",
"wallet": "google_pay",
"confirmation_token": "ctoken_…",
"payer_name": "Alex Demo",
"payer_email": "alex@example.com",
"payer_phone": "+61400000000",
"shipping_address": {
"first_name": "Alex",
"last_name": "Demo",
"address1": "1 Demo Street",
"city": "Sydney",
"state": "NSW",
"postal_code": "2000",
"country": "AU",
"phone": "+61400000000"
}
}
}
Charge confirmation_token for exactly amount in currency_code, using session_id as the provider's idempotency key, on the connection named by connection_key. Never log the token and never keep it after the provider answered. Then answer:
Provider result |
Answer |
|---|---|
Captured |
HTTP 200, |
Accepted, settles later |
HTTP 200, |
Needs 3-D Secure |
HTTP 200, |
Declined; token invalid, used or expired; amount mismatch |
A definite 4xx with |
{
"success": true,
"provider_session_id": "pi_…",
"express_status": "paid"
}
{
"success": true,
"launch_url": "https://fakepay.tringify.store/authenticate/pi_…",
"provider_session_id": "pi_…",
"express_status": "requires_action"
}
{
"success": false,
"error_code": "card_declined",
"error_message": "The card was declined."
}
A success carries exactly one express_status; launch_url only and always with requires_action; never browser_action. Tringify fails the session on any other shape. A normal charge to the same app (no express block) is answered with one browser handoff, as before.
Read the outcome#
express_status controls the browser handoff. Tringify reads status to record money: pending, paid with every captured payment, or failed with a reason. The app keeps no payment or refund outcome records and sends no result callbacks. Provider notifications wake the named attempt; scheduled platform checks provide correctness when notifications are missing. If a charge reply is lost, read the attempt at the provider before retrying creation with the same idempotency key. Late captures are still returned by status.
When buttons do not show#
Tringify decides on every page and tells the merchant and the theme why (ctx.express.reason). The reasons that concern your app: no enabled, ready connection of an express app for the market (EXPRESS_REASON_NO_PROVIDER), a connection without express_config (EXPRESS_REASON_PROVIDER_SETUP), the market's currency not in your currencies (EXPRESS_REASON_CURRENCY), and a total outside the adapter's limits (EXPRESS_REASON_ORDER_LIMIT). The rest are store and purchase rules: the switch is off, checkout is unavailable, a required checkout field, sign-in required, pickup, an empty cart, a discount code, a gift card, a personalised or sold-out item. Only automatic discounts apply in the sheet. The merchant sees each express provider, its wallets, markets and readiness under Online Store → Preferences → Express checkout.
Worked example: Fake Pay#
Fake Pay is a demo Stripe-like provider; no real money moves. Its Tringify app is the model for real wallet providers.
Declaration:
expresswith adapterfakepay_express, walletsapple_payandgoogle_pay, currenciesAED,AUD,INR,USD.Setup: the merchant enters the Fake Pay secret key and publishable key in the app's page. The connection check returns
express_config{"publishable_key": "pk_test_…"}in the key's mode.Storefront: Tringify's
fakepay_expressadapter loadshttps://fakepay.tringify.store/js/express.jswith that publishable key; the Fake Pay wallet sheet returns actoken_…confirmation token approved for the sheet's amount.Charge: the app creates and confirms a PaymentIntent,
POST /v1/payment_intentswithamount(minor units of the requestamount),currency,confirm=true,confirmation_token,return_urland metadata naming the session and order, sent withIdempotency-Keyset to thesession_id.Answer:
succeeded→paid;processing→processing;requires_action→requires_actionwith the provider authentication URL. A definite refusal returns 422 withFAKEPAY_CHARGE_REFUSED; reading a failed express attempt returnsFAKEPAY_PAYMENT_FAILED. Unknown outcomes remain retryable 503.Result: Tringify calls the app’s status endpoint. The app reads the PaymentIntent live and returns its actual capture timestamp, transaction ID, exact amount and currency. A verified Fake Pay notification sends only an empty-body wake. There are no app-side payment records, reports or check crons.
Fake Pay declares all five required operations: charge, status, cancel, refund and refund_status. Cancel answers the same status shape, including paid when already captured. Refund creation uses refund_id as a durable provider reference; refund_status reads it live. A repeat beyond the provider’s 24-hour HTTP cache returns that refund’s current outcome. Disconnecting or replacing credentials stops new charges on that account while retained credentials continue serving historical status and refunds until store-data erasure. The embedded app no longer has a payment-report attention list; payment failures belong to Tringify’s normal order and refund handling.
Test express checkout#
Connect a test account and confirm the connection check returns
express_configin test mode; remove the key and confirm the merchant sees the provider as not ready and no buttons show.Pay from a product page and from a cart page; confirm Tringify records every capture from the live status answer exactly once.
Pay with an amount that needs 3-D Secure; confirm the shopper returns to the order page and the order becomes paid after the platform reads status, including when the provider notification is missing.
Decline the token; confirm you answer a 4xx, nothing is charged, and the shopper can pay the same order at checkout.
Send the same express charge twice with one
session_id; confirm one provider payment exists and the second answer matches the first.Leave the charge answer unsent past the time limit; confirm a repeat finds the provider attempt, platform checks learn the outcome, and no second payment is created.
Search your logs for
ctoken_: there must be no match.