Appearance
Ramp errors
Same envelope as the rest of v2:
json
{
"error": {
"code": "invalid_request",
"message": "intentKey is required",
"request_id": "req_…"
}
}code is stable. Branch on it, not on message. request_id is also the X-Request-Id header; quote it when you report a problem.
Four codes carry extra fields on the error object itself, because each has its own recovery and the screen needs the data to run it. They are listed first.
Codes with a recovery
kyc_required
422. A buy was attempted for a person the provider will not take a deposit from yet. error.kyc is the current review state, in the shape /customers/kyc/status returns, so you can pick the right screen without another call.
json
{
"error": {
"code": "kyc_required",
"message": "Identity verification is needed before a deposit",
"kyc": { "state": "unverified", "rejectionReasons": [], "…": "…" }
}
}Route on kyc.state: unverified and expired go to the document form, pending to the waiting screen, rejected to the form with the reasons.
ramp_customer_required
409. The provider holds no profile for this wallet and the side you are running cannot proceed without one. Send the user to the details form and drop any local "already registered" hint you kept. A generic retry will fail the same way.
quote_drift
409. The executable price moved more than 1% from expectedTargetAmount. Carries expectedTargetAmount, actualTargetAmount, rateLabel, feeLabel and, on a sell, providerRecipientId. Nothing was booked. Show the new terms and, if accepted, resend. Details under Price drift.
provider_refused
422. The provider refused this specific action and said why. message is its sentence, unaltered: an account number the bank does not recognise, a deposit above the corridor cap, a person it will not register. Show it. This is different from upstream_error, which means we could not reach the provider at all and have nothing to tell the user.
All codes
| HTTP | code | Means | What to do |
|---|---|---|---|
| 400 | invalid_request | A missing or malformed field: no intentKey, an amount that is not positive, a document.type outside the three allowed, an image over 4MB, a sell without payout, a transfer for a wallet with no customer record, or sandbox: true outside staging | Fix the request. message names the field |
| 401 | unauthorized | proof missing, stale, from the wrong wallet, or replayed. details.reason is one of the proof_of_control_* reasons | Sign a fresh challenge with the wallet the request names |
| 403 | forbidden | The provider does not advertise that corridor, side or chain, the provider is switched off, or sandbox was requested where no sandbox exists | Re-read /providers and offer only what it lists |
| 404 | not_found | No such transfer for you: wrong id, wrong claim token, or a wallet that did not make it. Also an unknown provider | Check the id and the X-Ramp-Claim header |
| 409 | ramp_customer_required | No provider profile, and the side needs one | Details form |
| 409 | quote_drift | Price moved past the bound | Show the new terms, resend on accept |
| 422 | kyc_required | The person is not verified for a deposit | Verification screen, per error.kyc.state |
| 422 | provider_refused | The provider declined, with a reason | Show message |
| 429 | rate_limited | Too many requests from your IP | Back off. See Retry-After |
| 502 | upstream_error | The provider could not be reached, or a create with this intentKey is still in flight | Retry with backoff, same intentKey |
| 500 | internal | Something broke on our side | Retry; if it persists send us the request_id |
Treat 429 and 502 as transient. Treat 400, 401, 403 and 404 as your bug. Treat the four 409/422 codes as normal branches of the flow.
Rate limits
The ramp surface has no API keys, so the only limit is a per-IP ceiling shared with the rest of /v2. It is a brute-force backstop, not a budget: a client that polls one transfer every 3 seconds and one review every 10 is nowhere near it. A tight loop is. Over it you get 429 with Retry-After.
Two throttles sit behind the API on your behalf and are not errors: a transfer's provider state is re-read at most once every 8 seconds, and a person's review state at most once every 15. Polling faster than that returns the stored answer.
Timeouts
POST /v2/ramp/transfers makes several provider calls in sequence (recipient, quote, transfer on a sell) and can take a few seconds. Set your client timeout at 30 seconds or more. If it does time out, retry with the same intentKey; that is what it is for.