Docs
Dashboard

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.

Creating a session requires authentication

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

POST/checkout/hosted/:storeIdCreate a hosted checkout session (API key, dashboard session or checkout token)
POST/checkout/tokens/:storeIdMint a short-lived, single-use checkout token (API key or dashboard session)
POST/checkout/storefront/:slugPublic storefront checkout, priced by the server from product ids
GET/checkout/demoPublic demo session (test mode, nothing is stored or charged)
GET/checkout/:sessionIdRetrieve checkout session data (incl. crypto offer)
POST/checkout/:sessionId/payment-intentCreate the card PaymentIntent for a session
POST/checkout/:sessionId/completeComplete a checkout session against a settled payment
POST/checkout/:sessionId/crypto/settleVerify an on-chain stablecoin payment and settle
GET/checkout/:sessionId/crypto/statusPoll the crypto payment state

Authentication

Send exactly one of these with POST /checkout/hosted/:storeId:

CredentialHeaderUse it from
Store API keyx-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 sessionAuthorization: Bearer <JWT>The GullyPay dashboard (the merchant cart uses this).
Checkout tokenX-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

POST/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 amount and currency (the session must then use exactly those values, otherwise 400);
  • expires after ttlSeconds (60 to 900, default 600);
  • creates one session only (a second use returns 409);
  • cannot mint further tokens.
Mint (server) then create (browser)
# 1. Server side, with your API key
curl -X POST https://gullypay.xyz/api/checkout/tokens/$STORE_ID \
-H "x-api-key: rk_live_..." -H "Content-Type: application/json" \
-d '{"amount": 4999, "currency": "USD", "clientReferenceId": "order_1042"}'
# → { "token": "ct_...", "expiresAt": "...", "storeId": "...", "amount": 4999, "currency": "USD" }

# 2. Browser side, with the token only
curl -X POST https://gullypay.xyz/api/checkout/hosted/$STORE_ID \
-H "X-Checkout-Token: ct_..." -H "Content-Type: application/json" \
-d '{"amount": 4999, "currency": "USD", "successUrl": "https://yoursite.com/thanks"}'

Create a Hosted Session

POST/checkout/hosted/:storeIdCreate a hosted checkout session
Request Body
{
"amount": 4999,
"currency": "USD",
"successUrl": "https://yoursite.com/success",
"cancelUrl": "https://yoursite.com/cancel",
"customerEmail": "[email protected]",
"clientReferenceId": "order_1042"
}
FieldTypeRequiredDescription
amountintegerYesAmount in cents (1 to 100,000,000)
currencystringYesThree-letter ISO currency code
successUrlstringNoRedirect URL after successful payment (http/https)
cancelUrlstringNoRedirect URL if the customer cancels
customerEmailstringNoWhere the payment receipt is emailed
clientReferenceIdstringNoYour order id; echoed back in every checkout.session.* webhook
Response
{
"sessionId": "3f2b8c1e-9a4d-4e7b-8c21-0d5e6f7a8b9c",
"checkoutUrl": "https://checkout.gullypay.xyz/3f2b8c1e-9a4d-4e7b-8c21-0d5e6f7a8b9c",
"expiresAt": "2026-10-25T13:00:00.000Z"
}

Redirect the customer to checkoutUrl. Sessions accept payment for 30 minutes.

Checkout links

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)

POST/checkout/storefront/:slugCreate a session for a public storefront cart

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

Request Body
{
"items": [
  { "productId": "8c0f…", "quantity": 2 },
  { "productId": "51aa…", "variantId": "a7d2…", "quantity": 1 }
],
"customerEmail": "[email protected]"
}

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

GET/checkout/:sessionIdRetrieve checkout session data

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

Response (crypto portion)
{
"expired": false,
"session": { "id": "cs_abc123", "amount": 4999, "currency": "USD", "status": "open" },
"store": { "name": "Acme", "currency": "USD" },
"paymentMethods": ["card", "apple_pay", "google_pay", "usdc", "usdt", "glusd"],
"crypto": {
  "fiatAmountCents": 4999,
  "currency": "USD",
  "chains": [
    {
      "id": "base",
      "chainId": 8453,
      "token": "USDC",
      "tokenAddress": "0x833589fcd6edb6e08f4c7c32d4f71b54bda02913",
      "decimals": 6,
      "amountBaseUnits": "49990000",
      "settlementAddress": "0x…",
      "label": "USDC · Base",
      "sub": "~2s"
    }
    // ethereum (chainId 1), bsc (chainId 56, 18 decimals), solana (chainId null, base58 address),
    // gully-chain-testnet (chainId 485590, when enabled and a stablecoin is configured)…
  ]
}
}
FieldTypeDescription
fiatAmountCentsnumberThe order total in cents (equals session.amount)
currencystringSession/store currency (e.g. USD)
chains[].idstringbase | ethereum | bsc | solana | gully-chain-testnet
chains[].chainIdnumber | nullEVM numeric chain id; null for Solana
chains[].tokenstringStablecoin symbol (USDC; GLUSD on Gully Chain when only glUSD is configured)
chains[].tokenAddressstringERC-20 contract (0x…) or SPL mint (base58)
chains[].decimalsnumberToken decimals — 6 on Base/Ethereum/Solana, 18 on BSC
chains[].amountBaseUnitsstringExact amount to send in the token's base units (BigInt-safe: round(cents/100 · 10^decimals))
chains[].settlementAddressstringWhere the customer must send funds (EVM 0x… or Solana base58)
chains[].label / substringDisplay 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

GET/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.

Response (abridged)
{
"demo": true,
"expired": false,
"session": {
  "id": "demo",
  "status": "open",
  "amount": 6900,
  "currency": "USD",
  "livemode": false,
  "metadata": { "lineItems": [{ "name": "Gully Hoodie (navy)", "quantity": 1, "unitAmount": 5400 }] }
},
"store": { "name": "Gully Demo Store", "currency": "USD" },
"paymentMethods": ["card", "apple_pay", "google_pay", "usdc", "glusd"],
"demoCrypto": {
  "network": "gully-chain-testnet",
  "chainId": 485590,
  "label": "USDC · Gully Chain Testnet",
  "token": "USDC",
  "decimals": 6,
  "amountBaseUnits": "69000000",
  "settlementAddress": "0x…",
  "explorer": "https://testnet-explorer.gullychain.xyz"
}
}

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

POST/checkout/:sessionId/crypto/settleVerify an on-chain stablecoin payment and settle

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

Request Body
{
"chain": "base",
"token": "USDC",
"txHash": "0x9f… (EVM tx hash, or base58 Solana signature)",
"payerAddress": "0x… (optional)",
"email": "[email protected] (optional, for the receipt)"
}
FieldTypeRequiredDescription
chainstringYesbase | ethereum | bsc | solana | gully-chain-testnet
tokenstringYesStablecoin symbol, e.g. USDC
txHashstringYesEVM transaction hash (0x…, 32-byte) or base58 Solana signature
payerAddressstringNoThe wallet that paid (recorded for reconciliation)
emailstringNoWhere to email the receipt once the payment is verified
Response
{ "status": "paid", "txHash": "0x9f…" }

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.

Server-verified, on-chain only

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

GET/checkout/:sessionId/crypto/statusPoll the crypto payment state

Returns the real session/transaction state so the checkout page (e.g. a QR payer) can poll while waiting for settlement.

Response
{ "status": "pending", "txHash": "0x9f…" }
FieldTypeDescription
statusstringpending (awaiting settlement) | paid (verified & completed) | expired
txHashstringPresent once paid — the settling transaction
No background chain polling

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

POST/checkout/:sessionId/completeComplete a checkout session
Request Body
{
"paymentMethod": "card"
}
FieldTypeRequiredDescription
paymentMethodstringYesThe 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 *):

EventWhen
checkout.session.completedThe session was paid: card (Stripe confirmed) or crypto (verified on-chain). Sent once per session.
checkout.session.expiredThe session passed its 30-minute window unpaid. Sent once per session, within about a minute of expiry.
checkout.session.payment_failedStripe 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.

Receipts

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.

SDKs

The Go, Python, Dart, Swift and Kotlin SDKs create sessions with POST /checkout/hosted/:storeId using the client's API key. See SDKs.