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

json
{
	"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"
}
MemberMeaning
typeA URL naming the kind of failure. It is the stable identifier to branch on, and it links to this page
titleA short summary of the type, the same for every occurrence
statusThe HTTP status code, repeated from the status line
detailWhat went wrong with this request, when there is more to say than the title
instanceA 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

StatusTypeMeaning
400bad-requestThe request is malformed
400validation-errorThe request failed validation
400limit-exceededThe team has reached its limit for this resource
400idempotency-key-missingThis request requires an Idempotency-Key header
400idempotency-key-invalidThe Idempotency-Key header is not valid
401unauthorizedThe API key is missing or invalid
402subscription-requiredAn active subscription is required
403forbiddenThe API key lacks the required scope
404not-foundThe resource does not exist
409conflictThe resource's state prevents this request
409idempotency-key-in-useA request with this idempotency key is still being processed
415unsupported-media-typeThe request body's media type is not accepted
422idempotency-key-reusedThis idempotency key was already used for a different request
429rate-limitedToo many requests
500internalThe request failed on the server
500internal-errorThe change was saved but could not be read back
503endpoint-unavailableThe 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:

json
{
	"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

typescript
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:

typescript
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:

typescript
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:

typescript
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");
}
Documentation