# Errors and Retries

Prontuno returns structured JSON errors so your integration can decide whether to fix a request, ask for user action, or retry safely.

## Error format

A typical error contains a stable machine-readable `code`, a human-readable `message`, and retry guidance:

```
{
    "error": {
        "code": "rate_limit_exceeded",
        "message": "Rate limit exceeded",
        "retryable": true,
        "suggested_action": "retry_later",
        "retry_after_seconds": 12
    }
}
```

Use `error.code` for application logic. Do not match on `error.message`, which is intended for people and may become more descriptive over time.

Validation failures may also include an `errors` array with one entry per invalid field:

```
{
    "error": {
        "code": "validation_failed",
        "message": "Request validation failed"
    },
    "errors": [
        {
            "code": "missing_subject",
            "message": "subject is required",
            "field": "subject",
            "retryable": false,
            "suggested_action": "fix_input"
        }
    ]
}
```

For batch validation, an error entry can also include the zero-based `index` of the invalid email.

## HTTP status codes

- Status | Meaning | What to do
- `400` | The JSON or one or more request values are invalid. | Fix the request before trying again.
- `401` | The API key is missing or invalid. | Correct or replace the key.
- `402` | The team does not have enough credits for the request. | Top up credits, then submit the operation again.
- `403` | The key lacks a scope, or sending is disabled for the team or domain. | Correct permissions or contact support.
- `404` | The route or team-scoped resource was not found. | Check the endpoint and identifier.
- `409` | An idempotency request is still in progress, or the key was reused with a different body. | Follow the error code and `Retry-After` guidance.
- `413` | The JSON request body exceeds 15 MB. | Reduce the request or attachment size.
- `415` | The request body is not marked as JSON. | Send `Content-Type: application/json`.
- `422` | A value is well-formed but cannot be processed, such as an invalid cursor. | Replace the invalid value.
- `429` | The team exceeded its request rate limit. | Wait for `Retry-After`, then retry.
- `500` | An unexpected server error occurred. | Retry a safe, idempotent request with backoff.
- `503` | A required service is temporarily unavailable or the request could not be queued. | Follow `Retry-After` when present and retry with backoff.

## When to retry

Use the response body rather than the status code alone:

- Retry only when `error.retryable` is `true`.
- Wait at least the number of seconds in `Retry-After` or `error.retry_after_seconds`.
- Use exponential backoff with random jitter for repeated `429`, `500`, or `503` responses.
- Set a maximum attempt count or elapsed-time limit. Do not retry forever.
- For send operations, reuse the same `Idempotency-Key` and the unchanged JSON body on every retry.

A practical retry sequence is 2 seconds, 4 seconds, 8 seconds, 16 seconds, and then 30 seconds, adding a small random delay to each attempt. If the server returns a longer `Retry-After` value, use that value instead.

## Idempotency conflicts

- `idempotency_request_in_progress` is temporary. Wait for `Retry-After`, then resend the same body with the same key.
- `idempotency_key_conflict` is permanent for that key. The key has already been paired with a different request body. Do not retry it; use a new key for the new logical operation.

## Request IDs and support

Every response includes an `x-request-id` header. Log the request ID together with the HTTP status and `error.code`. If a problem persists, send those values to [Prontuno Support](https://prontuno.com/contact/). Never send your API key.
