Errors & rate limits
Predictable error shapes, idempotent writes and documented limits.
Error shape
Every failure returns the same envelope. request_id identifies the exact call — include it when you contact support. details is present on validation failures.
{
"success": false,
"request_id": "req_9f2c…",
"mode": "live",
"error": {
"code": "QUOTE_EXPIRED",
"message": "This quote expired. Request a new one.",
"details": { }
}
}Error codes
| Field | Type | Description | |
|---|---|---|---|
| INVALID_AMOUNT | 400 | optional | Amount missing or non-positive. |
| INVALID_RECEIVER | 400 | optional | Mobile number malformed. |
| QUOTE_EXPIRED | 400 | optional | Quote confirmed after its 10-minute TTL. |
| INSUFFICIENT_BALANCE | 400 | optional | You don't hold enough of the asset. |
| SANDBOX_ONLY | 400 | optional | Endpoint is sandbox-only (e.g. /sandbox/reset). |
| INVALID_ASSET | 400 | optional | Asset was not "gold" or "silver". |
| INVALID_INTERVAL | 400 | optional | Price-history interval was not "day" or "hour". |
| INVALID_FROM | 400 | optional | Price-history from date was not YYYY-MM-DD. |
| INVALID_TO | 400 | optional | Price-history to date was not YYYY-MM-DD. |
| INVALID_RANGE | 400 | optional | Price-history from is later than to. |
| INVALID_CODE | 400 | optional | KYC: the email verification code is wrong or has expired. |
| DISPOSABLE_EMAIL | 400 | optional | KYC: disposable email addresses are refused. Ask for a permanent one. |
| KYC_FAILED | 400 | optional | KYC: the step failed for a reason without a more specific code. Show error.message to the user. |
| KYC_REJECTED | 400 | optional | Test mode only: the magic PAN ZZZZZ9999Z, which always fails the PAN check. |
| INVALID_INPUT | 400 | optional | A KYC field failed a check (bad format, PAN not valid, name or date of birth doesn't match, under 18, already registered). details.field names it. |
| EMAIL_NOT_VERIFIED | 400 | optional | KYC: verify the user's email before the PAN step. |
| PAN_REQUIRED | 400 | optional | KYC: verify the PAN before the Aadhaar step. |
| PAN_PHOTO_NOT_ALLOWED | 400 | optional | KYC: the PAN photo route opens only after a verification-service outage or two failed instant PAN checks. Check v2.panPhotoAllowed in kyc/status. |
| INVALID_DOCUMENT | 400 | optional | KYC image isn't a readable base64 JPEG, PNG or PDF. |
| DOCUMENT_TOO_LARGE | 400 | optional | KYC image is over 4 MB. |
| INVALID_RETURN_URL | 400 | optional | DigiLocker returnUrl isn't a valid https URL. |
| RETURN_URL_NOT_ALLOWED | 400 | optional | DigiLocker returnUrl's origin isn't registered in your panel under Payments → Checkout → Return origins. |
| INVALID_TOKEN | 401 | optional | Missing, malformed or unrecognised bearer token. |
| INVALID_OTP | 401 | optional | Wrong OTP. 5 attempts, then the quote locks. |
| INSUFFICIENT_PARTNER_FUNDS | 402 | optional | Your wallet float / credit line can't cover this buy. |
| INSUFFICIENT_USER_BALANCE | 402 | optional | The end user doesn't hold enough to sell or redeem. |
| API_NOT_ALLOWED | 403 | optional | Your profile isn't enabled for this capability. |
| LIVE_NOT_ENABLED | 403 | optional | Live mode not activated yet. Sandbox keeps working. |
| SANDBOX_DISABLED | 403 | optional | Sandbox access disabled for your partner. |
| PARTNER_DISABLED | 403 | optional | Master kill-switch; both modes disabled. |
| KYC_REQUIRED | 403 | optional | The user must complete KYC first. |
| WALLET_NOT_CONFIGURED | 403 | optional | Partner wallet not set up — a separate onboarding step. |
| FEATURE_DISABLED | 403 | optional | That feature isn't switched on for your account. |
| DIGILOCKER_OFF | 403 | optional | DigiLocker is switched off for your account. Verify Aadhaar with photos instead. |
| QUOTE_NOT_FOUND | 404 | optional | quote_id was never issued, or isn't yours. |
| USER_CODE_NOT_FOUND | 404 | optional | user_code isn't recognised for your partner. |
| SESSION_NOT_FOUND | 404 | optional | That DigiLocker session id doesn't exist for this user. |
| USER_CODE_CONSUMED | 409 | optional | Single-use embed code already exchanged. |
| QUOTE_ALREADY_CONSUMED | 409 | optional | Quote already used. Replay with the original Idempotency-Key. |
| UNDER_REVIEW | 409 | optional | KYC: the user's KYC is already with OroPocket's review. Wait for kyc.approved or kyc.rejected. |
| ALREADY_APPROVED | 409 | optional | KYC: the user is already approved. Nothing to do. |
| ALREADY_VERIFIED | 409 | optional | KYC: the user's Aadhaar is already verified. |
| AADHAAR_OTP_RETIRED | 410 | optional | The Aadhaar OTP endpoints are retired. Verify Aadhaar with DigiLocker or photos — see KYC. |
| DAILY_LIMIT_EXCEEDED | 429 | optional | The send breaches your daily INR limit. |
| RATE_LIMITED | 429 | optional | Too many requests in the window — see below. |
| SANDBOX_DAILY_LIMIT | 429 | optional | Sandbox daily budget spent. Live keys are unaffected. |
| BUY_PROCESSING_FAILED | 500 | optional | Fulfilment failed after debit; your wallet is auto-refunded and the order voided. |
| PRICES_UNAVAILABLE | 503 | optional | Upstream price feed is down. |
| PROVIDER_UNAVAILABLE | 503 | optional | The verification service is briefly unavailable. Retry later; for PAN, the photo route opens. |
Idempotency
Send an Idempotency-Key header on mutating endpoints (buy, sell, send, gift and delivery confirms) to make retries safe. Any string up to 100 characters — a UUID is ideal.
Keys are scoped to your partner profile and endpoint and stay valid for at least 24 hours. Reusing one returns the original response with no new side effects, flagged by an idempotent_replay field in the envelope and an Idempotent-Replayed: true response header.
curl -X POST https://api.oropocket.com/partner/buy/confirm \
-H "Authorization: Bearer oro_live_xxx" \
-H "Idempotency-Key: 8f14e45f-cea1-4b2c-9d3a-1f2e3d4c5b6a" \
-d '{"quote_id":"tqt_xxx"}'QUOTE_ALREADY_CONSUMED you can't safely interpret.Rate limits
| Field | Type | Description | |
|---|---|---|---|
| POST /auth/request-otp | 5 / 15 min | optional | Per (IP, mobile). |
| POST /auth/verify-otp | 10 / 15 min | optional | Per (IP, mobile). |
| All other endpoints | 120 / min | optional | Per IP. |
| Sandbox, all endpoints | 1000 / day | optional | Per partner, cumulative. Resets 00:00 IST. Live is not capped this way. |
The per-minute limits are per IP. The sandbox daily cap is per partner and counts every authenticated request you make in sandbox, whatever the endpoint and whether it succeeds or returns a validation error — so a tight polling loop spends it just as fast as real testing does. Rotating your sandbox token does not reset it.
Sandbox responses carry X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds, the next 00:00 IST), so you can see where you stand without waiting to be blocked. Past the cap you get 429 SANDBOX_DAILY_LIMIT with a Retry-After. Your live token keeps working throughout.
If a genuine integration needs more than 1000 sandbox calls a day, ask — the ceiling is configurable per partner.
Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset — back off when remaining hits zero rather than retrying blindly.