Errors
Every failed request returns the failure envelope with an error.code. Each code has its own page with causes and fixes.
Handling errors
- Branch on the HTTP status and
error.code. Never branch onerror.message, which can change. - For
422 INVALID_REQUEST, readerror.fields. It lists each invalid field and why. - Log
doc_urlwith the error. It links to the page for that code. - Don't treat
available: falseon a quote as an error. See quote reason codes.
const res = await fetch(url, options);
const body = await res.json();
if (body.status === 'failed') {
const { code, fields } = body.error;
if (code === 'INVALID_REQUEST') showFieldErrors(fields);
else if (code === 'INSUFFICIENT_BALANCE') promptToFundWallet();
else if (res.status >= 500 || res.status === 429) retryLater();
else logError(code, body.error.doc_url);
}
Error codes
Every error response carries an error.code, including 400 and 5xx.
| Code | HTTP | When |
|---|---|---|
INVALID_JSON | 400 | The body isn't valid JSON |
IDEMPOTENCY_KEY_REQUIRED | 400 | A request that needs an Idempotency-Key arrived without one |
INVALID_REQUEST | 422 | A field is missing or invalid |
UNAUTHENTICATED | 401 | Missing, invalid or revoked access token |
TOKEN_EXPIRED | 401 | The access token is older than an hour; get a new one from /oauth/token |
INSUFFICIENT_SCOPE | 403 | The token doesn't carry the scope this endpoint needs |
ACCOUNT_ON_HOLD | 403 | Invoice overdue or account suspended; new bookings refused |
IP_NOT_ALLOWED | 403 | The account has the optional live IP allowlist on, and a live token was used from an IP address that isn't on it |
TEST_MODE_ONLY | 403 | A sandbox endpoint was called with a live token, or a live token was sent from a web page (the API Reference or the dashboard's API console) |
NOT_FOUND | 404 | No such object for this account |
ADDRESS_NOT_SERVICEABLE | 422 | Creating a draft with two verified addresses when one is outside Reeds' coverage. (A quote returns available: false with this reason instead.) |
INTERSTATE_NOT_AVAILABLE | 422 | Creating a draft from Lagos to the FCT: inter-state delivery is not available through the API. (A quote returns it as a reason.) |
PICKUP_DATE_OUT_OF_RANGE | 422 | pickup_date is in the past or more than 7 days ahead |
QUOTE_EXPIRED | 409 | The quote is older than 24 hours |
QUOTE_ALREADY_USED | 409 | The quote already booked a delivery; error.delivery_id is that delivery |
INSUFFICIENT_BALANCE | 402 | The wallet doesn't cover the total. Fund it and retry with the same Idempotency-Key. |
CREDIT_LIMIT_REACHED | 402 | Invoice account: what it owes (unbilled deliveries plus unpaid invoices) plus this delivery exceeds the limit |
ACTION_NOT_ALLOWED | 409 | The action conflicts with the object's state, for example booking a draft that isn't priced, or marking a delivered item ready. The error page lists every case. |
ADDRESS_IN_USE | 409 | Deleting an address that a draft still uses; cancel the draft first |
CANCEL_NOT_ALLOWED | 409 | The rider has already set off for the pickup |
SHIPPER_REQUIRED | 422 | A platform account created a delivery without shipper (the merchant it is for) |
PARCEL_TOO_HEAVY | 422 | package.weight_kg is over 30 kg |
PARCEL_TOO_LARGE | 422 | The longest side in package.dimensions_cm is over 120 cm |
PROHIBITED_ITEM | 422 | The description names something Reeds can't carry: cash, weapons, drugs, hazardous goods or perishable food |
IDEMPOTENCY_KEY_REUSED | 422 | Same Idempotency-Key, different request body |
REQUEST_IN_PROGRESS | 409 | Same Idempotency-Key while the first request is still running |
RATE_LIMITED | 429 | Too many requests, or the account has used today's allowance for address lookups and pricing |
INTERNAL_ERROR | 500 | Something failed inside Reeds; safe to retry with the same Idempotency-Key |
SERVICE_UNAVAILABLE | 503 | Reeds is busy (any endpoint, with Retry-After), or a service Reeds depends on (maps, payments) is unavailable; retry later |
The token endpoint (/oauth/token) follows the OAuth 2.0 standard instead: a wrong id or secret returns 401 with { "error": "invalid_client" }. A live client id used from an IP address that isn't on the account's IP allowlist, or sent from a web page, returns 400 with { "error": "unauthorized_client" }. See Authentication errors. When Reeds is busy it returns 503 with { "error": "temporarily_unavailable" } and Retry-After.
Quote reason codes
When Reeds can't serve a route, POST /quotes returns 200 with available: false and a reason. This is not an error: a platform asks many couriers at once and simply leaves Reeds out.
reason.code | When |
|---|---|
ADDRESS_NEEDS_CONFIRMATION | An address matched only approximately, or not at all. data.suggestion is Reeds' best match; quote again with coordinates or a place id, or create a draft. |
ADDRESS_NOT_SERVICEABLE | An address is outside Reeds' coverage, or there is no road route between the two |
INTERSTATE_NOT_AVAILABLE | The drop-off is in another state. Inter-state delivery is not available through the API. |
What to retry
| Response | Retry? |
|---|---|
401 TOKEN_EXPIRED | Get a new token, then retry the same request. |
402 INSUFFICIENT_BALANCE, 402 CREDIT_LIMIT_REACHED | Fix the cause (fund the wallet, pay an invoice), then retry with the same Idempotency-Key. |
403 ACCOUNT_ON_HOLD | Resolve the hold, then retry with the same key. |
403 IP_NOT_ALLOWED | Add the address in the dashboard (or call from an address that is on the list), then retry with the same key. |
403 TEST_MODE_ONLY | Use test credentials, or make the call from your server, then retry with the same key. |
409 REQUEST_IN_PROGRESS | Wait a moment, then retry with the same key. |
409 for a state conflict (QUOTE_EXPIRED, CANCEL_NOT_ALLOWED) | Don't retry as is. Get a fresh quote, or change your approach. |
429 | Wait for Retry-After, then retry with the same key. |
500, 503 | Retry with backoff and the same key. On a 503, wait for Retry-After first when it is present. |
400, 404, 422 | Don't retry. Fix the request and send it with a new key. |
The full rules are in Idempotency.