Skip to main content

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 CodeError CodeDescriptionRetryable
400BAD_REQUESTThe request body is malformed or missing required fieldsNo
401UNAUTHORIZEDAuthentication failed — missing or invalid API keyNo
403FORBIDDENThe API key does not have the required scopeNo
404NOT_FOUNDThe requested resource (mandate, tenant, event) does not existNo
409CONFLICTState conflict — e.g., revoking an already-revoked mandateNo
429TOO_MANY_REQUESTSRate limit exceeded — slow down and retryYes
500INTERNAL_ERRORAn unexpected server error occurredYes
503SERVICE_UNAVAILABLEThe service is temporarily unavailableYes

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 StateAllowed Transitions
ACTIVESUSPENDED, REVOKED
SUSPENDEDACTIVE (reactivate), REVOKED
REVOKEDNone (terminal state)
EXPIREDNone (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));
ParameterValueRationale
Max retries3Sufficient for transient errors
Base delay1000 msAllows the rate limiter to reset
Backoff factor2xStandard exponential backoff
Jitter0–50% of delayPrevents 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).