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:
For sandbox/testing:
Authentication
Include your API key or JWT token in the Authorization header of every request:
Two authentication methods are available:
| Method | Format | Use Case |
|---|---|---|
| API Key | rk_live_... or rk_test_... | Server-to-server integrations |
| JWT Token | eyJhbGci... | User sessions (dashboard, mobile apps) |
Rate Limiting
All API endpoints are rate-limited to protect service stability.
| Tier | Limit | Applies To |
|---|---|---|
| Standard | 1,000 requests/minute | Most endpoints |
| Auth | 10 requests/minute | POST /auth/login |
| Registration | 5 requests/minute | POST /auth/register |
Rate limit headers are included in every response:
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 returns404 Not Found, not500 - Timestamps are ISO 8601 format:
2025-06-09T12:00:00Z
Error Codes
All errors follow a consistent format:
| HTTP Status | Error | Description |
|---|---|---|
400 | Bad Request | Invalid request body or parameters. Check the details array. |
401 | Unauthorized | Missing or invalid authentication credentials. |
402 | Payment Required | x402 payment required to access this resource. |
403 | Forbidden | Valid credentials, but insufficient permissions (RBAC). |
404 | Not Found | The requested resource does not exist, or a path id is not a valid UUID. |
409 | Conflict | Duplicate resource (e.g., duplicate idempotency key). |
422 | Unprocessable Entity | Request is well-formed but semantically invalid. |
429 | Too Many Requests | Rate limit exceeded. Retry after X-RateLimit-Reset. |
500 | Internal Server Error | Unexpected server error. Contact support. |
503 | Service Unavailable | Temporary downtime. Retry with exponential backoff. |
Authentication
/auth/registerCreate a new user account/auth/loginAuthenticate and receive JWT token pair/auth/refreshRefresh an expired access token/auth/logoutRevoke a refresh token/auth/verify-emailConfirm an email address with the token from the verification email/auth/verify-emailEmail verification status of the signed-in user (JWT)/auth/verify-email/resendRe-send the verification email (JWT, 5 per hour)/auth/password/forgotEmail a password-reset link (always 200; 5 per 15 min per IP)/auth/password/resetSet a new password with the emailed token (single use, 60 min); revokes all sessions/auth/api-keysCreate a scoped API key/auth/api-keysList all API keys for the authenticated user/auth/api-keys/:idRevoke an API key/auth/me/deletionWhat still blocks deleting the signed-in account, and which businesses would close (JWT)/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/auth/me/deletion/retentionThe retention policy applied on deletion (JWT)POST /auth/register
| Field | Type | Required | Description |
|---|---|---|---|
email | string | Yes | Valid email address |
password | string | Yes | Minimum 8 characters, maximum 128 |
firstName | string | No | User's first name (max 100 chars) |
lastName | string | No | User's last name (max 100 chars) |
POST /auth/login
DELETE /auth/me
Closes the signed-in account. Used by Settings > Security, gullypay.xyz/account/delete and the mobile apps.
| Field | Type | Required | Description |
|---|---|---|---|
password | string | Unless the access token is under 5 minutes old | Current password (re-authentication) |
code | string | When 2FA is on | 6-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):
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)
/me/devicesRegister or refresh an Expo push token for the signed-in user (JWT; 20 per minute)/me/devicesList the user's push devices (tokens masked)/me/devices/:idRemove a push device/notifications/devices is accepted as an alias of /me/devices.
| Field | Type | Required | Description |
|---|---|---|---|
token | string | Yes | Expo push token (ExponentPushToken[…]) |
platform | string | Yes | ios | android |
app | string | Yes | consumer (xyz.gullypay.app) | merchant (xyz.gullypay.merchant) |
deviceName | string | No | Shown 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):
| Event | App | Title | data.url |
|---|---|---|---|
| Checkout completed / card payment succeeded | merchant | New payment | /payments |
Refund (payment.refunded) | merchant | Refund issued | /payments |
| Payout paid (Stripe) or stablecoin payout sent | merchant | Payout sent | /payouts |
| Wallet send / glUSD transfer out | consumer | Payment sent | /activity |
| glUSD transfer in | consumer | Payment 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
/merchantsCreate a merchant with organization and default store/merchantsList merchants for the authenticated user/merchants/:idGet merchant details/merchants/:idUpdate merchant profile/merchants/:id/storesAdd a store to a merchant/merchants/:id/storesList stores for a merchant/merchants/:id/onboardInitiate Stripe Connect onboarding/merchants/:id/onboard/statusCheck Stripe onboarding status/merchants/:id/team/membersInvite a team member/merchants/:id/team/membersList team members/merchants/:id/team/members/:memberIdChange a member's role (admin or owner)/merchants/:id/team/members/:memberIdRemove a member, or leave the team yourself/merchants/:id/team/meYour own role on this merchantPOST /merchants
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Business name (max 255 chars) |
slug | string | Yes | URL-safe identifier (max 100 chars) |
businessType | string | No | individual or company |
country | string | No | Two-letter ISO country code |
POST /merchants/:id/team/members
Available roles: owner, admin, manager, member, viewer.
Payments
/payments/intentsCreate a payment intent/payments/intents/:id/confirmConfirm and capture a payment/payments/:idRetrieve a transaction/merchants/:merchantId/paymentsList all transactions for a merchant/payments/:id/refundRefund a payment (full or partial)POST /payments/intents
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | Yes | Amount in smallest currency unit (min: 1) |
currency | string | Yes | Three-letter ISO currency code |
storeId | string | Yes | Target store ID |
merchantId | string | Yes | Merchant ID |
customerEmail | string | No | Customer's email for receipt |
idempotencyKey | string | No | Unique key to prevent duplicate charges |
POST /payments/:id/refund
| Field | Type | Required | Description |
|---|---|---|---|
amount | number | No | Partial refund amount (omit for full refund) |
reason | string | No | Reason for the refund |
Payouts
/merchants/:merchantId/payoutsCreate a payout to merchant bank account/merchants/:merchantId/payoutsList payout historyPOST /merchants/:merchantId/payouts
Checkout
/checkout/hosted/:storeIdCreate a hosted checkout session (API key, dashboard session or checkout token)/checkout/tokens/:storeIdMint a single-use checkout token (API key or dashboard session)/checkout/storefront/:slugPublic storefront checkout, priced server-side from product ids/checkout/demoPublic test-mode demo session (nothing stored or charged)/checkout/:sessionIdRetrieve checkout session data/checkout/:sessionId/completeComplete a checkout sessionPOST /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.
Payment Links
/merchants/:merchantId/payment-linksCreate a reusable payment link/merchants/:merchantId/payment-linksList payment linksPOST /merchants/:merchantId/payment-links
Products
/stores/:storeId/productsCreate a product/stores/:storeId/productsList products in a store/products/:idGet a single product/products/:idUpdate a product/products/:idSoft-delete a productPOST /stores/:storeId/products
| Field | Type | Required | Description |
|---|---|---|---|
title | string | Yes | Product name (max 500 chars) |
description | string | No | Product description |
type | string | Yes | physical or digital |
price | number | Yes | Price in smallest currency unit (min: 1) |
sku | string | No | Stock keeping unit |
status | string | No | draft or active (default: draft) |
Orders
/stores/:storeId/ordersCreate an order/stores/:storeId/ordersList orders for a store/orders/:idGet order details/orders/:id/fulfillMark an order as fulfilled/orders/:id/cancelCancel an orderPOST /stores/:storeId/orders
Wallets
/walletsCreate a wallet/wallets/meList wallets for the authenticated user/wallets/:idGet wallet details and balance/wallets/:id/sendSend USDC/USDT/glUSD to an address/wallets/:id/transactionsGet wallet transaction history/wallets/:id/swapSwap between supported tokensPOST /wallets/:id/send
POST /wallets/:id/swap
x402
See the dedicated x402 Protocol page for full documentation.
/x402/statusFacilitator status and networks (Gully Chain Testnet, 485590, when enabled)/x402/requirements/:resourceGet payment requirements for a resource (returns 402)/x402/paySubmit an x402 payment/x402/verifyVerify an x402 payment receipt/x402/refundRequest an x402 refund/x402/merchants/:merchantId/endpointsRegister a monetized API endpoint/x402/merchants/:merchantId/endpointsList registered endpoints/x402/merchants/:merchantId/usageGet usage and revenue analyticsCoupons & Collections
/stores/:storeId/couponsCreate a discount coupon/stores/:storeId/collectionsList product collections/stores/:storeId/customersList customers for a storePOST /stores/:storeId/coupons
| Field | Type | Required | Description |
|---|---|---|---|
code | string | Yes | Unique coupon code (max 50 chars) |
type | string | Yes | percentage or fixed |
value | number | Yes | Discount value (percentage or cents) |
usageLimit | number | No | Maximum number of redemptions |
minOrderAmount | number | No | Minimum order amount to apply coupon |
Public Stats and Health
These endpoints need no authentication and return no personal data.
/public/stats?days=7|30|90Platform-wide stats behind the charts on gullypay.xyz/healthLiveness: the API process is up/health/readyReadiness: 200 when the database answers, 503 otherwise/health/historyProbe history since the process started: uptime %, p50/p95 latency/health/statusComponent status and incident history for the status page/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.
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.
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.
Webhooks
/webhooks/stripeReceive Stripe webhook events (signature-verified)/webhooks/:sourceReceive webhooks from other sourcesSubscriptions & delivery monitoring
/merchants/:merchantId/webhooksCreate a webhook subscription (returns the signing secret once)/merchants/:merchantId/webhooksList webhook subscriptions/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.