---
title: "Errors"
description: "The Matchai API error format, every error code with its HTTP status and what to do about it, which requests are safe to retry, and how to use request ids."
last_updated: "2026-10-21"
lang: "en"
status: "coming-soon"
canonical: "https://matchai.co/docs/developers/errors"
---

# Errors

> The Matchai API error format, every error code with its HTTP status and what to do about it, which requests are safe to retry, and how to use request ids.

**Coming soon. This is documented ahead of launch so you can plan your integration; it is not available yet.**

Source: https://matchai.co/docs/developers/errors

Every error from the Matchai API has the same JSON shape. Branch on the stable `code`, and show the `message` to the person using your app: it says what happened and what to do next.

## The error format [#the-error-format]

```json title="Response (400)"
{
  "error": {
    "type": "invalid_request_error",
    "code": "parameter_invalid",
    "message": "price.amount must be greater than 0.",
    "param": "price.amount",
    "doc_url": "https://matchai.co/docs/developers/errors#parameter_invalid",
    "request_id": "req_7Hq2LmZ9xV4c"
  }
}
```

<TypeTable
  type={{
    type: { type: 'string', required: true, description: 'The broad kind of error: invalid_request_error, authentication_error, energy_error, permission_error, not_found_error, conflict_error, unprocessable_error, rate_limit_error or api_error.' },
    code: { type: 'string', required: true, description: 'A stable, machine-readable code, such as parameter_invalid or scope_missing. Every code is explained below.' },
    message: { type: 'string', required: true, description: 'What happened and what to do next, in plain words and in the account\'s language. Safe to show to a person.' },
    param: { type: 'string', description: 'The request field the error is about, when there is one. For contact_exists it holds the id of the existing contact.' },
    doc_url: { type: 'string', required: true, description: 'A link to the section of this page that explains the code.' },
    request_id: { type: 'string', description: 'The id of this request. The same id is in the Matchai-Request-Id header.' },
  }}
/>

Three habits make errors painless:

- **Branch on `code`, not on `message`.** Codes are stable. Messages are written for people, follow the account's language, and can be reworded.
- **Treat unknown values as unknown, not as a crash.** New codes and types can be added without a new version. If you meet a `code` you do not know, fall back to its `type` and its HTTP status. An unknown `type` is handled like `api_error`.
- **Log the request id.** See [request ids](#request-ids).

## Status codes [#status-codes]

| Status | Type | Meaning | Retry? |
|---|---|---|---|
| `202` | None | Not an error. A [final action](https://matchai.co/docs/developers/authentication#final-actions) is waiting for the account holder's approval. Nothing has happened yet. | No. Wait for `approval.resolved`. |
| `400` | `invalid_request_error` | The request is malformed or a parameter is invalid. | No. Fix the request. |
| `401` | `authentication_error` | The key or token is missing, expired, revoked or wrong. | After refreshing an expired token, once. |
| `402` | `energy_error` | Not enough Matchai Energy, or the connection reached its Energy limit. | No, not until there is Energy again. |
| `403` | `permission_error` | The credential is valid, but cannot do this. | No. |
| `404` | `not_found_error` | It does not exist, or this account cannot see it. | No. |
| `409` | `conflict_error` | It clashes with the current state. | No. Change the request. |
| `422` | `unprocessable_error` | The request is well formed but breaks a product rule. | No. Change the request. |
| `429` | `rate_limit_error` | Too many requests for this connection. | Yes, after `Retry-After` seconds. |
| `500` | `api_error` | Something went wrong on Matchai's side. | Yes, with the same `Idempotency-Key`. |

## Request ids [#request-ids]

Every response, successful or not, carries a `Matchai-Request-Id` header. Errors repeat it as `error.request_id`. Log it with every failed call. When you contact [Matchai support](https://matchai.co/support), include it: it lets Matchai find exactly what happened on that request.

```http title="Response header"
Matchai-Request-Id: req_7Hq2LmZ9xV4c
```

## Retry safely [#retry-safely]

Retrying is safe when the same request can be repeated without doing the action twice. That is what the `Idempotency-Key` header is for.

- Send an `Idempotency-Key` on every `POST`. A UUID works.
- A retry with the **same key** within 24 hours returns the first response instead of acting twice. A retry of a final action under **Ask me** returns the same pending approval.
- Reusing a key with a **different body** returns `409 idempotency_key_reused`. Make a new key for a new request.

Retry network errors and timeouts, `429` and `500`. Do not retry `400`, `403`, `404`, `409` or `422` unchanged: they fail the same way again. A `402` keeps failing until the account has Energy again or the account holder raises the connection's limit.

Wait before each retry: honour `Retry-After` when there is one, and otherwise wait longer each time, with a little randomness so that many clients do not retry together.

```js title="retry.mjs"
import { randomUUID } from 'node:crypto';

const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms));

// 1 s, 2 s, 4 s... plus up to a second of jitter.
const backoff = (attempt) => 2 ** (attempt - 1) * 1000 + Math.random() * 1000;

export async function postWithRetry(url, body, { attempts = 4 } = {}) {
  const idempotencyKey = randomUUID(); // The same key on every attempt.

  for (let attempt = 1; ; attempt += 1) {
    let response;
    try {
      response = await fetch(url, {
        method: 'POST',
        headers: {
          Authorization: `Bearer ${process.env.MATCHAI_API_KEY}`,
          'Content-Type': 'application/json',
          'Idempotency-Key': idempotencyKey,
        },
        body: JSON.stringify(body),
      });
    } catch (networkError) {
      if (attempt >= attempts) throw networkError;
      await sleep(backoff(attempt));
      continue;
    }

    const retryable = response.status === 429 || response.status >= 500;
    if (!retryable || attempt >= attempts) return response;

    const retryAfter = Number(response.headers.get('Retry-After'));
    await sleep(retryAfter > 0 ? retryAfter * 1000 : backoff(attempt));
  }
}
```

## Error codes [#error-codes]

Each code below is the target of the `doc_url` that comes with it.

### Invalid requests [#invalid-requests]

#### `parameter_invalid` [#parameter_invalid]

**`400` · `invalid_request_error`.** A field is missing, has the wrong type, or is out of range. `param` names the field, with dots for nested fields, such as `price.amount`. Fix the request: sending it again unchanged fails again.

Other `400` answers, such as a body that is not valid JSON, carry the same `type`. Treat any `400` as "this request is wrong".

### Authentication [#authentication]

#### `token_expired` [#token_expired]

**`401` · `authentication_error`.** The access token has expired. With [Connect with Matchai](https://matchai.co/docs/developers/authentication/connect-with-matchai), exchange your refresh token for a new access token, then repeat the request once. If the refresh fails, the connection has ended and the professional needs to connect again.

A `401` can also mean that the key or token is missing, mistyped or revoked, or that the connection was ended. Those carry the same `type`. Ask for a new [API key](https://matchai.co/docs/developers/authentication/api-keys), or send the professional through the connect flow again.

### Energy [#energy]

Matchai Energy is never a number in the API. These errors say only that there is not enough.

#### `insufficient_energy` [#insufficient_energy]

**`402` · `energy_error`.** The account does not have enough Matchai Energy for this action. Nothing was done and nothing was spent. The account holder can get more from the Matchai Store, and `GET /v1/me` reports the state of their Energy (`full`, `comfortable`, `low` or `empty`). Show the `message` to the person, and try again once they have topped up.

#### `energy_limit_reached` [#energy_limit_reached]

**`402` · `energy_error`.** This connection has used the Energy limit the account holder set for it. Nothing was done and nothing was spent. `GET /v1/me/connection` reports `energy_limit_reached: true`. The account holder decides whether to raise the limit; your app should not retry in a loop.

### Permissions [#permissions]

#### `scope_missing` [#scope_missing]

**`403` · `permission_error`.** The key or token is valid, but lacks the [scope](https://matchai.co/docs/developers/authentication#scopes) this action needs. The `message` names it. Create a new [API key](https://matchai.co/docs/developers/authentication/api-keys) that includes the scope, or send the professional through the [connect flow](https://matchai.co/docs/developers/authentication/connect-with-matchai) again with a longer scope list.

#### `not_allowed` [#not_allowed]

**`403` · `permission_error`.** The account's role does not allow this. The connection acts with the account holder's own permissions and no more, so something they cannot do in Matchai, such as editing a team's work as a member without edit rights, they cannot do through your app either. Tell the person, and do not retry.

### Not found [#not-found]

#### `resource_not_found` [#resource_not_found]

**`404` · `not_found_error`.** The thing does not exist, or this account cannot see it. Matchai gives the same answer for both, so a response never confirms that something exists in someone else's account. Check the id, and check that you are using the credential of the account you meant.

### Conflicts [#conflicts]

#### `contact_exists` [#contact_exists]

**`409` · `conflict_error`.** The email or phone number you sent already belongs to one of the account's contacts, so nothing was added. The id of the existing contact is in `error.param`. Update that contact instead, or use `POST /v1/enquiries`, which matches existing contacts for you.

#### `subdomain_taken` [#subdomain_taken]

**`409` · `conflict_error`.** The subdomain you asked for is not available. Subdomains use lowercase letters, digits and hyphens. Choose another and try again.

#### `calendar_conflict` [#calendar_conflict]

**`409` · `conflict_error`.** The time overlaps another appointment, and the response lists the appointments that clash. Offer another time. Only send `book_over_conflict: true` once the account holder has agreed to the clash.

#### `slot_unavailable` [#slot_unavailable]

**`409` · `conflict_error`.** The time you asked for was taken in the meantime, for example between showing a visitor the free times and recording their visit request. Fetch the free times again and offer another.

#### `idempotency_key_reused` [#idempotency_key_reused]

**`409` · `conflict_error`.** You sent an `Idempotency-Key` that Matchai already saw within the last 24 hours, with a different request body. Use a new key for a new request. Reuse a key only to repeat the exact same request.

### Refused by a product rule [#refused-by-a-product-rule]

#### `rooms_exceed_count` [#rooms_exceed_count]

**`422` · `unprocessable_error`.** A listing's bedroom or bathroom count cannot go below the rooms listed on it. Remove the room first, then lower the count. `param` names the field, such as `property.bedrooms`.

#### `recipient_suppressed` [#recipient_suppressed]

**`422` · `unprocessable_error`.** Matchai will not send email to this address, because earlier messages to it bounced or were reported as unwanted. Tell the person, and reach the contact another way.

#### `contact_blocked` [#contact_blocked]

**`422` · `unprocessable_error`.** This contact is blocked, so Matchai refuses to message them. Tell the person, and do not retry.

#### `content_not_allowed` [#content_not_allowed]

**`422` · `unprocessable_error`.** The content you sent breaks Matchai's content rules. The `message` says what to change. Do not retry it unchanged.

#### `handle_not_routable` [#handle_not_routable]

**`422` · `unprocessable_error`.** Publishing the Matchai Page failed because the account's username cannot be a web address yet. The account holder needs to choose a different username in Matchai.

### Rate limits [#rate-limits]

#### `rate_limited` [#rate_limited]

**`429` · `rate_limit_error`.** Too many requests for this connection. The response carries `Retry-After`, the number of seconds to wait. See [rate limits](https://matchai.co/docs/developers/rate-limits#in-the-matchai-api).

### Matchai errors [#matchai-errors]

#### `internal_error` [#internal_error]

**`500` · `api_error`.** Something went wrong on Matchai's side. It is safe to retry with the same `Idempotency-Key`, a few times, with growing delays. If it keeps failing, contact [Matchai support](https://matchai.co/support) with the request id.

<Cards>
  <Card title="Rate limits" href="https://matchai.co/docs/developers/rate-limits">
    How requests are limited, and how to stay under the limit.
  </Card>
  <Card title="Authentication" href="https://matchai.co/docs/developers/authentication">
    Scopes, final actions and Energy limits.
  </Card>
  <Card title="API reference" href="https://matchai.co/docs/developers/api-reference">
    The errors each endpoint can return.
  </Card>
</Cards>

## Sitemap

- [Every page of Matchai Docs](https://matchai.co/docs/sitemap.md)
- [llms.txt](https://matchai.co/docs/llms.txt)
