Checkout
The hosted checkout flow generates a GullyPay-hosted payment page. Your server creates a session for a
store, you send the customer to the returned checkoutUrl, and GullyPay tells your server when the
customer has paid with a signed checkout.session.completed webhook.
POST /checkout/hosted/:storeId needs one of the credentials below. Reading a session
(GET /checkout/:sessionId) and paying it stay public: the session id in the customer's link is the
only thing the hosted page needs.
Endpoints
/checkout/hosted/:storeIdCreate a hosted checkout session (API key, dashboard session or checkout token)/checkout/tokens/:storeIdMint a short-lived, single-use checkout token (API key or dashboard session)/checkout/storefront/:slugPublic storefront checkout, priced by the server from product ids/checkout/demoPublic demo session (test mode, nothing is stored or charged)/checkout/:sessionIdRetrieve checkout session data (incl. crypto offer)/checkout/:sessionId/payment-intentCreate the card PaymentIntent for a session/checkout/:sessionId/completeComplete a checkout session against a settled payment/checkout/:sessionId/crypto/settleVerify an on-chain stablecoin payment and settle/checkout/:sessionId/crypto/statusPoll the crypto payment stateAuthentication
Send exactly one of these with POST /checkout/hosted/:storeId:
| Credential | Header | Use it from |
|---|---|---|
| Store API key | x-api-key: rk_… or Authorization: Bearer rk_… | Your server. The key's owner must be a member of the store's merchant. A key with scopes must include checkout:write (a key with no scopes is unrestricted). A key bound to a merchant only works for that merchant's stores. |
| Dashboard session | Authorization: Bearer <JWT> | The GullyPay dashboard (the merchant cart uses this). |
| Checkout token | X-Checkout-Token: ct_… (or Authorization: Bearer ct_…) | Your own browser or app, without exposing the API key. |
Missing credentials return 401; a credential for another merchant or store returns 403.
Session creation is rate limited to 30 requests per minute per client IP.
Checkout tokens
/checkout/tokens/:storeIdMint a checkout token (API key or dashboard session only)Your server mints a token, hands it to your frontend, and the frontend creates the session. A token:
- is bound to one store, and optionally to an exact
amountandcurrency(the session must then use exactly those values, otherwise400); - expires after
ttlSeconds(60 to 900, default 600); - creates one session only (a second use returns
409); - cannot mint further tokens.
Create a Hosted Session
/checkout/hosted/:storeIdCreate a hosted checkout session| Field | Type | Required | Description |
|---|---|---|---|
amount | integer | Yes | Amount in cents (1 to 100,000,000) |
currency | string | Yes | Three-letter ISO currency code |
successUrl | string | No | Redirect URL after successful payment (http/https) |
cancelUrl | string | No | Redirect URL if the customer cancels |
customerEmail | string | No | Where the payment receipt is emailed |
clientReferenceId | string | No | Your order id; echoed back in every checkout.session.* webhook |
Redirect the customer to checkoutUrl. Sessions accept payment for 30 minutes.
Links have the form https://checkout.gullypay.xyz/<sessionId>. The same page is also served at
https://gullypay.xyz/checkout/<sessionId>; until the checkout host is switched on for an environment,
checkoutUrl uses that form.
Storefront Checkout (public)
/checkout/storefront/:slugCreate a session for a public storefront cartUsed by GullyPay storefront pages and the consumer app. No credentials: the body names products and quantities only, and the server prices the cart from the store's live catalog (a product's chosen variant, or its first variant), so a shopper can never choose the amount. Rate limited to 10 requests per minute per client IP.
The response has the same shape as the hosted session response. The priced line items are stored on
the session and included in its webhooks as lineItems.
Retrieve Session Data
/checkout/:sessionIdRetrieve checkout session dataReturns the session's amount, currency, status, store details, and supported payment methods. The hosted checkout page calls this to render itself.
When at least one crypto settlement address is configured on the platform, the response also includes
a crypto object describing exactly how the customer can pay with stablecoins. Chains whose
settlement address or token is not configured are omitted (never a placeholder) — so the array
reflects real, payable options only.
| Field | Type | Description |
|---|---|---|
fiatAmountCents | number | The order total in cents (equals session.amount) |
currency | string | Session/store currency (e.g. USD) |
chains[].id | string | base | ethereum | bsc | solana | gully-chain-testnet |
chains[].chainId | number | null | EVM numeric chain id; null for Solana |
chains[].token | string | Stablecoin symbol (USDC; GLUSD on Gully Chain when only glUSD is configured) |
chains[].tokenAddress | string | ERC-20 contract (0x…) or SPL mint (base58) |
chains[].decimals | number | Token decimals — 6 on Base/Ethereum/Solana, 18 on BSC |
chains[].amountBaseUnits | string | Exact amount to send in the token's base units (BigInt-safe: round(cents/100 · 10^decimals)) |
chains[].settlementAddress | string | Where the customer must send funds (EVM 0x… or Solana base58) |
chains[].label / sub | string | Display label and latency hint |
Gully Chain Testnet (gully-chain-testnet, chain id 485590, Gully Labs' L3) is offered only while the API
runs with GULLY_CHAIN_ENABLED=true and a stablecoin contract is configured for it:
USDC_TOKEN_GULLY_CHAIN_TESTNET (then token is USDC), or else GLUSD_TOKEN_GULLY_CHAIN_TESTNET
(then token is GLUSD, label glUSD · Gully Chain). Decimals default to 6 and can be set with
USDC_DECIMALS_GULLY_CHAIN_TESTNET / GLUSD_DECIMALS_GULLY_CHAIN_TESTNET. Until a contract is set the
chain is left out of chains. Settlement is verified on chain 485590 over GULLY_CHAIN_RPC_URL
(the public testnet RPC when unset) exactly like the other EVM chains.
The stablecoin is assumed ≈ 1 USD. Send at least amountBaseUnits of token to settlementAddress
on the chosen chain, then call the settle endpoint below with the resulting transaction hash.
Demo Checkout Session
/checkout/demoPublic demo session (test mode)Public, no authentication. Returns a static, test-mode session for the demo page at
gullypay.xyz/checkout/demo. It never touches the database or a
payment provider, so nothing can be paid or stored. The shape matches GET /checkout/:sessionId plus
"demo": true and a demoCrypto object that shows a Gully Chain Testnet (chain id 485590) payment request.
Rate limited to 60 requests per minute per IP.
Session ids are UUIDs. A :sessionId that is not a UUID (for example a typo or a truncated id) returns
404 Not Found, never a 500.
Settle a Crypto Payment
/checkout/:sessionId/crypto/settleVerify an on-chain stablecoin payment and settleSubmits the on-chain proof of payment. The server recomputes the expected amount, settlement address, and token for the chosen chain (it never trusts a client-supplied amount), then verifies the transaction on-chain via the GullyPay facilitator's verifier: the transfer must have succeeded, have enough confirmations, and credit the settlement address with at least the required amount. Only then is the session marked paid and a real transaction recorded.
| Field | Type | Required | Description |
|---|---|---|---|
chain | string | Yes | base | ethereum | bsc | solana | gully-chain-testnet |
token | string | Yes | Stablecoin symbol, e.g. USDC |
txHash | string | Yes | EVM transaction hash (0x…, 32-byte) or base58 Solana signature |
payerAddress | string | No | The wallet that paid (recorded for reconciliation) |
email | string | No | Where to email the receipt once the payment is verified |
status is paid on success or failed (with a reason) when on-chain verification does not pass —
no receipt is issued without verification. Settlement is idempotent per txHash: replaying the same
proof for the same session returns paid; a txHash already used to settle a different order is rejected.
A verified transfer is accepted up to 24 hours after the session expired, so a customer who paid late is
never left with funds that match no order. Rate limited to 20 requests per minute per client IP.
Amounts and settlement addresses are always recomputed server-side and confirmed against the chain.
A failed response means the payment was not found on-chain for the required amount to the settlement
address — it is never a receipt.
Poll Crypto Payment Status
/checkout/:sessionId/crypto/statusPoll the crypto payment stateReturns the real session/transaction state so the checkout page (e.g. a QR payer) can poll while waiting for settlement.
| Field | Type | Description |
|---|---|---|
status | string | pending (awaiting settlement) | paid (verified & completed) | expired |
txHash | string | Present once paid — the settling transaction |
Status reflects database state only. Settlement is driven by the settle endpoint — the checkout page
submits the wallet transaction (or lets the payer paste a tx hash). GullyPay does not scan chains in the
background for this endpoint, so a payment is reflected as paid only after POST /crypto/settle
verifies it.
Complete a Session
/checkout/:sessionId/completeComplete a checkout session| Field | Type | Required | Description |
|---|---|---|---|
paymentMethod | string | Yes | The selected method, e.g. card, apple_pay, google_pay, usdc, glusd |
Completion never creates a payment: it only finalizes the session against a payment that has already
settled (a card PaymentIntent confirmed by Stripe, or a verified crypto settlement). Without one it
returns 400. The response carries the successUrl to redirect the customer to. Card payments are also
completed server-side when Stripe reports payment_intent.succeeded, so a customer who closes the tab
after paying still gets a completed session.
Session Events (webhooks)
Every session change is pushed to your webhook endpoints as a signed event. Subscribe to
these event types in the Developer Dashboard (or with *):
| Event | When |
|---|---|
checkout.session.completed | The session was paid: card (Stripe confirmed) or crypto (verified on-chain). Sent once per session. |
checkout.session.expired | The session passed its 30-minute window unpaid. Sent once per session, within about a minute of expiry. |
checkout.session.payment_failed | Stripe reported a failed card attempt. The session stays open, so the customer can retry. |
Fulfil orders on checkout.session.completed only. A crypto transfer that verifies on-chain is still
accepted for up to 24 hours after the session expired (the customer has paid), so a completed event
can follow an expired one for the same session: treat completed as final.
See Webhooks: checkout events for the payload, signature check and retry policy.
When the session has a customer email (from customerEmail, the card form, or the crypto settle
email field), GullyPay emails the customer a receipt once the session completes.
The Go, Python, Dart, Swift and Kotlin SDKs create sessions with POST /checkout/hosted/:storeId using the
client's API key. See SDKs.