Errors & status codes
Every v2 error response is JSON with a small, consistent shape. This page catalogs the codes you'll see, what triggers each, and how to recover. When something's wrong, start at the status code and walk the page.
Error shape
{
"error": "not_retryable",
"message": "Cannot retry order in completed status. Must be failed."
}error— short machine-readable code. Safe to branch on in code.message— human-readable sentence. Safe to log, include in support tickets, and surface in internal dashboards. Do not surface raw to end users — wording can change.
Status code table
200201204400message names the bad field. Fix + retry.401403invoices:read). Toggle in the API Keys page.404409completed order.422forceWalletPayment is on.500502/503/504Debugging 401
Nine times out of ten a 401 is one of: bad key, bad secret, timestamp format, or path mismatch. Walk this checklist top-down — stop at the first hit.
- Fresh copy the key and secret from the business-app API Keys page. A trailing newline (shell copy-paste) or a leading space breaks the header.
- Timestamp format. Must be milliseconds since epoch, as a decimal string. Not ISO-8601, not seconds. Your clock must be within 30 s of Armada's.
- Path in the signed payload. The signed
pathmust exactly match what arrives on the wire — including any query string. If axios/Guzzle/etc. serializes an emptyparamsobject as a trailing?, sign that. - Body byte-identical. If you pretty-print the JSON for logging and then send a compact copy, the hash won't match. Stringify once, sign + send the same bytes.
- Secret rotation. If someone rotated the secret recently, old clients still using the cached value will 401. Grab the new one.
See Authentication for the full signing walkthrough with worked examples.
Common 400s by endpoint
A non-exhaustive list of the errors you'll bump into while integrating, in rough order of frequency.
POST /v2/deliveries
destination.latitude: is required for location_format— you setdestination_format: "location_format"but didn't provide lat/lng. Either include them or switch to a structured format.branch_id is required when merchant has multiple branches— you sentorigin_format: "location_format"on a multi-branch merchant. Usebranch_formatwith an explicitbranch_id, or associate the merchant with a single branch.Origin: Branch not found or does not belong to this merchant— thebranch_idis wrong or is another merchant's. Fetch the list withGET /v2/branches.payment.amount: must be a positive number— zero or negative. Minimum is anything > 0.reference: is required— always send a partner-side unique id for idempotency.
POST /v2/deliveries/:id/cancel
409 not_cancellable— the order is already in a terminal state (completed/canceled/failed). Fetch withGETto see current status.
POST /v2/deliveries/:id/retry
409 not_retryable— the order is not infailed. Onlyfailedcan be retried; forcanceledorcompleted, create a new delivery.
Auth / permissions (any endpoint)
403with a message about permissions — the key lacks the specific cap. Example:invoices:read required. Edit the key in the API Keys page.
Retry strategy
When you see a transient error — network timeout, 5xx, connection reset — retry
with exponential backoff. The SDKs don't retry automatically (to avoid double-dispatching on
ambiguous responses), so this is on you. A reasonable baseline:
async function withRetry(fn, { maxAttempts = 5 } = {}) {
for (let attempt = 1; attempt <= maxAttempts; attempt++) {
try {
return await fn();
} catch (err) {
const s = err.status;
// Retry transient server errors, not client errors.
if (s >= 500 || s === 429 || s === undefined /* network */) {
if (attempt === maxAttempts) throw err;
const wait = Math.min(16_000, 500 * Math.pow(2, attempt - 1));
await new Promise((r) => setTimeout(r, wait));
continue;
}
throw err; // 4xx → don't retry
}
}
}
const { data } = await withRetry(() => armada.deliveries.create(body));