Error Reference
Every error returned by the Monogoto API uses the same JSON structure, making it straightforward to write a single error handler that covers all failure modes.
Error Response Format
{
"status_code": 400,
"message": "Field 'username' is required",
"request_id": "a1b2c3d4-e5f6-7890-abcd-ef1234567890"
}
| Field | Type | Description |
|---|---|---|
status_code |
integer | Mirrors the HTTP response status code |
message |
string | Human-readable explanation of what went wrong |
request_id |
string | Unique trace ID for this request — always include this when contacting support |
Support tip: When filing a bug report or opening a support ticket, include the full error response body. The
request_idallows the Monogoto team to locate the exact request in server-side logs.
Error Codes
400 Bad Request
The server could not process the request because the input was malformed or incomplete.
Common causes
- A required field is missing from the request body
- A field value is the wrong type (e.g. a string where an integer is expected)
- A field value falls outside the allowed range or enum
- The request body is not valid JSON
Example
{
"status_code": 400,
"message": "Field 'username' is required",
"request_id": "b3c4d5e6-f7a8-9012-bcde-f12345678901"
}
Fix: Compare your request payload against the endpoint's schema in the API Reference. Pay attention to required fields, field names (case-sensitive), and allowed values.
401 Unauthorized
The request could not be authenticated. Either no token was provided, or the token is invalid or expired.
Common causes
- The
Authorizationheader is absent - The access token has expired (tokens are valid for 4 hours)
- The token was manually revoked or invalidated by a refresh
- The
Bearerprefix is missing or misspelled
Example
{
"status_code": 401,
"message": "Token has expired or is invalid",
"request_id": "c4d5e6f7-a8b9-0123-cdef-012345678902"
}
Fix: Refresh the access token using POST /v1/auth/refresh. If the refresh token has also expired, re-authenticate via POST /v1/auth/token. See the Authentication guide for the full token lifecycle.
403 Forbidden
The request was authenticated but the caller is not authorised to perform this action.
Common causes
- The token lacks the required scope for this endpoint
- The account has been suspended or restricted
- Attempting to read or modify another account's resources
Example
{
"status_code": 403,
"message": "Insufficient scope: 'admin' required",
"request_id": "d5e6f7a8-b9c0-1234-defa-123456789003"
}
Fix: Check the scope badge on the endpoint page in the API Reference. If you believe your account should have the required scope, contact support@monogoto.io and include the request_id.
404 Not Found
The requested resource does not exist at the given path.
Common causes
- An incorrect or stale resource ID in the URL (e.g. wrong ICCID, group ID, or filter ID)
- The resource was deleted after the ID was recorded
- A typo in the URL path segment
Example
{
"status_code": 404,
"message": "Thing '8937010000000000000' not found",
"request_id": "e6f7a8b9-c0d1-2345-efab-234567890004"
}
Fix: Verify the resource ID by listing the parent collection first (e.g. GET /v1/things to confirm the ICCID exists before calling GET /v1/things/{iccid}).
409 Conflict
The request is valid but conflicts with the current state of the resource on the server.
Common causes
- Trying to activate a SIM that is already active
- Creating a resource with a name or identifier that already exists
- A concurrent modification conflict — another request mutated the resource between your read and write
Example
{
"status_code": 409,
"message": "SIM '8937010000000000000' is already in 'active' state",
"request_id": "f7a8b9c0-d1e2-3456-fabc-345678900005"
}
Fix: Fetch the current state of the resource before mutating it. For concurrent modification scenarios, implement an optimistic locking strategy using ETags or version fields where the endpoint supports them.
422 Unprocessable Entity
The request body is structurally valid JSON with correct field types, but the values violate a business rule.
Common causes
- A rate plan is not compatible with the SIM's home network
- Insufficient account balance to complete the operation
- A field combination that is logically invalid (e.g. conflicting filter rules)
- Exceeding a resource quota
Example
{
"status_code": 422,
"message": "Rate plan 'PLAN-EU-5G' is not available for SIM on network 'US-MVNO-1'",
"request_id": "a8b9c0d1-e2f3-4567-abcd-456789000006"
}
Fix: Read the message field carefully — it will describe the exact constraint that was violated. Consult the relevant endpoint documentation or contact support if the constraint is unclear.
429 Too Many Requests
Your integration has exceeded the rate limit for the current time window.
Common causes
- Too many requests sent in a short window (API limit is per User ID)
- Too many login or refresh attempts from one IP (auth limit is 5 per 60 seconds per IP)
Example
{
"status_code": 429,
"message": "Rate limit exceeded. Retry after 47 seconds.",
"request_id": "b9c0d1e2-f3a4-5678-bcde-567890000007"
}
Fix: Read the Retry-After response header and wait the indicated number of seconds. Then retry with exponential backoff. See the Rate Limits guide for a complete implementation.
500 Internal Server Error
An unexpected error occurred on Monogoto's servers. This is not caused by your request.
Example
{
"status_code": 500,
"message": "An unexpected error occurred. Please try again or contact support.",
"request_id": "c0d1e2f3-a4b5-6789-cdef-678900000008"
}
Fix: Retry the request with exponential backoff (1s, 2s, 4s). If the error persists after several retries, check the API Status page for ongoing incidents, or contact support@monogoto.io with the request_id.
502 Bad Gateway / 503 Service Unavailable
A downstream service that the API depends on is temporarily unavailable, or the API gateway itself is restarting.
Fix: These errors are transient. Apply the same retry-with-backoff approach as for 500 errors. Monitor the API Status page for incident updates. Under normal conditions these resolve within seconds to minutes.
Error Handling Patterns
Classifying Errors
Not all errors should be handled the same way. A well-designed integration distinguishes between errors that are worth retrying and those that require user intervention:
| Status code | Class | Action |
|---|---|---|
400 |
Client error — bad request | Fix the input; do not retry |
401 |
Auth error — token expired | Refresh token, then retry once |
403 |
Auth error — insufficient scope | Do not retry; escalate to configuration |
404 |
Client error — wrong ID | Verify the ID; do not retry blindly |
409 |
State conflict | Read current state, adjust, then retry |
422 |
Business rule violation | Fix the request semantics; do not retry |
429 |
Transient — rate limited | Wait for Retry-After, then retry |
500 |
Transient — server error | Retry with exponential backoff |
502 / 503 |
Transient — unavailable | Retry with exponential backoff |
Complete Error Handler
class ApiError extends Error {
constructor(message, statusCode, requestId) {
super(message);
this.name = 'ApiError';
this.statusCode = statusCode;
this.requestId = requestId;
}
isRetryable() {
return [429, 500, 502, 503].includes(this.statusCode);
}
isAuthError() {
return this.statusCode === 401;
}
toString() {
return `${this.name} [${this.statusCode}]: ${this.message} (request_id: ${this.requestId})`;
}
}
async function callApi(url, options = {}, { tokenStore, maxRetries = 3 } = {}) {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
const res = await fetch(url, {
...options,
headers: {
'Content-Type': 'application/json',
'Authorization': `Bearer ${tokenStore.accessToken}`,
...options.headers,
},
});
if (res.ok) return res.json();
const body = await res.json().catch(() => ({
status_code: res.status,
message: res.statusText,
request_id: 'unknown',
}));
const err = new ApiError(body.message, body.status_code, body.request_id);
// 401: refresh token and retry once
if (err.isAuthError() && attempt === 0) {
await tokenStore.refresh();
continue;
}
// 429 / 5xx: wait and retry
if (err.isRetryable() && attempt < maxRetries) {
const retryAfter = res.headers.get('Retry-After');
const delay = retryAfter
? parseInt(retryAfter, 10) * 1000
: Math.pow(2, attempt) * 1000 + Math.random() * 500;
console.warn(`${err} — retrying in ${(delay / 1000).toFixed(1)}s`);
await new Promise(r => setTimeout(r, delay));
continue;
}
// Non-retryable or exhausted retries
throw err;
}
}
from dataclasses import dataclass
@dataclass
class ApiError(Exception):
message: str
status_code: int
request_id: str
def __str__(self):
return f"ApiError [{self.status_code}]: {self.message} (request_id: {self.request_id})"
@property
def is_retryable(self):
return self.status_code in {429, 500, 502, 503}
@property
def is_auth_error(self):
return self.status_code == 401
def call_api(method: str, url: str, token_store, max_retries: int = 3, **kwargs) -> dict:
for attempt in range(max_retries + 1):
resp = requests.request(method, url, headers={
"Authorization": f"Bearer {token_store.access_token}",
"Content-Type": "application/json",
}, **kwargs)
if resp.ok:
return resp.json()
try:
body = resp.json()
except Exception:
body = {"status_code": resp.status_code, "message": resp.reason, "request_id": "unknown"}
err = ApiError(
message=body.get("message", "Unknown error"),
status_code=body.get("status_code", resp.status_code),
request_id=body.get("request_id", "unknown"),
)
# 401: refresh once then retry
if err.is_auth_error and attempt == 0:
token_store.refresh()
continue
# 429 / 5xx: wait and retry
if err.is_retryable and attempt < max_retries:
retry_after = resp.headers.get("Retry-After")
delay = (float(retry_after) if retry_after else (2 ** attempt)) + random.uniform(0, 0.5)
print(f"{err} — retrying in {delay:.1f}s")
time.sleep(delay)
continue
raise errRelated Guides
- Authentication — How to refresh tokens when you receive
401 - Rate Limits — Retry strategies and backoff implementation for
429 - API Status — Real-time platform health for
500/502/503diagnosis