Skip to main content

Documentation Index

Fetch the complete documentation index at: https://docs.stablepay.global/llms.txt

Use this file to discover all available pages before exploring further.

All API errors follow a consistent format.

Error Response Format

{
  "error": "Error Type",
  "message": "Human-readable description",
  "code": "ERROR_CODE"
}
FieldTypeDescription
errorstringShort error category (e.g., "Bad Request", "Conflict")
messagestringHuman-readable description of what went wrong
codestringMachine-readable error code for programmatic handling (e.g., KYC_INCOMPLETE)
Some error responses may include additional context fields depending on the endpoint — for example, attemptsRemaining on OTP errors or matchScore on name mismatch errors.

HTTP Status Codes

CodeMeaning
400Bad Request - Invalid parameters
401Unauthorized - Invalid/missing API key
403Forbidden - IP not whitelisted
404Not Found - Resource doesn’t exist
409Conflict - Resource already exists
422Unprocessable - Validation failed
429Too Many Requests - Rate limited
500Internal Error - Our fault

Common Error Codes

User Errors

CodeDescription
USER_EXISTSMobile number already registered
USER_NOT_FOUNDUser ID doesn’t exist
KYC_INCOMPLETEUser hasn’t completed KYC
NO_BANK_ACCOUNTNo verified bank account

Transaction Errors

CodeDescription
PENDING_TRANSACTION_EXISTSUser has an active transaction
AMOUNT_TOO_LOWBelow minimum ($10)
AMOUNT_TOO_HIGHAbove maximum ($50,000)
QUOTE_EXPIREDTransaction expired

Payout Errors

CodeDescription
KYC_NOT_VERIFIEDUser KYC status is not verified at payout time
KYC_LEVEL_INSUFFICIENTUser must complete at least basic KYC
BANK_ACCOUNT_INVALIDBank account not found or not verified
PAYOUT_FAILEDPayout provider rejected the transfer

KYC Errors

CodeDescription
PAN_INVALIDInvalid PAN format
PAN_NOT_FOUNDPAN not in database
AADHAAR_OTP_EXPIREDOTP session expired
BANK_VERIFICATION_FAILEDPenny drop failed
NAME_MISMATCHDocument names don’t match

Rate Limiting

Rate-limited responses (429) include headers to help you manage request pacing:
HeaderDescription
X-RateLimit-LimitMaximum requests allowed per minute
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1750003260

{
  "error": "Rate Limit Exceeded",
  "message": "Too many requests. Please try again later.",
  "retryAfter": 60
}
Use the retryAfter field (seconds) or the X-RateLimit-Reset header to schedule your next request.

Error Handling Examples

async function createTransaction(userId, amount) {
  try {
    const response = await fetch('https://api.stablepay.global/v2/transactions', {
      method: 'POST',
      headers: {
        'Authorization': `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': crypto.randomUUID()
      },
      body: JSON.stringify({ userId, amount, asset: 'USDC', network: 'polygon' })
    });

    if (!response.ok) {
      const error = await response.json();

      switch (error.code) {
        case 'KYC_INCOMPLETE':
          return { error: 'Please complete KYC first' };
        case 'PENDING_TRANSACTION_EXISTS':
          return { error: 'You have an active transaction' };
        case 'AMOUNT_TOO_LOW':
          return { error: 'Minimum amount is $10' };
        default:
          return { error: error.message };
      }
    }

    return await response.json();
  } catch (err) {
    return { error: 'Network error. Please try again.' };
  }
}

Idempotency

For safe retries, include an Idempotency-Key header (UUID) on mutating requests:
curl -X POST https://api.stablepay.global/v2/transactions \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Idempotency-Key: 550e8400-e29b-41d4-a716-446655440000" \
  -d '{"userId": "usr_abc", "amount": "100", "asset": "USDC", "network": "polygon"}'
Same idempotency key = same response (no duplicate transactions).

Idempotency Conflict (409)

If you send the same idempotency key with a different request body, you’ll receive a 409 Conflict:
{
  "error": "Idempotency Conflict",
  "message": "This idempotency key was already used with a different request body",
  "code": "IDEMPOTENCY_CONFLICT"
}
ScenarioResponse
New unique key201 — Transaction created
Same key, same body (retry)200 — Cached response, Idempotent-Replayed: true header
Same key, different body409 — Conflict, no action taken
Keys expire after 24 hours.