# Errors and limits

> The /v1 error format and every error code, paging, idempotency keys, rate limits and the daily property search allowance.

This page covers what every `/v1` request shares: the error format, paging, safe retries of creates, and the limits. Listings and property-source routes keep the error shape they have always had; [Authentication](https://www.fondaro.com/docs/api/authentication.md#scopes) shows both.

## Read an error

Every error on `/v1` has the same envelope:

```json
{
  "error": {
    "code": "API_KEY_SCOPE_MISSING",
    "message": "This API key is missing the crm:write scope.",
    "status": 403,
    "details": { "missing": ["crm:write"] }
  },
  "requestId": "0b6f8f0e-6d2b-4b8a-9f43-5f1c2a7d9e11"
}
```

Branch on `code`. `message` is for people and can change. `details` is present when there is more to say, for example the missing scopes or a list of validation issues.

A request that fails validation answers `400` with `VALIDATION_FAILED` and the problems in `details.issues`. Unknown query parameters and body fields are ignored on most routes, so check spelling against the route's page. A body sent to a route that takes none is refused. A JSON body sent without `Content-Type: application/json` is not read and answers `400`:

```json
{
  "error": {
    "code": "VALIDATION_FAILED",
    "message": "The request is not valid. See details.issues.",
    "status": 400,
    "details": {
      "issues": [{ "path": "query.limit", "message": "Too big: expected number to be <=100" }]
    }
  },
  "requestId": "5b2d7c1e-3f4a-4e89-b6a0-9c8d7e6f5a43"
}
```

### Error codes

| Status | Code | When |
|--------|------|------|
| `400` | `VALIDATION_FAILED` | A parameter or body is missing or malformed. See `details.issues` |
| `400` | `BAD_REQUEST` | The request is not valid in another way |
| `401` | `UNAUTHENTICATED` | The key is missing, malformed, unknown, revoked or expired, or you sent a session token. The response carries a `WWW-Authenticate: Bearer realm="Fondaro API"` header |
| `403` | `API_KEY_SCOPE_MISSING` | The key lacks a scope. `details.missing` lists them |
| `403` | `ORG_ADMIN_REQUIRED` | Only an organization admin can do this |
| `403` | `SUBSCRIPTION_REQUIRED` | This needs an active Fondaro subscription |
| `403` | `ORG_DISABLED` | The organization is disabled |
| `403` | `API_KEY_HAS_NO_OWNER` | The key has no owner. Create a new key. Rotating an older key does not change this |
| `403` | `API_KEY_LEGACY` | The key was made before 4 October 2026. `/v1` does not accept it. Create a new key; rotating the older key does not change this |
| `403` | `API_KEY_HAS_WEBSITES` | The key lists allowed websites. `/v1` is for servers, so make a key without websites for it |
| `409` | `API_KEY_LIMIT_REACHED` | Creating a key would pass the limit of 10 active keys per person or 50 per organization. Revoke one you no longer use |
| `403` | `API_KEY_OWNER_NOT_IN_ORGANIZATION` | The key's owner left the agency |
| `403` | `API_KEY_ORIGIN_NOT_ALLOWED` | A browser request. `/v1` is for servers |
| `403` | `API_OPERATION_NOT_DECLARED` | A route that does not declare its access is refused. If you see this, tell support |
| `403` | `FORBIDDEN` | You do not have access to this |
| `404` | `NOT_FOUND` | The record does not exist or is not yours. For a record the message is "`<Resource>` not found.", whichever it is. An unknown path, or a method a path does not support, answers "No such endpoint." |
| `409` | `CONFLICT` | The request conflicts with the current state |
| `409` | `IDEMPOTENCY_KEY_REUSED`, `IDEMPOTENCY_KEY_IN_PROGRESS` | See Retry a create safely |
| `413` | `PAYLOAD_TOO_LARGE` | The body is too large |
| `415` | `UNSUPPORTED_MEDIA_TYPE` | The body's content type is not supported. Send `Content-Type: application/json` |
| `422` | `UNPROCESSABLE` | The request is understood but cannot be applied |
| `429` | `RATE_LIMITED` | Too many requests. Wait `Retry-After` seconds |
| `429` | `DAILY_LIMIT_REACHED` | The daily property site search allowance is used up |
| `502`, `503` | `INTERNAL` | A service Fondaro depends on is briefly unavailable. Retry with backoff |
| `500` | `INTERNAL` | Something went wrong on our side |
| `503` | `AUTH_UNAVAILABLE` | Fondaro could not check the key's owner just now. Retry shortly |

A `404` does not tell you whether a record exists: a lead you cannot see answers the same as one that does not exist. A domain error can carry its own `code` and extra `details`; the page for that route lists it.

A `5xx` never contains a stack trace or a message from a service behind Fondaro.

### Request ids

Every `/v1` response, success or error, has an `X-Request-Id` header, and every error carries the same value as `requestId`. Fondaro generates it; a value you send is ignored. When you contact support, send the `requestId`, the time, the method and the path. Never send the key.

## Page through a list

A list is an object with `data` and `hasMore`:

```json
{
  "data": [{ "id": "c7d2…" }, { "id": "b61a…" }],
  "hasMore": true,
  "nextOffset": 2
}
```

Each route's page names its paging style and its `limit` range. The styles that exist:

| Style | How | Used by |
|-------|-----|---------|
| Page number | Send `page` and `limit`. Leads answer `page`, `limit` and an exact `total`; brochures answer `hasMore` and, when it is known, `total` | Leads, brochures |
| Offset | Send `offset` and `limit`. While `hasMore` is true, send `nextOffset` as the next `offset`. Calls, viewings and documents also carry `total` | Lead timeline, calls, viewings, documents |
| Older than | Send `before` with the time of the last message you have | A lead's conversation |
| Capped | One page, at most a fixed number of rows. `hasMore: true` means more exist: narrow your filters to reach them | Deals, tasks, emails, members, buyer requests, collections, open houses |
| Whole list | Everything, in one response, with `hasMore: false` | Notes, tags, teams |

`limit` is an upper bound, so a page can hold fewer rows than asked for. Offset paging has no snapshot: rows added or removed while you page can shift between pages.

## Retry a create safely

Send an `Idempotency-Key` header on a `POST` that creates something (for example a lead, a task, a note or a deal; the route's page says when it accepts one). The header is optional: 1 to 255 visible ASCII characters, such as a UUID.

```bash
curl -X POST https://api.fondaro.com/v1/leads \
  -H "Authorization: Bearer $FONDARO_API_KEY" \
  -H "Idempotency-Key: 6f1c2d3e-0a4b-4c5d-8e9f-1a2b3c4d5e6f" \
  -H "Content-Type: application/json" \
  -d '{ "firstName": "Anna", "lastName": "Berg", "email": "anna@example.com", "phoneNumber": "+46701234567", "crmStatus": "lead" }'
```

- **Retry after a timeout or a `5xx`:** send the same key with the same request. If the first attempt succeeded, you get its response back with `Idempotent-Replayed: true`, and nothing is created twice.
- **Same key, different request:** `409` with `IDEMPOTENCY_KEY_REUSED`. A request is the same when the method, the path and the JSON body match exactly, including key order. The query string is not compared.
- **Same key while the first request still runs:** `409` with `IDEMPOTENCY_KEY_IN_PROGRESS`. Wait a moment and retry. The marker lasts up to 5 minutes.
- **Failed requests are not stored.** A `4xx` or `5xx` frees the key, so you can fix the request and send it again with the same key.
- **Responses are kept for 24 hours.** Keys belong to the API key that sent them.
- **Responses that carry a secret, or are larger than 512 KB, are not stored.** A retry runs the request again.

Without the header, a retried create creates again. If the idempotency store is briefly unavailable, requests still run, without protection.

## Stay within rate limits

Each API key has its own allowance per endpoint:

| Window | Requests |
|--------|---------|
| A minute | 120 |
| 10 minutes | 600 |
| An hour | 3,000 |

Fondaro can raise one key's per-minute limit for a busy integration; the 10-minute and hourly windows scale with it. Responses carry the remaining allowance in `X-RateLimit-Limit-<window>` and `X-RateLimit-Remaining-<window>` headers, one pair per window (`short`, `medium`, `long`).

Over a limit, the API answers `429` with `RATE_LIMITED`, a `Retry-After` header in seconds and an `X-RateLimit-Reset` header with the reset time in epoch seconds. Wait that long, then retry; slow down rather than retry at once.

Two reads cost more to run and share one limit: semantic search of your CRM (`POST /v1/search`) and listing matches (`POST /v1/listing-matches`). Together they allow 30 requests a minute per key, and 30 a minute for your whole organization across all its keys, so creating more keys does not raise it. Over it, the API answers `429` with `RATE_LIMITED` and `Retry-After`. This budget is separate from the one the n8n integration API uses.

## Plan around the daily search allowance

Searches of the property sites you can use with Fondaro share one allowance per organization: 300 upstream requests a day, counted from midnight UTC. It is shared by the dashboard, Ask Fondaro and every API key, so a busy script uses what your team would otherwise have. Cached results are free. When it is used up:

- On `/v1`, the API answers `429` with `DAILY_LIMIT_REACHED`.
- On `POST /property-sources/{source}/search` and the other `/property-sources` routes, which keep their own error shape, it answers `400` with `PROPERTY_PORTAL_QUOTA_EXHAUSTED`.

Either way, wait for the next UTC day.

## Handle new versions

The `/v1` routes change only by adding: new routes, new response fields, new optional parameters and new error codes. Ignore fields and codes you do not know. A breaking change would be a new version path, `/v2`, announced in the changelog, with `/v1` kept running.

Source: https://www.fondaro.com/docs/api/errors-and-limits
