# Errors

Every failure has the same shape: an HTTP status, and an `error` object with a stable `code` and a
`message` that says how to fix it.

```json
{
  "success": false,
  "error": {
    "code": "VALIDATION_ERROR",
    "message": "One of first_name or company_name is required",
    "details": { "fieldErrors": { "email": ["Invalid email"] } }
  }
}
```

**Branch on `code`, never on `message`.** We improve the wording of messages all the time. `details`
is there when it helps: the fields that failed validation, the stage requirements a deal does not
meet yet, or the limit that refused a key.

Each endpoint in the [API reference](/reference) lists the codes it can return. The tables below
are the ones you will meet most.

## Codes you will see everywhere

| Status | Code                  | What happened                                                                                     |
| ------ | --------------------- | ------------------------------------------------------------------------------------------------- |
| `400`  | `VALIDATION_ERROR`    | The body or a query parameter is not valid. `details.fieldErrors` names each field, and `message` says the fix. |
| `400`  | `MALFORMED_JSON`      | The body is not valid JSON.                                                                       |
| `400`  | `BAD_REQUEST`         | An id in the path is not a valid id.                                                              |
| `401`  | `UNAUTHORIZED`        | The key is missing, malformed, or revoked, or its broker is deactivated. See [Authentication](/authentication). |
| `403`  | `READ_ONLY_KEY`       | The key is read-only, and this is a write.                                                        |
| `403`  | `KEY_LIMITED`         | A limit on the key refuses this request. `details.limit` names it. See [Key limits](/authentication#limits-on-a-key). |
| `403`  | `VIEW_ONLY`           | The key's broker has view-only access, and this is a write.                                       |
| `403`  | `SANDBOX_REFUSED`     | A sandbox cannot do this, such as billing or phone calls. See [Sandbox](/sandbox).                |
| `403`  | `FORBIDDEN`           | The person your key acts as cannot do this in BrokerPlus either, such as send from the connected mailbox. |
| `403`  | `POLICY_BLOCKED`      | Your brokerage's automation policy does not allow messages to clients to be asked for this way. An admin changes it in BrokerPlus. |
| `404`  | `NOT_FOUND`           | The record does not exist, or your key cannot see it. See below.                                  |
| `409`  | `CONFLICT`            | The record changed while we checked it, or another system controls it. Read it again first.       |
| `409`  | `APPROVAL_TASK`       | The task asks the broker to approve a BrokerPlus suggestion. Only the broker decides it, in BrokerPlus. |
| `409`  | `NOT_SENT`            | A request that [reaches a client](/reaching-clients) could not go out as it stands, and nothing was sent. `message` says why. |
| `410`  | `CONTACT_MERGED`      | The contact was merged into another one. `details.merged_into_id` is the one it became.           |
| `412`  | `EMAIL_NOT_CONNECTED` | The broker has no connected mailbox, so nothing can be emailed as them.                           |
| `412`  | `SMS_NOT_CONNECTED`   | The broker has no phone number, so nothing can be texted as them.                                 |
| `413`  | `FILE_TOO_LARGE`      | The upload is over the size limit.                                                                |
| `429`  | `RATE_LIMITED`        | More than 600 requests in a minute. Wait for `Retry-After` seconds.                               |
| `503`  | `SERVICE_UNAVAILABLE`, `TRY_AGAIN` | BrokerPlus is briefly busy and did nothing. Wait a moment and send the same request again.        |
| `5xx`  | `INTERNAL_ERROR`      | Something failed on our side, but a record might already have changed. Check before you retry.    |

## Codes for one area

| Area          | Codes                                                                                                                                 |
| ------------- | ------------------------------------------------------------------------------------------------------------------------------------- |
| Idempotency and batch | `IDEMPOTENCY_KEY_IN_FLIGHT`, `IDEMPOTENCY_KEY_CLAIMED`, `IDEMPOTENCY_KEY_REUSED`, `EXTERNAL_ID_TAKEN`, `EXTERNAL_ID_IN_FLIGHT`. See [Idempotency and batch](/idempotency). |
| Change feed   | `CHECKPOINT_EXPIRED` (`410`): the checkpoint is older than 30 days. See [Staying in sync](/sync).                                      |
| Contacts      | `DUPLICATE_CONTACT` (you sent `check_duplicates: true` and the person is in the book), `ALREADY_EXISTS`, `ALREADY_MERGED`, `MERGE_CONFLICT`, `CLIENT_DATA_LOCKED`, `NO_EMAIL_ON_FILE`, `NO_PHONE_ON_FILE`. |
| Deals         | `INVALID_PARTY`, `INVALID_BOARD`, `INVALID_STAGE`, `INVALID_LENDER`, `BROKER_HAS_NO_BOARDS`, `STAGE_REQUIREMENTS_UNMET`, `DEAL_LOCKED`, `PARTY_ALREADY_ON_DEAL`, `HAS_PROFILE`, `AML_GATE_BLOCKED`. |
| Commission    | `COMMISSION_ACCESS`, `NO_COMMISSION`, `COMMISSION_EXISTS`, `INVALID_BROKER`.                                                           |
| Tasks         | `INVALID_DEAL`, `INVALID_CONTACT`, `INVALID_ASSIGNEE`, `NO_TEAM`, `ALREADY_COMPLETED`, `ALREADY_CANCELLED`, `ALREADY_OPEN`.            |
| Scheduling    | `SLOT_TAKEN`, `HOST_DOUBLE_BOOKED`, `MEETING_TYPE_DISABLED`, `BOOKING_CANCELED`, `NOT_A_BOOKING`, `CONTACT_INCOMPLETE`.                |
| Conditions    | `REVIEW_REQUIRED`: review the lender email in BrokerPlus before you change the condition.                                             |
| Consents      | `ALREADY_REVOKED`.                                                                                                                    |
| Suggestions   | `APPROVE_IN_BROKERPLUS`: the suggestion reaches a client, so the broker approves it in BrokerPlus.                                     |
| Sandbox       | `SANDBOX_ONLY`: the endpoint works only with a sandbox key.                                                                           |
| Other         | `INVALID_PROVINCE`, `INVALID_ROLE_LABEL`, `INVALID_APPROVAL_NUMBER`, `LIMIT_REACHED`.                                                  |

The reference entry for each endpoint says what each of these means there.

## A 404 doesn't always mean deleted

A record your key cannot see looks exactly like one that does not exist. A record from another
brokerage, from a team the broker is not on, or one that was moved or deleted, all return `404`.
That is on purpose: the API never confirms that a record exists to a key that cannot read it.

`GET /v1/me` tells you which brokerage and broker your key acts as. Start there when a `404`
surprises you.

## What to retry

| Failure                         | Retry?                                                                                       |
| ------------------------------- | -------------------------------------------------------------------------------------------- |
| `429 RATE_LIMITED`              | Yes, after `Retry-After` seconds.                                                            |
| `503`                           | Yes, the same request, after a short wait. Back off exponentially if it repeats.             |
| `409 CONFLICT`                  | Read the record again, check the change still makes sense, then send it.                     |
| A timeout, or a `5xx`           | Check whether the record changed first. A server error does not prove nothing happened. With an [`Idempotency-Key`](/idempotency), resend the same request with the same key. |
| Any other `4xx`                 | No. Fix the request, the key, or the data first. Retrying unchanged gets the same answer.     |
