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
503AUDIT_UNAVAILABLE/verify, /decisions or /executions reached a decision but could not record it in the audit log, so it was not returned. Honour Retry-After; the retry gets a fresh decisionYes

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 a platform or per-route rate limit:

{
"error": "TOO_MANY_REQUESTS",
"message": "Rate limit exceeded"
}

How to fix: Implement exponential backoff (see below). If your integration legitimately needs more than the per-address allowance, contact support to register your egress range. 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).