Docs
Dashboard

API Reference

Complete REST API documentation for the GullyPay platform. All endpoints use JSON request and response bodies.

Base URL

All API requests are made to:

https://gullypay.xyz/api

For sandbox/testing:

https://gullypay.xyz/api

Authentication

Include your API key or JWT token in the Authorization header of every request:

Authorization: Bearer rk_live_your_api_key

Two authentication methods are available:

MethodFormatUse Case
API Keyrk_live_... or rk_test_...Server-to-server integrations
JWT TokeneyJhbGci...User sessions (dashboard, mobile apps)

Rate Limiting

All API endpoints are rate-limited to protect service stability.

TierLimitApplies To
Standard1,000 requests/minuteMost endpoints
Auth10 requests/minutePOST /auth/login
Registration5 requests/minutePOST /auth/register

Rate limit headers are included in every response:

Response Headers
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1718928000

When a rate limit is exceeded, the API returns HTTP 429 Too Many Requests.

Request Format

  • All request bodies must be valid JSON with Content-Type: application/json
  • Amounts are in the smallest currency unit (cents for USD, pence for GBP)
  • Resource ids in paths are UUIDs (e.g. 3f2b8c1e-5d4a-4e7b-9c1a-2b6d8e0f4a11). A path id that is not a valid UUID returns 404 Not Found, not 500
  • Timestamps are ISO 8601 format: 2025-06-09T12:00:00Z

Error Codes

All errors follow a consistent format:

Error Response
{
"statusCode": 400,
"message": "Validation failed",
"error": "Bad Request",
"details": [
  { "field": "amount", "message": "amount must be at least 1" }
]
}
HTTP StatusErrorDescription
400Bad RequestInvalid request body or parameters. Check the details array.
401UnauthorizedMissing or invalid authentication credentials.
402Payment Requiredx402 payment required to access this resource.
403ForbiddenValid credentials, but insufficient permissions (RBAC).
404Not FoundThe requested resource does not exist, or a path id is not a valid UUID.
409ConflictDuplicate resource (e.g., duplicate idempotency key).
422Unprocessable EntityRequest is well-formed but semantically invalid.
429Too Many RequestsRate limit exceeded. Retry after X-RateLimit-Reset.
500Internal Server ErrorUnexpected server error. Contact support.
503Service UnavailableTemporary downtime. Retry with exponential backoff.

Authentication

POST/auth/registerCreate a new user account
POST/auth/loginAuthenticate and receive JWT token pair
POST/auth/refreshRefresh an expired access token
POST/auth/logoutRevoke a refresh token
POST/auth/verify-emailConfirm an email address with the token from the verification email
GET/auth/verify-emailEmail verification status of the signed-in user (JWT)
POST/auth/verify-email/resendRe-send the verification email (JWT, 5 per hour)
POST/auth/password/forgotEmail a password-reset link (always 200; 5 per 15 min per IP)
POST/auth/password/resetSet a new password with the emailed token (single use, 60 min); revokes all sessions
POST/auth/api-keysCreate a scoped API key
GET/auth/api-keysList all API keys for the authenticated user
DELETE/auth/api-keys/:idRevoke an API key
GET/auth/me/deletionWhat still blocks deleting the signed-in account, and which businesses would close (JWT)
DELETE/auth/meDelete the signed-in account: password (or a sign-in in the last 5 min) and a 2FA code when on; 5 per 15 min
GET/auth/me/deletion/retentionThe retention policy applied on deletion (JWT)

POST /auth/register

Request Body
{
"email": "[email protected]",
"password": "secure-password-123",
"firstName": "Jane",
"lastName": "Doe"
}
FieldTypeRequiredDescription
emailstringYesValid email address
passwordstringYesMinimum 8 characters, maximum 128
firstNamestringNoUser's first name (max 100 chars)
lastNamestringNoUser's last name (max 100 chars)

POST /auth/login

Request Body
{
"email": "[email protected]",
"password": "secure-password-123"
}
Response
{
"accessToken": "eyJhbGciOiJIUzI1NiIs...",
"refreshToken": "rt_a1b2c3d4..."
}

DELETE /auth/me

Closes the signed-in account. Used by Settings > Security, gullypay.xyz/account/delete and the mobile apps.

Request Body
{
"password": "secure-password-123",
"code": "123456"
}
FieldTypeRequiredDescription
passwordstringUnless the access token is under 5 minutes oldCurrent password (re-authentication)
codestringWhen 2FA is on6-digit TOTP code

Errors: 401 INVALID_PASSWORD, 401 REAUTH_REQUIRED (no password and the session is older than 5 minutes), 401 MFA_REQUIRED, and 409 ACCOUNT_DELETION_BLOCKED with a blockers array while money is still moving or held. Deletion never strands funds; it waits while:

  • the user is the only owner of a business with a payout in progress (payouts_in_flight), an unpaid stablecoin / x402 balance (merchant_balance_owed), a checkout that can still be paid (open_checkouts) or an open dispute (open_disputes);
  • the user's embedded wallet holds stablecoins or more than 0.001 of the native gas token (wallet_balance), its balance cannot be read (wallet_unverified, fail-closed), or the internal glUSD balance is not zero (internal_balance).

Each blocker has a detail and the action that clears it. On success (200):

Response
{
"deleted": true,
"closedAt": "2026-10-26T10:00:00.000Z",
"merchantsClosed": ["4b1d…"],
"retention": [{ "data": "Email, name, password hash, 2FA secret", "onDeletion": "anonymised", "period": "Immediately", "why": "…" }]
}

In one transaction the user's email, name, password hash and 2FA secret are anonymised; sessions, passkeys, push devices and team memberships are deleted; the user's API keys are revoked; wallets are frozen (status: frozen, encrypted key kept for recovery); businesses the user solely owned are closed with their API keys revoked and webhooks disabled; an account.deleted audit entry is written; and a confirmation email goes to the old address. Payment, ledger, payout and audit records are kept for 7 years and support tickets for 2 years, linked only to the anonymous user id (see the privacy policy).

Push devices (mobile apps)

POST/me/devicesRegister or refresh an Expo push token for the signed-in user (JWT; 20 per minute)
GET/me/devicesList the user's push devices (tokens masked)
DELETE/me/devices/:idRemove a push device

/notifications/devices is accepted as an alias of /me/devices.

POST /me/devices
{
"token": "ExponentPushToken[xxxxxxxxxxxxxxxxxxxxxx]",
"platform": "ios",
"app": "consumer",
"deviceName": "Ada's iPhone"
}
FieldTypeRequiredDescription
tokenstringYesExpo push token (ExponentPushToken[…])
platformstringYesios | android
appstringYesconsumer (xyz.gullypay.app) | merchant (xyz.gullypay.merchant)
deviceNamestringNoShown in the device list (max 100 chars)

A token is unique: registering it again refreshes it, and registering it from another account moves it to that account. Pushes go through the Expo push service and are sent only when the API runs with PUSH_NOTIFICATIONS_ENABLED=true (EXPO_ACCESS_TOKEN is optional, for Expo enhanced push security):

EventAppTitledata.url
Checkout completed / card payment succeededmerchantNew payment/payments
Refund (payment.refunded)merchantRefund issued/payments
Payout paid (Stripe) or stablecoin payout sentmerchantPayout sent/payouts
Wallet send / glUSD transfer outconsumerPayment sent/activity
glUSD transfer inconsumerPayment received/activity

Every team member of the business gets merchant pushes. Each push is claimed once per user and event (payment:<transactionId>, payout:<id>, …), so the same payment reported by the checkout page and the Stripe webhook notifies once. Tokens Expo reports as DeviceNotRegistered are deleted.


Merchants

POST/merchantsCreate a merchant with organization and default store
GET/merchantsList merchants for the authenticated user
GET/merchants/:idGet merchant details
PATCH/merchants/:idUpdate merchant profile
POST/merchants/:id/storesAdd a store to a merchant
GET/merchants/:id/storesList stores for a merchant
POST/merchants/:id/onboardInitiate Stripe Connect onboarding
GET/merchants/:id/onboard/statusCheck Stripe onboarding status
POST/merchants/:id/team/membersInvite a team member
GET/merchants/:id/team/membersList team members
PATCH/merchants/:id/team/members/:memberIdChange a member's role (admin or owner)
DELETE/merchants/:id/team/members/:memberIdRemove a member, or leave the team yourself
GET/merchants/:id/team/meYour own role on this merchant

POST /merchants

Request Body
{
"name": "Acme Electronics",
"slug": "acme-electronics",
"businessType": "company",
"country": "US"
}
FieldTypeRequiredDescription
namestringYesBusiness name (max 255 chars)
slugstringYesURL-safe identifier (max 100 chars)
businessTypestringNoindividual or company
countrystringNoTwo-letter ISO country code

POST /merchants/:id/team/members

Request Body
{
"userId": "usr_abc123",
"role": "admin"
}

Available roles: owner, admin, manager, member, viewer.


Payments

POST/payments/intentsCreate a payment intent
POST/payments/intents/:id/confirmConfirm and capture a payment
GET/payments/:idRetrieve a transaction
GET/merchants/:merchantId/paymentsList all transactions for a merchant
POST/payments/:id/refundRefund a payment (full or partial)

POST /payments/intents

Request Body
{
"amount": 2499,
"currency": "USD",
"storeId": "store_abc123",
"merchantId": "merch_xyz789",
"customerEmail": "[email protected]",
"idempotencyKey": "order_12345"
}
FieldTypeRequiredDescription
amountnumberYesAmount in smallest currency unit (min: 1)
currencystringYesThree-letter ISO currency code
storeIdstringYesTarget store ID
merchantIdstringYesMerchant ID
customerEmailstringNoCustomer's email for receipt
idempotencyKeystringNoUnique key to prevent duplicate charges

POST /payments/:id/refund

Request Body
{
"amount": 1000,
"reason": "Customer requested refund"
}
FieldTypeRequiredDescription
amountnumberNoPartial refund amount (omit for full refund)
reasonstringNoReason for the refund

Payouts

POST/merchants/:merchantId/payoutsCreate a payout to merchant bank account
GET/merchants/:merchantId/payoutsList payout history

POST /merchants/:merchantId/payouts

Request Body
{
"amount": 50000,
"currency": "USD",
"destination": "ba_ext_bank_account_id"
}

Checkout

POST/checkout/hosted/:storeIdCreate a hosted checkout session (API key, dashboard session or checkout token)
POST/checkout/tokens/:storeIdMint a single-use checkout token (API key or dashboard session)
POST/checkout/storefront/:slugPublic storefront checkout, priced server-side from product ids
GET/checkout/demoPublic test-mode demo session (nothing stored or charged)
GET/checkout/:sessionIdRetrieve checkout session data
POST/checkout/:sessionId/completeComplete a checkout session

POST /checkout/hosted/:storeId

Requires x-api-key: rk_… (scope checkout:write if the key is scoped), a dashboard JWT, or X-Checkout-Token: ct_…. See Checkout.

Request Body
{
"amount": 4999,
"currency": "USD",
"successUrl": "https://yoursite.com/success",
"cancelUrl": "https://yoursite.com/cancel"
}
Response
{
"sessionId": "3f2b8c1e-9a4d-4e7b-8c21-0d5e6f7a8b9c",
"checkoutUrl": "https://checkout.gullypay.xyz/3f2b8c1e-9a4d-4e7b-8c21-0d5e6f7a8b9c",
"expiresAt": "2026-10-25T13:00:00.000Z"
}

POST/merchants/:merchantId/payment-linksCreate a reusable payment link
GET/merchants/:merchantId/payment-linksList payment links

POST /merchants/:merchantId/payment-links

Request Body
{
"amount": 1999,
"currency": "USD",
"storeId": "store_abc123",
"description": "Premium Plan - Monthly"
}

Products

POST/stores/:storeId/productsCreate a product
GET/stores/:storeId/productsList products in a store
GET/products/:idGet a single product
PATCH/products/:idUpdate a product
DELETE/products/:idSoft-delete a product

POST /stores/:storeId/products

Request Body
{
"title": "Wireless Headphones Pro",
"description": "Premium noise-canceling wireless headphones",
"type": "physical",
"price": 14999,
"sku": "WHP-001",
"status": "active"
}
FieldTypeRequiredDescription
titlestringYesProduct name (max 500 chars)
descriptionstringNoProduct description
typestringYesphysical or digital
pricenumberYesPrice in smallest currency unit (min: 1)
skustringNoStock keeping unit
statusstringNodraft or active (default: draft)

Orders

POST/stores/:storeId/ordersCreate an order
GET/stores/:storeId/ordersList orders for a store
GET/orders/:idGet order details
PATCH/orders/:id/fulfillMark an order as fulfilled
PATCH/orders/:id/cancelCancel an order

POST /stores/:storeId/orders

Request Body
{
"customerId": "cust_abc123",
"items": [
  { "variantId": "var_xyz789", "quantity": 2 },
  { "variantId": "var_def456", "quantity": 1 }
],
"currency": "USD"
}

Wallets

POST/walletsCreate a wallet
GET/wallets/meList wallets for the authenticated user
GET/wallets/:idGet wallet details and balance
POST/wallets/:id/sendSend USDC/USDT/glUSD to an address
GET/wallets/:id/transactionsGet wallet transaction history
POST/wallets/:id/swapSwap between supported tokens

POST /wallets/:id/send

Request Body
{
"toAddress": "0x742d35Cc6634C0532925a3b844Bc9e7595f2bD38",
"amount": 100.50,
"currency": "USDC"
}

POST /wallets/:id/swap

Request Body
{
"fromToken": "USDC",
"toToken": "glUSD",
"amount": 500
}

x402

See the dedicated x402 Protocol page for full documentation.

GET/x402/statusFacilitator status and networks (Gully Chain Testnet, 485590, when enabled)
GET/x402/requirements/:resourceGet payment requirements for a resource (returns 402)
POST/x402/paySubmit an x402 payment
POST/x402/verifyVerify an x402 payment receipt
POST/x402/refundRequest an x402 refund
POST/x402/merchants/:merchantId/endpointsRegister a monetized API endpoint
GET/x402/merchants/:merchantId/endpointsList registered endpoints
GET/x402/merchants/:merchantId/usageGet usage and revenue analytics

Coupons & Collections

POST/stores/:storeId/couponsCreate a discount coupon
GET/stores/:storeId/collectionsList product collections
GET/stores/:storeId/customersList customers for a store

POST /stores/:storeId/coupons

Request Body
{
"code": "SUMMER2025",
"type": "percentage",
"value": 20,
"usageLimit": 100,
"minOrderAmount": 5000
}
FieldTypeRequiredDescription
codestringYesUnique coupon code (max 50 chars)
typestringYespercentage or fixed
valuenumberYesDiscount value (percentage or cents)
usageLimitnumberNoMaximum number of redemptions
minOrderAmountnumberNoMinimum order amount to apply coupon

Public Stats and Health

These endpoints need no authentication and return no personal data.

GET/public/stats?days=7|30|90Platform-wide stats behind the charts on gullypay.xyz
GET/healthLiveness: the API process is up
GET/health/readyReadiness: 200 when the database answers, 503 otherwise
GET/health/historyProbe history since the process started: uptime %, p50/p95 latency
GET/health/statusComponent status and incident history for the status page
GET/checkout/demoStatic test-mode demo checkout session (see Checkout)

GET /public/stats

Platform-wide totals and daily series for the last days days. days is 7, 30 or 90; any other value falls back to 30. Results are cached for 60 seconds (Cache-Control: public, max-age=30, stale-while-revalidate=60) and the endpoint allows 120 requests per minute per IP.

Response (abridged)
{
"generatedAt": "2026-10-26T12:00:00.000Z",
"windowDays": 30,
"totals": {
  "merchants": 42, "stores": 57, "payments": 1310, "volumeCents": 8423100,
  "checkoutSessions": 1688, "checkoutCompleted": 1204,
  "x402Endpoints": 18, "x402Payments": 9120, "x402Settlements": 9101, "paymentLinks": 73
},
"daily": [
  { "date": "2026-10-25", "volumeCents": 312000, "payments": 48, "x402Payments": 301,
    "newMerchants": 2, "checkoutSessions": 61, "glusdMintedCents": 0, "glusdRedeemedCents": 0 }
],
"chainMix": [{ "network": "gully-chain-testnet", "label": "Gully Chain", "payments": 12 }],
"railMix": [{ "rail": "crypto", "label": "Stablecoin", "payments": 230, "volumeCents": 1250000 }],
"glusd": { "supplyCents": 0, "mintedCents": 0, "redeemedCents": 0, "pendingMints": 0, "holders": 0 },
"uptime": { "uptimePct": 100, "p50Ms": 2, "p95Ms": 5, "intervalMs": 60000, "since": "…", "samples": [] },
"networks": [
  { "network": "gully-chain-testnet", "label": "Gully Chain", "chainId": 485590, "enabled": true },
  { "network": "base", "label": "Base", "chainId": 8453, "enabled": true },
  { "network": "ethereum", "label": "Ethereum", "chainId": 1, "enabled": true },
  { "network": "bsc", "label": "BSC", "chainId": 56, "enabled": true },
  { "network": "solana", "label": "Solana", "chainId": null, "enabled": true },
  { "network": "base-sepolia", "label": "Base Sepolia", "chainId": 84532, "enabled": true }
],
"fees": { "…": "the published fee schedule" }
}

chainMix lists every supported network, zeros included. In networks, Gully Chain Testnet has enabled: false while the API runs without GULLY_CHAIN_ENABLED=true.

GET /health/ready

For load balancers and deploy checks. Returns 200 with "status": "ready" when Postgres answers, and 503 with "status": "not_ready" when it does not.

Response
{ "status": "ready", "ready": true, "database": true, "latencyMs": 2, "timestamp": "2026-10-26T12:00:00.000Z" }

GET /health/history

Real database probe results since this API process started: one probe every 60 seconds, the last 24 hours kept. uptimePct, p50Ms, p95Ms and since are null before the first probe.

Response
{
"samples": [{ "t": 1793016000000, "ok": true, "latencyMs": 2 }],
"uptimePct": 100,
"p50Ms": 2,
"p95Ms": 4,
"intervalMs": 60000,
"since": "2026-10-26T08:00:00.000Z"
}

Webhooks

POST/webhooks/stripeReceive Stripe webhook events (signature-verified)
POST/webhooks/:sourceReceive webhooks from other sources

Subscriptions & delivery monitoring

POST/merchants/:merchantId/webhooksCreate a webhook subscription (returns the signing secret once)
GET/merchants/:merchantId/webhooksList webhook subscriptions
GET/merchants/:merchantId/webhooks/deliveriesRecent delivery attempts + 24h stats (count, success rate, avg response)

See the dedicated Webhooks page for event types, payload formats, and verification.