Error Handling
API failures are RFC 9457 problem details. Learn the format, every problem type, and how to handle them in your integration.
Every API failure is answered with an RFC 9457 problem details document, served as Content-Type: application/problem+json. Successful responses keep their data/meta envelope.
Problem Format
{
"type": "https://uptime.sergiodxa.com/docs/api/errors/not-found",
"title": "The resource does not exist",
"status": 404,
"detail": "Monitor not found",
"instance": "urn:uuid:0b6a4c1e-3f7d-4e8a-9c21-5d8f0a7b3e64"
}| Member | Meaning |
|---|---|
type | A URL naming the kind of failure. It is the stable identifier to branch on, and it links to this page |
title | A short summary of the type, the same for every occurrence |
status | The HTTP status code, repeated from the status line |
detail | What went wrong with this request, when there is more to say than the title |
instance | A urn:uuid: identifier for this one failure; quote it when you contact support |
Treat any type you do not recognize by its status, since new types may be added.
Problem Types
| Status | Type | Meaning |
|---|---|---|
| 400 | bad-request | The request is malformed |
| 400 | validation-error | The request failed validation |
| 400 | limit-exceeded | The team has reached its limit for this resource |
| 400 | idempotency-key-missing | This request requires an Idempotency-Key header |
| 400 | idempotency-key-invalid | The Idempotency-Key header is not valid |
| 401 | unauthorized | The API key is missing or invalid |
| 402 | subscription-required | An active subscription is required |
| 403 | forbidden | The API key lacks the required scope |
| 404 | not-found | The resource does not exist |
| 409 | conflict | The resource's state prevents this request |
| 409 | idempotency-key-in-use | A request with this idempotency key is still being processed |
| 415 | unsupported-media-type | The request body's media type is not accepted |
| 422 | idempotency-key-reused | This idempotency key was already used for a different request |
| 429 | rate-limited | Too many requests |
| 500 | internal | The request failed on the server |
| 500 | internal-error | The change was saved but could not be read back |
| 503 | endpoint-unavailable | The endpoint is unavailable to this team |
Each type is https://uptime.sergiodxa.com/docs/api/errors/ followed by the name above, so not-found is https://uptime.sergiodxa.com/docs/api/errors/not-found.
Validation Errors
A validation-error problem lists every invalid field in errors. Each entry's pointer is an RFC 6901 JSON Pointer into the request body, or "" for the request as a whole:
{
"type": "https://uptime.sergiodxa.com/docs/api/errors/validation-error",
"title": "The request failed validation",
"status": 400,
"instance": "urn:uuid:5f1d2c3b-8a4e-4b6f-9d7c-2e1a0b9c8d7f",
"errors": [
{ "pointer": "/url", "code": "invalid", "message": "Expected a valid URL" },
{
"pointer": "/intervalSeconds",
"code": "invalid",
"message": "Expected a value of at least 60"
}
]
}Use errors to show feedback next to each invalid field in your UI.
Best Practices
Branch on the Type
let response = await fetch("https://uptime.sergiodxa.com/api/v1/monitors", {
method: "POST",
headers: { Authorization: `Bearer ${apiKey}`, "Content-Type": "application/json" },
body: JSON.stringify(data),
});
if (response.headers.get("Content-Type")?.startsWith("application/problem+json")) {
let problem = await response.json();
let name = problem.type.split("/").pop(); // "validation-error"
}Log the Instance
Store type and instance in your logs, so a failure can be traced to the exact request:
if (!response.ok) {
let problem = await response.json();
console.error(
`API error ${problem.type} (${problem.instance}): ${problem.detail ?? problem.title}`,
);
}Show User-Friendly Messages
Map problem types to messages for your users instead of displaying raw API responses:
let userMessages: Record<string, string> = {
"validation-error": "Please check your input and try again.",
"limit-exceeded": "You've reached the maximum number of resources.",
unauthorized: "Please sign in to continue.",
forbidden: "You don't have permission to perform this action.",
"not-found": "The requested resource could not be found.",
"rate-limited": "Too many requests. Please wait a moment.",
internal: "Something went wrong. Please try again later.",
};Implement Retry Logic
For transient errors (429 and 5xx), retry with exponential backoff, honoring Retry-After when the response carries it:
async function fetchWithRetry(url: string, options: RequestInit, maxRetries = 3) {
for (let attempt = 0; attempt < maxRetries; attempt++) {
let response = await fetch(url, options);
if (response.ok) return response;
if (response.status === 429 || response.status >= 500) {
let retryAfter = Number(response.headers.get("Retry-After"));
let delay = retryAfter > 0 ? retryAfter * 1000 : Math.pow(2, attempt) * 1000;
await new Promise((resolve) => setTimeout(resolve, delay));
continue;
}
return response;
}
throw new Error("Max retries exceeded");
}