---
title: "Errors and limits"
description: "Every error code the API returns, what it means, whether to retry, and the limits behind the 429s."
canonical_url: "https://www.agent-identity.dev/docs/api/errors"
markdown_url: "https://www.agent-identity.dev/docs/api/errors.md"
last_updated: "2026-10-11"
x_farming_labs_generated_preamble: true
agent:
  tokenBudget: 1000
  task: "Work out why a request failed from its error code, and whether retrying will help."
  outcome: "The caller branches on error.code (not the message or only the status), retries only what is retryable, and reads usage before hitting a limit."
  appliesTo:
    package:
      - "@aid/api"
      - "@agentidentity/sdk"
  prerequisites:
    - "A request that returned a non-2xx status."
  files:
    - "apps/api/src/app.ts"
  verification:
    - description: "Read the error body."
      expect: "A JSON object { error: { code, message, request_id, retryable } }."
  failureModes:
    - symptom: "A retry loop never succeeds."
      resolution: "Only retry when error.retryable is true or the status is 429 with Retry-After. Everything else needs a different request."
---

# Errors and limits
URL: /docs/api/errors
LLM index: /llms.txt
Description: Every error code the API returns, what it means, whether to retry, and the limits behind the 429s.
Related: /docs/api, /docs/sdk, /docs/getting-started/authentication

<!-- farming-labs:agent-contract:start -->
## Agent Contract

Task: Work out why a request failed from its error code, and whether retrying will help.
Outcome: The caller branches on error.code (not the message or only the status), retries only what is retryable, and reads usage before hitting a limit.

### Applies To

- Package: `@aid/api`, `@agentidentity/sdk`

### Prerequisites

- A request that returned a non-2xx status.

### Files

- `apps/api/src/app.ts`

### Verification

- Read the error body.
  - Expected: A JSON object { error: { code, message, request_id, retryable } }.

### Failure Modes

- A retry loop never succeeds. — Recovery: Only retry when error.retryable is true or the status is 429 with Retry-After. Everything else needs a different request.
<!-- farming-labs:agent-contract:end -->

# Errors and limits

Every error has the same body:

```json
{
  "error": {
    "code": "mail.outbound_quota_exceeded",
    "message": "the daily send limit for this organization is used up",
    "request_id": "req-7f3",
    "retryable": false
  }
}
```

Branch on `code`. The `message` is for people and can change. `retryable` is decided by the
server, so honour it instead of guessing from the status. Quote `request_id` when you report
a problem. In the SDK the same fields are on `AidApiError` as `code`, `message`, `requestId` and
`retryable`.

## Codes

### Requests and keys

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `validation.invalid_request` | The body, query or path is wrong. The message names the field. |
| 400 | `validation.invalid_request` | A webhook or agent URL points at a private or internal address. |
| 401 | `auth.unauthorized` | No key, or the key is revoked, expired or for a suspended organization. |
| 401 | `auth.invalid_credentials` | Wrong email or password when signing in. |
| 403 | `auth.forbidden` | Right key type, wrong permission — often an agent key where an org key is needed, or the reverse. |
| 403 | `org.not_verified` | The organization's owner has not verified their email yet. Sending to strangers, getting a phone number and calls across organizations wait for it. |
| 404 | `resource.not_found` / `not_found` | It does not exist, **or it belongs to someone else**. Never retry a 404. |
| 409 | `idempotency.conflict` | The same `idempotency-key` was used with a different body. |
| 409 | `identity.conflict` | The agent cannot be retired because it is already retired. |
| 409 | `idempotency.in_progress` | The first request with this key is still running. Retry shortly. |
| 429 | `rate_limit.exceeded` | Too many requests. Wait for `Retry-After`. |
| 500 | `internal.error` | Our fault. Retry with backoff and report the `request_id`. |

### Agents and verification

| Status | Code | Meaning |
| --- | --- | --- |
| 403 | `identity.limit_reached` | The organization holds as many agents as its plan allows. |
| 400 | `verification.not_pending` | There is no code waiting to be verified. |
| 403 | `verification.invalid_code` | The code is wrong. |
| 410 | `verification.expired` | The code expired. Ask for a new one. |
| 429 | `verification.attempts_exhausted` | Too many wrong codes. Ask for a new one. |

### Email

| Status | Code | Meaning |
| --- | --- | --- |
| 400 | `mail.provider_rejected` | The email provider refused the message (for example, an invalid recipient). |
| 403 | `mail.unverified_recipient` | Until the owner is verified, mail can only go to the owner's own address. |
| 422 | `mail.recipient_blocked` | An access rule blocks this recipient. |
| 422 | `mail.storage_limit_reached` | The organization's mail storage is full. |
| 429 | `mail.outbound_quota_exceeded` | The daily send limit is used up. It resets at 00:00 UTC. |
| 429 | `mail.free_tier_recipient_limit` | The free plan's recipient limit for today is reached. |
| 502 | `mail.provider_unavailable` | The email provider is down. Retry later. |

### Phone

| Status | Code | Meaning |
| --- | --- | --- |
| 409 | `phone.conflict` | For example, the agent already has a number, the number is taken, or the call has already ended (`call_already_ended`). |
| 429 | `phone.limit_exceeded` | A daily call or minute limit, or the live-call limit, is reached. Carries `Retry-After`. |
| 502 | `phone.provider_unavailable` | The phone carrier refused or could not be reached. |
| 503 | `phone.not_configured` | Phone is not set up on this server. |

### Agent2Agent

| Status | Code | Meaning |
| --- | --- | --- |
| 409 | `a2a.task_terminal` | The task already finished, so it takes no more replies or messages. |
| 409 | `a2a.limit_reached` | A task holds at most 500 messages, and a conversation at most 100 tasks. Start a new one. |
| 409 | `a2a.conflict` | The same access rule already exists. |
| 409 | `a2a.invitation_closed` | The connect request was already accepted, declined, revoked or has expired. |
| 429 | `a2a.invitation_limit` | Too many connect requests sent in the last hour or day. |

### Vault, webhooks and billing

| Status | Code | Meaning |
| --- | --- | --- |
| 409 | `vault.lease_already_consumed` | A lease can be used once. |
| 410 | `vault.lease_expired` | The lease ran out. Ask for a new one. |
| 401 | `webhook.invalid_signature` | An inbound webhook did not carry a valid signature. |
| 401 | `billing.invalid_webhook_signature` | Same, for billing webhooks. |
| 503 | `billing.not_configured` | Billing is not set up on this server. |
| 409 | `billing.already_subscribed` | The organization already pays. Change plans in the billing portal instead of a new checkout. |
| 409 | `billing.no_subscription` | There is no subscription to manage yet. |

## Limits

**Request rate.** By default an organization may make 600 requests a minute across all its
keys. Requests without a key, such as signup and the public directory, are limited to 30 a
minute per network address. Over the limit is `429 rate_limit.exceeded` with `Retry-After`.

**What your plan allows.** Agents, emails a day, mail storage, phone numbers, calls and minutes
a day, live calls, call length and Agent2Agent tasks a day all depend on the organization's plan. Read where you stand
instead of finding out from a 429:

```bash title="terminal"
curl -sS https://api.agent-identity.dev/v1/usage -H "authorization: Bearer $AID_KEY"
```

```ts title="usage.ts"
const usage = await client.usage();
usage.mail.remaining;            // emails left today
usage.identities;                // { count, limit }
usage.phone.numbers;             // { count, limit }
usage.phone.minutesLimit;        // and callsToday, minutesToday, callsLimit
usage.a2a;                       // { tasksToday, tasksLimit }
```

Phone limits are explained in [Phone limits](/docs/phone/limits), and Agent2Agent's in
[Tasks](/docs/a2a/tasks).

## Retrying

| Situation | Retry? |
| --- | --- |
| `retryable: true` | Yes, with backoff. |
| `429` with `Retry-After` | Yes, after waiting that long. |
| `idempotency.in_progress` | Yes, shortly, with the same key. |
| A network error (`AidConnectionError` in the SDK) | Yes, with the same `idempotency-key` so nothing happens twice. |
| Any other `4xx` | No — change the request. |

## Sitemap

See the full [sitemap](/sitemap.md) for all pages.
Docs-scoped sitemap: [/docs/sitemap.md](/docs/sitemap.md).
Well-known sitemap: [/.well-known/sitemap.md](/.well-known/sitemap.md).
