Error Handling
This guide covers the error responses returned by the Mandaitor API, how to handle them in your application, and recommended retry strategies.
Error Response Format
All error responses follow a consistent JSON structure:
{
"error": "ERROR_CODE",
"message": "A human-readable description of the error"
}
The error field contains a machine-readable error code (uppercase with underscores), while message provides additional context for debugging.
HTTP Status Codes
The Mandaitor API uses standard HTTP status codes to indicate the outcome of a request:
| Status Code | Error Code | Description | Retryable |
|---|---|---|---|
400 | BAD_REQUEST | The request body is malformed or missing required fields | No |
401 | UNAUTHORIZED | Authentication failed — missing or invalid API key | No |
403 | FORBIDDEN | The API key does not have the required scope | No |
404 | NOT_FOUND | The requested resource (mandate, tenant, event) does not exist | No |
409 | CONFLICT | State conflict — e.g., revoking an already-revoked mandate | No |
429 | TOO_MANY_REQUESTS | Rate limit exceeded — slow down and retry | Yes |
500 | INTERNAL_ERROR | An unexpected server error occurred | Yes |
503 | SERVICE_UNAVAILABLE | The service is temporarily unavailable | Yes |
Common Error Scenarios
400 Bad Request
Returned when the request body is invalid. The message field describes what is wrong:
{
"error": "BAD_REQUEST",
"message": "principal and delegate are required"
}
How to fix: Check your request body against the API Reference. Ensure all required fields are present and have valid values.
401 Unauthorized
Returned when authentication fails:
{
"error": "UNAUTHORIZED",
"message": "Invalid or missing API key"
}
How to fix: Verify that you are sending the X-Api-Key header with a valid, non-revoked API key. See the Authentication Guide for details.
404 Not Found
Returned when the requested resource does not exist:
{
"error": "NOT_FOUND",
"message": "Mandate mdt_01HXY... not found"
}
How to fix: Verify the resource ID. Note that mandates are scoped to your tenant — you cannot access mandates belonging to other tenants.
409 Conflict
Returned when an operation conflicts with the current state of a resource. This commonly occurs during mandate lifecycle transitions:
{
"error": "CONFLICT",
"message": "Mandate is already revoked"
}
How to fix: Check the current state of the mandate before attempting lifecycle operations. Valid state transitions are:
| Current State | Allowed Transitions |
|---|---|
ACTIVE | SUSPENDED, REVOKED |
SUSPENDED | ACTIVE (reactivate), REVOKED |
REVOKED | None (terminal state) |
EXPIRED | None (terminal state) |
429 Too Many Requests
Returned when you exceed the rate limit for your usage plan:
{
"error": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded"
}
How to fix: Implement exponential backoff (see below). For sustained high throughput, consider upgrading to the Enterprise plan. See the Rate Limiting Guide for details.
Handling Errors in the SDK
The Mandaitor SDK throws typed errors that you can catch and handle:
import { MandaitorClient } from "@mandaitor/sdk";
const client = new MandaitorClient({
apiKey: process.env.MANDAITOR_API_KEY!,
tenantId: process.env.MANDAITOR_TENANT_ID!,
});
try {
const mandate = await client.createMandate(request);
} catch (error) {
if (error instanceof Error) {
// Access the error message
console.error("Mandate creation failed:", error.message);
}
}
Differentiating error types
A recommended pattern for handling different error types:
async function safeCreateMandate(request: CreateMandateRequest) {
try {
return await client.createMandate(request);
} catch (error: any) {
const status = error?.status ?? error?.response?.status;
switch (status) {
case 400:
// Validation error — fix the request
console.error("Invalid request:", error.message);
throw error;
case 401:
// Auth error — check API key
console.error("Authentication failed — check your API key");
throw error;
case 409:
// Conflict — mandate may already exist
console.warn("Conflict:", error.message);
return null;
case 429:
// Rate limited — retry with backoff
console.warn("Rate limited, retrying...");
await sleep(1000);
return client.createMandate(request);
case 500:
case 503:
// Server error — retry with backoff
console.warn("Server error, retrying...");
await sleep(2000);
return client.createMandate(request);
default:
throw error;
}
}
}
Retry Strategy
For retryable errors (429, 500, 503), use exponential backoff with jitter:
async function withRetry<T>(
fn: () => Promise<T>,
maxRetries: number = 3,
baseDelay: number = 1000
): Promise<T> {
for (let attempt = 0; attempt <= maxRetries; attempt++) {
try {
return await fn();
} catch (error: any) {
const status = error?.status ?? error?.response?.status;
const isRetryable = [429, 500, 503].includes(status);
if (!isRetryable || attempt === maxRetries) {
throw error;
}
// Exponential backoff with jitter
const delay = baseDelay * Math.pow(2, attempt);
const jitter = delay * 0.5 * Math.random();
await new Promise((resolve) => setTimeout(resolve, delay + jitter));
}
}
throw new Error("Unreachable");
}
// Usage
const mandate = await withRetry(() => client.createMandate(request));
Recommended retry parameters
| Parameter | Value | Rationale |
|---|---|---|
| Max retries | 3 | Sufficient for transient errors |
| Base delay | 1000 ms | Allows the rate limiter to reset |
| Backoff factor | 2x | Standard exponential backoff |
| Jitter | 0–50% of delay | Prevents thundering herd |
Idempotency
Mandate creation is not idempotent — calling createMandate twice with the same parameters will create two separate mandates. If you need to ensure exactly-once creation, implement idempotency on your side by checking for existing mandates before creating new ones.
Verification requests (POST /verify) are inherently idempotent — calling them multiple times with the same parameters always returns the same decision (assuming the mandate state has not changed).