Errors and retries
HTTP status codes, the Mono error envelope, and how to retry failed requests safely.
Every Mono response carries an HTTP status code. Successful calls return 2xx; failures return 4xx (your request was wrong) or 5xx (something went wrong on Mono's side). This page documents the status codes Mono uses, the error envelope you can expect on every failure, and the retry policy that turns transient failures into eventual success without duplicating side effects.
Rate limits are not yet enforced uniformly across every Mono endpoint, but 429 will start
appearing as we tighten them. The retry rules below describe the contract you should code
against today so that your integration already behaves correctly when limits kick in.
HTTP status codes
| Status | Meaning | Retryable? |
|---|---|---|
| 200 (OK) | Request completed successfully. | n/a |
| 201 (Created) | A new resource was created. | n/a |
| 204 (NoContent) | Request succeeded; no body to return. | n/a |
| 400 (BadRequest) | Malformed JSON, missing field, or non-compliant header. The request itself is wrong. | No |
| 401 (Unauthorized) | Missing or invalid Authorization header. | No (fix credential) |
| 403 (Forbidden) | Authenticated, but lacking scope for this resource. | No (fix scope) |
| 404 (NotFound) | Resource does not exist. | No |
| 408 (RequestTimeout) | Mono did not receive the full request in time. | Yes, with backoff |
| 409 (Conflict) | Idempotency-key collision or state conflict — see Idempotency Keys. | No (application logic) |
| 422 (UnprocessableEntity) | Request structure is valid, but the content is rejected (business validation). | No |
| 429 (TooManyRequests) | Rate limit exceeded. Honor the Retry-After header. | Yes, after Retry-After |
| 500 (InternalServerError) | Unexpected server-side failure. | Yes, with backoff |
| 502, 503, 504 | Upstream/gateway transient failures. | Yes, with backoff |
Error envelope
Every failure response carries a standard JSON envelope so clients can parse errors uniformly:
{
"code": "400 Bad Request",
"errors": [
{
"error_code": "item_not_found",
"message": "Item doesn't exist",
"path": "#/item/id",
"url": "https://api.mono.co/docs#errors"
}
],
"id": "log_7MkWaFqvfosB8fzHhb1Eql",
"message": "Malformed request"
}Fields:
code— the HTTP status and reason phrase (e.g.,"404 Not Found").errors[]— one entry per validation failure. Useful when a single request has multiple invalid fields.error_code— stable, machine-readable code for this specific failure.message— short human description.path— JSON pointer into the request payload identifying the offending field.url— link to extended documentation for this error code, when available.
id— Mono-generated request identifier. Include this when contacting support — it lets us locate the exact request in our logs.message— top-level human description suitable for surfacing to operators (not end users).
429 and other rate-limit responses use the same envelope with error_code: "rate_limited".
When to retry
Retry only on transient failures, never on application errors:
| Response | Retry? | Why |
|---|---|---|
429 | Yes, after Retry-After. | Transient. Request was not processed; nothing changed on Mono's side. |
408, 500–504 | Yes, with exponential backoff. | Transient. Request may or may not have been processed — idempotency key protects. |
400, 422 | No. | The request itself is malformed. Retrying will not change the answer. |
401, 403 | No. | Credentials or scope problem. Fix the auth, then send a fresh request. |
404, 409 | No. | Resource missing or state conflict. Application logic must react. |
Four rules apply to every retry:
- Honor
Retry-After. If the header is present (always on429, sometimes on503), sleep for at least that many seconds before retrying. Do not retry sooner. - Use exponential backoff with jitter when
Retry-Afteris absent. Start at 1 second, double on each subsequent failure, add 0–500 ms of random jitter, and cap the delay at 30 seconds. Jitter prevents the thundering-herd problem where many clients retry in lockstep. - Cap the retry budget. Up to 5 retries per logical operation is a reasonable default. After that, surface the failure — retrying forever just shifts the problem.
- Reuse the idempotency key. Every retry must carry the same
X-Idempotency-Keyas the original attempt. Otherwise, a successful retry after an ambiguous5xxcould double-charge a customer.
A minimal retry loop in Python:
import random
import time
import httpx
RETRYABLE_STATUSES = {408, 429, 500, 502, 503, 504}
def request_with_backoff(client, method, url, **kwargs, max_retries=5):
delay = 1.0
for attempt in range(max_retries + 1):
response = client.request(method, url, **kwargs)
if response.status_code not in RETRYABLE_STATUSES:
return response
retry_after = response.headers.get("Retry-After")
wait = float(retry_after) if retry_after else delay + random.uniform(0, 0.5)
time.sleep(min(wait, 30.0))
delay = min(delay * 2, 30.0)
return response # last response, still failingDesigning for resilience up front
Even before rate limits become strict, three habits keep bursty workloads out of trouble:
- Spread bursty work. Batch jobs that fan out thousands of calls — payroll runs, end-of-day reconciliation, mass card issuance — should be paced from the client side rather than fired in parallel. A few requests per second sustained beats a thousand at once and a recovery window afterward.
- Cache reads. Reference data (catalogs, fee schedules, capability lookups) rarely changes inside a single request cycle. Cache the response and only hit Mono when the cache misses.
- Separate sync from async paths. A user-facing checkout call should not share its budget with a nightly export. Use separate API keys for separate workloads when possible so a runaway batch job cannot exhaust the limit a real customer needs.
Common mistakes
| Mistake | Symptom | Fix |
|---|---|---|
Retrying immediately on 429. | Repeated 429s and longer total recovery time. | Sleep for at least Retry-After seconds before the next attempt. |
Ignoring the Retry-After header. | Retries land before the limit has reset. | Treat Retry-After as authoritative when present. |
| Retrying without an idempotency key. | Duplicate transfers once the retry eventually succeeds. | Generate the key once, send it on every attempt. |
Retrying 400/422 as if transient. | Wasted attempts; the same error returns every time. | Only retry on 408, 429, and 5xx. |
| No retry cap. | Stuck workers, blocked queues, alert fatigue. | Cap at a small number of retries (5 is a reasonable default) and surface up. |
| Sharing a key across services and traffic classes. | One noisy batch job rate-limits user-facing checkout traffic. | Issue separate API keys for separate workloads. |
Dropping the id field when reporting an issue. | Support cannot find the request in logs. | Always include id from the error envelope when you escalate. |
Next steps
- Idempotency Keys — required on every retried write.
- Authentication — credential handling for the
401and403cases. - Webhooks — Mono's own retry policy for the events it sends to your system.