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": "Error Type",
"message": "Human-readable description",
"code": "ERROR_CODE"
}
| Field | Type | Description |
|---|
error | string | Short error category (e.g., "Bad Request", "Conflict") |
message | string | Human-readable description of what went wrong |
code | string | Machine-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
| Code | Meaning |
|---|
400 | Bad Request - Invalid parameters |
401 | Unauthorized - Invalid/missing API key |
403 | Forbidden - IP not whitelisted |
404 | Not Found - Resource doesn’t exist |
409 | Conflict - Resource already exists |
422 | Unprocessable - Validation failed |
429 | Too Many Requests - Rate limited |
500 | Internal Error - Our fault |
Common Error Codes
User Errors
| Code | Description |
|---|
USER_EXISTS | Mobile number already registered |
USER_NOT_FOUND | User ID doesn’t exist |
KYC_INCOMPLETE | User hasn’t completed KYC |
NO_BANK_ACCOUNT | No verified bank account |
Transaction Errors
| Code | Description |
|---|
PENDING_TRANSACTION_EXISTS | User has an active transaction |
AMOUNT_TOO_LOW | Below minimum ($10) |
AMOUNT_TOO_HIGH | Above maximum ($50,000) |
QUOTE_EXPIRED | Transaction expired |
Payout Errors
| Code | Description |
|---|
KYC_NOT_VERIFIED | User KYC status is not verified at payout time |
KYC_LEVEL_INSUFFICIENT | User must complete at least basic KYC |
BANK_ACCOUNT_INVALID | Bank account not found or not verified |
PAYOUT_FAILED | Payout provider rejected the transfer |
KYC Errors
| Code | Description |
|---|
PAN_INVALID | Invalid PAN format |
PAN_NOT_FOUND | PAN not in database |
AADHAAR_OTP_EXPIRED | OTP session expired |
BANK_VERIFICATION_FAILED | Penny drop failed |
NAME_MISMATCH | Document names don’t match |
Rate Limiting
Rate-limited responses (429) include headers to help you manage request pacing:
| Header | Description |
|---|
X-RateLimit-Limit | Maximum requests allowed per minute |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix 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
JavaScript
TypeScript
Python
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.' };
}
}
interface StablePayError {
error: string;
message: string;
code: string;
}
interface StablePayResponse<T> {
success: boolean;
data: T;
}
async function createTransaction(
userId: string,
amount: string
): Promise<StablePayResponse<any> | { error: string }> {
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 err: StablePayError = await response.json();
if (response.status === 429) {
const retryAfter = parseInt(response.headers.get('retryAfter') ?? '60');
return { error: `Rate limited. Retry in ${retryAfter}s` };
}
return { error: err.message };
}
return response.json();
}
import requests
import uuid
def create_transaction(user_id: str, amount: str) -> dict:
response = requests.post(
"https://api.stablepay.global/v2/transactions",
headers={
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json",
"Idempotency-Key": str(uuid.uuid4()),
},
json={
"userId": user_id,
"amount": amount,
"asset": "USDC",
"network": "polygon",
},
)
if response.status_code == 429:
retry_after = response.json().get("retryAfter", 60)
return {"error": f"Rate limited. Retry in {retry_after}s"}
if not response.ok:
err = response.json()
return {"error": err.get("message", "Unknown error")}
return response.json()
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"
}
| Scenario | Response |
|---|
| New unique key | 201 — Transaction created |
| Same key, same body (retry) | 200 — Cached response, Idempotent-Replayed: true header |
| Same key, different body | 409 — Conflict, no action taken |
Keys expire after 24 hours.