Error Handling
Error Response Format
All API errors follow a consistent JSON structure with an appropriate HTTP status code:
{
"error": {
"code": "ERROR_CODE",
"message": "Human-readable description of what went wrong"
}
}The code field is a stable, machine-readable identifier you can use in your error handling logic. The message field provides additional context and may vary between occurrences of the same error code.
Error Categories
Authentication Errors (401)
Returned when the request cannot be authenticated.
| Code | Description |
|---|---|
UNAUTHORIZED | The Authorization header is missing, malformed, or contains an invalid token. |
KEY_EXPIRED | The API key was valid but has passed its expiration date. Create a new key to continue. |
{
"error": {
"code": "UNAUTHORIZED",
"message": "Missing or invalid Authorization header"
}
}Authorization Errors (403)
Returned when the authenticated user does not have permission to access the requested resource.
| Code | Description |
|---|---|
FORBIDDEN | You are attempting to access or modify a resource that belongs to another user (e.g. revoking someone else's API key). |
Not Found Errors (404)
Returned when the requested resource does not exist.
| Code | Description |
|---|---|
NOT_FOUND | The resource identified in the URL (extraction, API key, etc.) does not exist or has been deleted. |
Validation Errors (400)
Returned when the request body or parameters are invalid.
| Code | Description |
|---|---|
VALIDATION_ERROR | The request body failed schema validation. The message field describes which fields are invalid. |
MAX_KEYS_REACHED | You have reached the maximum of 10 active API keys. Revoke an existing key before creating a new one. |
{
"error": {
"code": "MAX_KEYS_REACHED",
"message": "Maximum of 10 active API keys allowed"
}
}Rate Limiting Errors (429)
Returned when usage limits have been exceeded.
| Code | Description |
|---|---|
USAGE_LIMIT_EXCEEDED | Your monthly extraction limit has been reached. Upgrade your plan or wait until the next billing period. |
{
"error": {
"code": "USAGE_LIMIT_EXCEEDED",
"message": "Monthly extraction limit reached (25/25). Upgrade your plan for more extractions."
}
}Server Errors (500)
Returned when something unexpected happens on the server side.
| Code | Description |
|---|---|
INTERNAL_ERROR | An unexpected server error occurred. These are automatically logged and investigated. |
EXTRACTION_FAILED | The AI processing pipeline encountered an error while extracting data from your document. The document may be corrupted, unreadable, or in an unsupported format. |
Retryable Errors
Not all errors should be retried. Here is a breakdown:
| Code | Retryable | Action |
|---|---|---|
INTERNAL_ERROR | Yes | Retry with exponential backoff |
EXTRACTION_FAILED | Yes | Retry -- the AI may produce a different result. If it persists, check the document quality |
USAGE_LIMIT_EXCEEDED | No | Upgrade your plan or wait until the next billing period |
UNAUTHORIZED | No | Fix your authentication credentials |
KEY_EXPIRED | No | Create a new API key |
FORBIDDEN | No | You are accessing a resource you do not own |
NOT_FOUND | No | The resource does not exist -- verify the ID |
VALIDATION_ERROR | No | Fix the request body to match the expected schema |
MAX_KEYS_REACHED | No | Revoke an existing key before creating a new one |
Retry Strategy
For retryable errors, use exponential backoff to avoid overwhelming the server. Here is a reusable implementation:
interface ApiResponse<T> {
data?: T;
error?: { code: string; message: string };
}
async function fetchWithRetry<T>(
url: string,
options: RequestInit,
maxRetries = 3,
): Promise<ApiResponse<T>> {
let lastError: Error | null = null;
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
const response = await fetch(url, options);
const body = await response.json();
if (response.ok) {
return body as ApiResponse<T>;
}
const errorCode = body?.error?.code;
// Only retry server errors
if (
(errorCode === "INTERNAL_ERROR" || errorCode === "EXTRACTION_FAILED") &&
attempt < maxRetries
) {
const delayMs = 1000 * Math.pow(2, attempt); // 1s, 2s, 4s
console.log(`Retrying in ${delayMs}ms (attempt ${attempt + 1}/${maxRetries})...`);
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
// Non-retryable error -- return immediately
return body as ApiResponse<T>;
} catch (err) {
lastError = err as Error;
if (attempt < maxRetries) {
const delayMs = 1000 * Math.pow(2, attempt);
await new Promise((resolve) => setTimeout(resolve, delayMs));
continue;
}
}
}
throw lastError ?? new Error("Max retries exceeded");
}Usage:
const result = await fetchWithRetry<ExtractionResult>(
"https://api.docmap.io/v1/extractions/run",
{
method: "POST",
headers: {
Authorization: `Bearer ${apiKey}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
templateId: "tmpl_abc123",
fileName: "invoice.pdf",
pdfBase64: encodedPdf,
mimeType: "application/pdf",
}),
},
);
if (result.error) {
console.error(`Extraction failed: [${result.error.code}] ${result.error.message}`);
} else {
console.log("Extracted data:", result.data);
}Best Practices
Always check the HTTP status code first. A
2xxstatus means success; anything else is an error. Do not assume the response body has adatafield without checking.Parse the error body for the
codefield. Usecode(notmessage) in your control flow. Error messages may change over time, but error codes are stable.Handle specific error codes. Branch on known codes like
USAGE_LIMIT_EXCEEDEDorKEY_EXPIREDto show targeted messages to your users or trigger specific workflows.Log unknown errors. If you encounter an error code you do not handle explicitly, log the full response (status code, error code, and message) for debugging.
Do not retry 4xx errors. Authentication, authorization, validation, and not-found errors will not resolve on their own. Fix the underlying issue before retrying.
Set a retry limit. Never retry indefinitely. Three retries with exponential backoff (1s, 2s, 4s) is a reasonable default for server errors.
