Skip to content

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.

All errors follow this structure:

{
"error": {
"type": "validation_error",
"message": "Supplier VAT number is required",
"details": [
{
"path": "supplier.vatNumber",
"message": "Required"
}
],
"requestId": "req_a1b2c3d4e5f6"
}
}
FieldTypeAlways presentDescription
typestringYesMachine-readable error category
messagestringYesHuman-readable description
detailsarrayNoField-level validation errors (only for validation_error)
requestIdstringYesUnique request ID for debugging and support
StatusTypeDescription
400validation_errorRequest body failed validation (missing fields, wrong types, invalid values)
401authentication_errorMissing, invalid, revoked, or expired API key
403forbidden_errorValid key but insufficient permissions or plan limit exceeded
403usage_limit_exceededMonthly invoice limit reached
403limit_exceededResource limit reached (e.g., max companies)
404not_foundResource does not exist or belongs to a different account
409conflict_errorIdempotency key conflict or resource already exists
429rate_limit_errorToo many requests — back off and retry
500server_errorInternal server error — safe to retry
502server_errorBad gateway (upstream government system error)
503server_errorService temporarily unavailable — retry with backoff

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).

{
"error": {
"type": "authentication_error",
"message": "Invalid API key",
"requestId": "req_a1b2c3d4e5f6"
}
}

Common causes:

  • Missing Authorization header
  • Header format is wrong (must be Bearer <key>)
  • API key has been revoked
  • API key has expired
  • Key does not exist
{
"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.

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

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

{
"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: 60
X-RateLimit-Remaining: 0
X-RateLimit-Reset: 1705312860
Retry-After: 30

How to handle: Wait for the Retry-After duration and retry. The SDK handles this automatically with exponential backoff.

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

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);
}
}
MandatoError (base)
├── ValidationError (400)
├── AuthenticationError (401)
├── ForbiddenError (403)
├── NotFoundError (404)
├── ConflictError (409)
├── RateLimitError (429)
└── ServerError (500+)

All error classes extend MandatoError and include:

PropertyTypeDescription
messagestringHuman-readable error message
statusnumberHTTP status code
typestringMachine-readable error type
requestIdstringRequest ID for support reference

ValidationError additionally includes:

PropertyTypeDescription
detailsarrayPer-field validation errors

RateLimitError additionally includes:

PropertyTypeDescription
limitnumberRequests per minute allowed
retryAfternumberSeconds to wait before retrying

The SDK automatically retries requests that fail with 429 or 5xx status codes, using exponential backoff:

AttemptDelay
1st retry500ms
2nd retry1 second
3rd retry2 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.

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:

FieldDescription
errorMessageRaw government error (original language)
errorTranslatedPlain English translation
errorFixActionable 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.

  1. Always check the requestId — Include it in support tickets for fast diagnosis.

  2. Handle ValidationError details — Show per-field errors in your UI so users can fix issues before resubmitting.

  3. Use the validate endpoint — Call POST /v1/invoices/validate before creating invoices to catch errors early without consuming quota.

  4. 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.

  5. Log errors structurally — Include error.type, error.status, and error.requestId in 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,
});
}
}