Offer express checkout

Let shoppers pay with Apple Pay or Google Pay from the product and cart pages, through a reviewed Tringify adapter.

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#

  1. 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.

  2. 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.

  3. 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.

  4. Tringify creates the order and opens a payment session, then sends your charge endpoint the usual charge request with an express block carrying the token.

  5. 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.

  6. 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-declaration.json json
{
  "express": {
    "adapter": "fakepay_express",
    "wallets": [
      "apple_pay",
      "google_pay"
    ],
    "currencies": [
      "AUD",
      "USD"
    ]
  },
  "operations": [
    "charge",
    "refund"
  ],
  "service_type": "payment"
}

Field

Rule

adapter

A reviewed Tringify adapter for your provider (below). Adapters are added by Tringify after review; ask for one before you build.

wallets

The wallets you offer, a non-empty subset of the adapter's: apple_pay, google_pay. Shoppers see Tringify's labels for them.

currencies

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

fakepay_express

apple_pay, google_pay

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

https://fakepay.tringify.store/js/express.js

stripe_express

apple_pay, google_pay, link, paypal, amazon_pay, klarna

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

https://js.stripe.com/v3/

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.

connection-check-response.json json
{
  "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_…"
  }
}
  1. Never a secret: unknown fields, secret keys, scripts and URLs make Tringify reject the whole connection check. The limit is 2048 bytes.

  2. It is kept only with status ready, and refreshed on every connection check.

  3. express_config from an app whose declaration has no express is rejected (express_not_declared).

  4. 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.

express-charge-request.json json
{
  "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, express_status paid.

Accepted, settles later

HTTP 200, express_status processing.

Needs 3-D Secure

HTTP 200, express_status requires_action with launch_url: the provider's HTTPS authentication page, which returns the shopper to return_url.

Declined; token invalid, used or expired; amount mismatch

A definite 4xx with success: false, error_code and error_message, never HTTP 200. Nothing was charged; the shopper pays the same order at checkout.

express-charge-paid.json json
{
  "success": true,
  "provider_session_id": "pi_…",
  "express_status": "paid"
}
express-charge-requires-action.json json
{
  "success": true,
  "launch_url": "https://fakepay.tringify.store/authenticate/pi_…",
  "provider_session_id": "pi_…",
  "express_status": "requires_action"
}
express-charge-declined.json json
{
  "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.

  1. Declaration: express with adapter fakepay_express, wallets apple_pay and google_pay, currencies AED, AUD, INR, USD.

  2. 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.

  3. Storefront: Tringify's fakepay_express adapter loads https://fakepay.tringify.store/js/express.js with that publishable key; the Fake Pay wallet sheet returns a ctoken_… confirmation token approved for the sheet's amount.

  4. Charge: the app creates and confirms a PaymentIntent, POST /v1/payment_intents with amount (minor units of the request amount), currency, confirm=true, confirmation_token, return_url and metadata naming the session and order, sent with Idempotency-Key set to the session_id.

  5. Answer: succeeded → paid; processing → processing; requires_action → requires_action with the provider authentication URL. A definite refusal returns 422 with FAKEPAY_CHARGE_REFUSED; reading a failed express attempt returns FAKEPAY_PAYMENT_FAILED. Unknown outcomes remain retryable 503.

  6. 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#

  1. Connect a test account and confirm the connection check returns express_config in test mode; remove the key and confirm the merchant sees the provider as not ready and no buttons show.

  2. Pay from a product page and from a cart page; confirm Tringify records every capture from the live status answer exactly once.

  3. 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.

  4. Decline the token; confirm you answer a 4xx, nothing is charged, and the shopper can pay the same order at checkout.

  5. Send the same express charge twice with one session_id; confirm one provider payment exists and the second answer matches the first.

  6. 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.

  7. Search your logs for ctoken_: there must be no match.