Error Handling
Mandato uses conventional HTTP status codes and a consistent error response format across all endpoints. Every error includes a machine-readable type, a human-readable message, and a requestId for support reference.
Error response format
Section titled “Error response format”All errors follow this structure:
{ "error": { "type": "validation_error", "message": "Supplier VAT number is required", "details": [ { "path": "supplier.vatNumber", "message": "Required" } ], "requestId": "req_a1b2c3d4e5f6" }}Fields
Section titled “Fields”| Field | Type | Always present | Description |
|---|---|---|---|
type | string | Yes | Machine-readable error category |
message | string | Yes | Human-readable description |
details | array | No | Field-level validation errors (only for validation_error) |
requestId | string | Yes | Unique request ID for debugging and support |
HTTP status codes
Section titled “HTTP status codes”| Status | Type | Description |
|---|---|---|
400 | validation_error | Request body failed validation (missing fields, wrong types, invalid values) |
401 | authentication_error | Missing, invalid, revoked, or expired API key |
403 | forbidden_error | Valid key but insufficient permissions or plan limit exceeded |
403 | usage_limit_exceeded | Monthly invoice limit reached |
403 | limit_exceeded | Resource limit reached (e.g., max companies) |
404 | not_found | Resource does not exist or belongs to a different account |
409 | conflict_error | Idempotency key conflict or resource already exists |
429 | rate_limit_error | Too many requests — back off and retry |
500 | server_error | Internal server error — safe to retry |
502 | server_error | Bad gateway (upstream government system error) |
503 | server_error | Service temporarily unavailable — retry with backoff |
Error types in detail
Section titled “Error types in detail”Validation error (400)
Section titled “Validation error (400)”Returned when the request body does not pass schema validation. The details array contains per-field errors.
{ "error": { "type": "validation_error", "message": "Invalid request body", "details": [ { "path": "supplier.vatNumber", "message": "Required" }, { "path": "lines[0].unitPrice", "message": "Expected number, received string" }, { "path": "country", "message": "Invalid enum value. Expected 'RO' | 'IT' | 'BE' | 'PL' | 'FR' | 'DE'" } ], "requestId": "req_a1b2c3d4e5f6" }}How to handle: Fix the request body based on the details array. Each entry points to the exact field (path) and describes the issue (message).
Authentication error (401)
Section titled “Authentication error (401)”{ "error": { "type": "authentication_error", "message": "Invalid API key", "requestId": "req_a1b2c3d4e5f6" }}Common causes:
- Missing
Authorizationheader - Header format is wrong (must be
Bearer <key>) - API key has been revoked
- API key has expired
- Key does not exist
Forbidden error (403)
Section titled “Forbidden error (403)”{ "error": { "type": "usage_limit_exceeded", "message": "Monthly invoice limit reached (50). Upgrade your plan for more.", "limit": 50, "remaining": 0, "requestId": "req_a1b2c3d4e5f6" }}How to handle: Upgrade your plan or wait until the next billing cycle.
Not found error (404)
Section titled “Not found error (404)”{ "error": { "type": "not_found", "message": "Invoice not found", "requestId": "req_a1b2c3d4e5f6" }}Note: For security, Mandato returns 404 (not 403) when a resource exists but belongs to a different account. This prevents resource enumeration.
Conflict error (409)
Section titled “Conflict error (409)”{ "error": { "type": "conflict_error", "message": "API key is already revoked", "requestId": "req_a1b2c3d4e5f6" }}Also returned for idempotency key conflicts — when you submit a different request body with the same X-Idempotency-Key.
Rate limit error (429)
Section titled “Rate limit error (429)”{ "error": { "type": "rate_limit_error", "message": "Rate limit exceeded. Retry after 30 seconds.", "requestId": "req_a1b2c3d4e5f6" }}Response headers include rate limit details:
X-RateLimit-Limit: 60X-RateLimit-Remaining: 0X-RateLimit-Reset: 1705312860Retry-After: 30How to handle: Wait for the Retry-After duration and retry. The SDK handles this automatically with exponential backoff.
Server error (500/502/503)
Section titled “Server error (500/502/503)”{ "error": { "type": "server_error", "message": "Internal server error", "requestId": "req_a1b2c3d4e5f6" }}How to handle: Retry with exponential backoff. The SDK does this automatically (up to 3 retries). If the issue persists, contact support with the requestId.
Handling errors with the SDK
Section titled “Handling errors with the SDK”The Mandato SDK throws typed error classes that you can catch and handle:
import { MandatoClient, MandatoError, ValidationError, AuthenticationError, NotFoundError, RateLimitError, ServerError,} from "@getmandato/sdk";
const mandato = new MandatoClient({ apiKey: "sk_test_your_key" });
try { const { data: invoice } = await mandato.invoices.create({ country: "RO", supplier: { vatNumber: "RO12345678", name: "TechVision SRL" }, customer: { vatNumber: "RO87654321", name: "Client SRL" }, lines: [{ description: "Consulting", unitPrice: 1000, vatRate: 19 }], }); console.log("Invoice created:", invoice.id);} catch (error) { if (error instanceof ValidationError) { // 400 -- fix the request body console.error("Validation failed:", error.message); for (const detail of error.details) { console.error(` ${detail.path}: ${detail.message}`); } } else if (error instanceof AuthenticationError) { // 401 -- check your API key console.error("Auth failed:", error.message); } else if (error instanceof NotFoundError) { // 404 -- resource not found console.error("Not found:", error.message); } else if (error instanceof RateLimitError) { // 429 -- SDK already retried, limit still exceeded console.error(`Rate limited. Retry after ${error.retryAfter}s`); } else if (error instanceof ServerError) { // 500+ -- SDK already retried console.error("Server error:", error.message); } else if (error instanceof MandatoError) { // Other API error console.error(`API error (${error.status}): ${error.message}`); } else { // Network error or unexpected issue console.error("Unexpected error:", error); }
// Every MandatoError has a requestId for support if (error instanceof MandatoError) { console.error("Request ID:", error.requestId); }}Error class hierarchy
Section titled “Error class hierarchy”MandatoError (base) ├── ValidationError (400) ├── AuthenticationError (401) ├── ForbiddenError (403) ├── NotFoundError (404) ├── ConflictError (409) ├── RateLimitError (429) └── ServerError (500+)All error classes extend MandatoError and include:
| Property | Type | Description |
|---|---|---|
message | string | Human-readable error message |
status | number | HTTP status code |
type | string | Machine-readable error type |
requestId | string | Request ID for support reference |
ValidationError additionally includes:
| Property | Type | Description |
|---|---|---|
details | array | Per-field validation errors |
RateLimitError additionally includes:
| Property | Type | Description |
|---|---|---|
limit | number | Requests per minute allowed |
retryAfter | number | Seconds to wait before retrying |
Automatic retries
Section titled “Automatic retries”The SDK automatically retries requests that fail with 429 or 5xx status codes, using exponential backoff:
| Attempt | Delay |
|---|---|
| 1st retry | 500ms |
| 2nd retry | 1 second |
| 3rd retry | 2 seconds |
The maximum number of retries defaults to 3 and is configurable:
const mandato = new MandatoClient({ apiKey: "sk_test_your_key", maxRetries: 5, // Up to 5 retries timeout: 60_000, // 60-second timeout per request});Non-retryable errors (400, 401, 403, 404, 409) are thrown immediately without retrying.
Government error translation
Section titled “Government error translation”When a government system (ANAF, SDI, KSeF, etc.) rejects an invoice, the raw error is often in the local language and uses technical codes. Mandato provides AI-powered translation:
| Field | Description |
|---|---|
errorMessage | Raw government error (original language) |
errorTranslated | Plain English translation |
errorFix | Actionable suggestion to fix the issue |
const { data: invoice } = await mandato.invoices.get("inv_rejected_id");
if (invoice.status === "rejected") { console.log("Raw error:", invoice.errorMessage); // "CUI cumparator invalid - nu este inregistrat in scopuri de TVA"
console.log("Translated:", invoice.errorTranslated); // "Customer VAT number is not registered for VAT purposes in Romania"
console.log("Suggested fix:", invoice.errorFix); // "Verify the customer's CUI at anaf.ro. If the customer is not VAT-registered, // use a B2C invoice format instead."}This feature is available for all supported countries. The translation is generated automatically when a rejection is received.
Best practices
Section titled “Best practices”-
Always check the
requestId— Include it in support tickets for fast diagnosis. -
Handle
ValidationErrordetails — Show per-field errors in your UI so users can fix issues before resubmitting. -
Use the validate endpoint — Call
POST /v1/invoices/validatebefore creating invoices to catch errors early without consuming quota. -
Let the SDK handle retries — Do not implement your own retry logic for rate limits and server errors. The SDK handles this with proper backoff.
-
Log errors structurally — Include
error.type,error.status, anderror.requestIdin your logs for debugging.
try { await mandato.invoices.create(invoiceData);} catch (error) { if (error instanceof MandatoError) { logger.error("Mandato API error", { type: error.type, status: error.status, message: error.message, requestId: error.requestId, }); }}