# Authentication

> Which credential each part of the Fondaro API takes, and how to create, scope, edit, rotate and revoke the API keys that act as you.

Every request to `https://api.fondaro.com` carries one credential, and each part of the API accepts only its own kind. For the Fondaro API (CRM, listings, property search, brochures, documents) that is an API key. Give it the fewest scopes that work.

## Choose a credential

| Credential | Looks like | Header | Opens |
|------------|-----------|--------|-------|
| API key | `fondaro_pk_` + 32 letters and digits | `Authorization: Bearer <key>` | The `/v1` routes, `/properties/*` and `/property-sources/*`, within its scopes |
| Integration key | `fdr_n8n_` + 32 hex characters | `Authorization: Bearer <key>` | `/integrations/v1/*`. See [Integration reporting](https://www.fondaro.com/docs/api/integrations.md) |
| MCP key or OAuth token | `fdr_mcp_…` or `fdr_oat_…` | `Authorization: Bearer <token>` | The MCP server only. See [MCP server](https://www.fondaro.com/docs/api/mcp.md) |
| Session token | A signed-in member's session | `Authorization: Bearer <session-token>` | The routes the Fondaro dashboard and apps use, as that member |

A credential sent to routes it does not open returns `401`. A session token on `/v1` returns `401`, and so does an MCP key.

Session tokens are short-lived and belong to one signed-in person. The pages for dashboard features (Calendar, Inbox, Documents and others) document those routes. For automation that runs on its own, use an API key, an integration key or an MCP credential.

## An API key acts as you

A key acts as the person who created it. A member's key reaches their own leads and what they can see. An admin's key reaches the whole agency. Fondaro checks the role on every call, so a key never has more access than its person.

- **Scopes narrow it further.** A key can do only what its scopes allow, and never more than its person.
- **`/v1` accepts only newer, server keys.** A key made before 4 October 2026 answers `403` with `API_KEY_LEGACY`, and a key that lists allowed websites answers `403` with `API_KEY_HAS_WEBSITES`. Create a new key without websites for `/v1`. Rotating an older key does not change this: it stays an older key, so make a new one.
- **Keys are limited.** Each person can have 10 active keys and each organization 50. Creating one past the limit answers `409` with `API_KEY_LIMIT_REACHED`; revoke a key you no longer use first.
- **A key has an owner.** A key without one answers `403` with `API_KEY_HAS_NO_OWNER`. Create a new key.
- **If you leave the agency, your keys stop working.** On `/v1`, every key of a leaver stops within 5 minutes. A key created on or after 4 October 2026 (or first created then, before a rotation) also stops on the listing and property-source routes within 5 minutes. Older keys keep working on listings and property search as agency keys, so an admin should revoke them.
- **An admin can revoke any key except those set up by Fondaro**, which are listed as such and cannot be changed or revoked from the page. An admin sees each key's owner, including one whose owner left.
- **Listing changes** made with a key are recorded in the listing's history as `apikey:<key id>`.

## Send a key

```bash
curl https://api.fondaro.com/v1/me \
  -H "Authorization: Bearer $FONDARO_API_KEY"
```

`Authorization: Bearer` works on every route a key opens. `X-API-Key: <key>` and `Authorization: ApiKey <key>` are still accepted. If you send more than one, `Authorization: ApiKey` and `X-API-Key` win over `Bearer`. Keys are never accepted in the query string.

Keys are for servers. If a key must run in a web page, see Keep keys safe.

## Scopes

A key can do only what its scopes allow. Each scope needs the person to have that access too.

| Scope | Allows | Admin only |
|-------|--------|------------|
| `crm:read` | Read your leads, notes, tasks, deals, calendar, calls, conversations, emails, tags and team, and search the CRM | No |
| `crm:write` | Create and update leads, notes, tasks and deals, change statuses and tags, and (admins) assign leads | No |
| `properties:read` | Search and read Fondaro listings, counts, aggregations and themes; read viewings, open houses, collections and buyer requests | No |
| `properties:write` | Create, update, renew and delete your own listings, set their portal publications and generate listing descriptions | Yes |
| `media:write` | Upload listing photos with `POST /properties/media/upload` | Yes |
| `sources:read` | Browse your connected property sources through `/property-sources` | No |
| `brochures:read` | Read your brochures and their public links | No |
| `brochures:write` | Create, update, renew, revoke and delete brochures and the listings in them. Creating a brochure, adding or refreshing listings and renewing also need `properties:read` | No |
| `documents:read` | Read your documents and folders | No |
| `documents:write` | Create Markdown documents, change them, share or stop sharing them, and attach them to leads | No |

Listings belong to the agency, so only an admin can give a key `properties:write` or `media:write`. A key created without scopes holds `properties:read` and `sources:read` for a member, and those plus `properties:write` and `media:write` for an admin. Keys made before 4 October 2026 keep the access they had.

Older keys and keys created without scopes also carry `agents:read`. It is reserved: it opens no route and cannot be added to a key. `PATCH` answers `400` with `API_KEY_SCOPE_RESERVED`, and `POST` refuses it as a validation error. It still appears in `GET /api-keys` and `GET /v1/me`.

A key without the scope a route needs gets `403`. On `/v1`:

```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"
}
```

The listing and property-source routes keep their own error shape:

```json
{
  "statusCode": 403,
  "code": "API_KEY_SCOPE_MISSING",
  "message": "This API key is missing the properties:write scope.",
  "missingScopes": ["properties:write"],
  "requiredScopes": ["properties:write"]
}
```

Every operation in the [API reference](https://www.fondaro.com/docs/api-reference.md) names its scope in `x-required-scopes`. In the dashboard's API keys page the same scopes appear as areas with a level: **No access**, **Read**, or **Read and write**.

## Create a key

`POST /api-keys`

Creates a key that acts as you. Any member, with a session token. You can also do it on the API keys page: **Settings › Integrations › For developers › API keys**.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `name` | Body | string | Yes | 1 to 255 characters |
| `scopes` | Body | string[] | No | Scopes from the table above, no repeats. Omit for the default described above |
| `allowedOrigins` | Body | string[] | No | Up to 20 website origins that may use the key from a browser. Not allowed with a server scope |
| `expiresInDays` | Body | integer | No | 1 to 3650. Omit for a key that does not expire |

```bash
curl -X POST https://api.fondaro.com/api-keys \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "Reporting script",
    "scopes": ["crm:read"],
    "expiresInDays": 90
  }'
```

```json
{
  "id": "a1b2c3d4-5678-4abc-9def-123456789abc",
  "key": "fondaro_pk_Q7r2Lm9XcV4tB8nK3pW6yZ1aD5sF0gHj",
  "prefixHint": "fondaro_pk_Q",
  "scopes": ["crm:read"]
}
```

`key` is shown only in this response. Fondaro stores a hash of it and its first 12 characters (`prefixHint`). In the dashboard, the default expiry for a new key is 90 days.

A member who asks for `properties:write` or `media:write` gets `403` with `API_KEY_SCOPE_NOT_GRANTABLE`.

### Use a key in a web page

`crm:*`, `brochures:*` and `documents:*` are server scopes. A key that holds one is for servers only:

- It cannot list allowed websites: `POST` and `PATCH` answer `400` with `API_KEY_ORIGINS_NOT_ALLOWED_FOR_SERVER_SCOPES`.
- A request that carries an `Origin` header (a browser) answers `403` with `API_KEY_ORIGIN_NOT_ALLOWED`.
- `/v1` sends no CORS headers, so a browser cannot read its responses anyway.

Anyone can read a key that ships in a web page. A key used in a web page holds `properties:read` (and `sources:read` if it searches your sources) only, never `properties:write` or `media:write`, and lists the website. An origin is a scheme, host and optional port, such as `https://www.youragency.com`: no path, no wildcard, and plain `http` only for `localhost`. A browser request from an origin the key does not list gets `403` with `API_KEY_ORIGIN_NOT_ALLOWED`. Requests with no `Origin` header (your server, a build step, curl) are not origin-checked.

## List keys

`GET /api-keys`

Lists your organization's keys. Any member, with a session token. A member sees every key but can change only their own.

```bash
curl https://api.fondaro.com/api-keys \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
[
  {
    "id": "a1b2c3d4-5678-4abc-9def-123456789abc",
    "name": "Reporting script",
    "status": "active",
    "prefixHint": "fondaro_pk_Q",
    "legacy": false,
    "system": false,
    "scopes": ["crm:read"],
    "allowedOrigins": [],
    "rateLimitPerMinute": null,
    "createdById": "user_2abcDEF",
    "mine": true,
    "ownerState": "member",
    "createdAt": "2026-10-04T12:00:00.000Z",
    "lastUsedAt": "2026-10-05T09:15:00.000Z",
    "lastUsedIp": "203.0.113.7",
    "lastUsedUserAgent": "curl/8.7.1",
    "usageCount": 247,
    "expiresAt": "2027-01-02T12:00:00.000Z",
    "rotatedFromId": null,
    "graceUntil": null
  }
]
```

`mine` is true for your keys. `ownerState` is `member`, `left` (the owner is no longer in the agency) or `unknown` (the owner could not be checked). `ownerState` is also `unknown` for a revoked, expired or rotated-away key. On a colleague's key, a member sees `lastUsedIp` and `lastUsedUserAgent` as `null`; admins and the key's owner see them. The usage fields can lag by about 30 seconds. `legacy: true` marks an older key with no `prefixHint`.

## Edit a key

`PATCH /api-keys/:id`

Changes a key without changing its secret. Send at least one field. The response is the row shown above.

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `name` | Body | string | No | 1 to 255 characters |
| `scopes` | Body | string[] | No | The full new set. At least one scope, no repeats |
| `expiresAt` | Body | string or null | No | An ISO 8601 date in the future, at most 3650 days from now. `null` removes the expiry |
| `allowedOrigins` | Body | string[] | No | The full new list. `[]` removes it |

```bash
curl -X PATCH https://api.fondaro.com/api-keys/a1b2c3d4-5678-4abc-9def-123456789abc \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "scopes": ["crm:read", "crm:write"], "expiresAt": "2027-06-30T00:00:00.000Z" }'
```

Who may do what:

- **Narrow, rename, change expiry or websites:** the key's owner, or an admin.
- **Give a key more access:** only its owner, and only within their role. An admin cannot widen a member's key. A key an admin edits for someone else can be narrowed, not widened.
- **Remove every website from a key:** only the key's owner. An admin may narrow the list but not clear it; anyone else gets `403` with `API_KEY_NOT_YOURS` ("Only the person this key acts as can remove its website limits").
- A member editing another person's key gets `403` with `API_KEY_NOT_YOURS`.

A narrowed key loses the scope on its next call. A revoked, expired or rotated key cannot be edited: `409` with `API_KEY_NOT_ACTIVE`. Edit its replacement instead.

## Rotate a key

`POST /api-keys/:id/rotate`

Issues a new secret with the same name, scopes, allowed origins, rate limit, expiry and owner. The old key keeps working for 24 hours, then returns `401`. A member rotates their own keys; an admin any key.

```bash
curl -X POST https://api.fondaro.com/api-keys/a1b2c3d4-5678-4abc-9def-123456789abc/rotate \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "id": "f0e1d2c3-b4a5-4968-8776-655443322110",
  "key": "fondaro_pk_Zk3mN8pQ2rT6vX9yB1cE4gH7jL0oS5uW",
  "prefixHint": "fondaro_pk_Z",
  "rotatedFromId": "a1b2c3d4-5678-4abc-9def-123456789abc",
  "previousKeyValidUntil": "2026-10-06T12:00:00.000Z"
}
```

If the key had no owner, the person who rotates it becomes its owner. A key rotates once: rotate its replacement next time. Rotating a revoked or expired key returns `409` with `API_KEY_NOT_ACTIVE`; rotating it again returns `409` with `API_KEY_ALREADY_ROTATED`.

## Revoke a key

`DELETE /api-keys/:id`

Stops the key at once. This cannot be undone. A member revokes their own keys; an admin any key.

```bash
curl -X DELETE https://api.fondaro.com/api-keys/a1b2c3d4-5678-4abc-9def-123456789abc \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "success": true }
```

## Get the website guide for AI builders

`GET /api-keys/integration-guide`

Returns, as JSON, a Markdown guide for your organization that an AI site builder can turn into a listings website. It never contains a key: it uses the `FONDARO_API_KEY` placeholder. Any member, with a session token. See [Build with AI](https://www.fondaro.com/docs/api/ai-builders.md).

| Parameter | In | Type | Required | Description |
|-----------|----|------|----------|-------------|
| `keyId` | Query | UUID | No | An active key; its scopes and origins shape the guide's notes |

```bash
curl "https://api.fondaro.com/api-keys/integration-guide?keyId=a1b2c3d4-5678-4abc-9def-123456789abc" \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "markdown": "# Fondaro listings for Your Agency ...",
  "fileName": "fondaro-mls-integration.md",
  "contractVersion": "v1",
  "generatedAt": "2026-10-05T12:00:00.000Z",
  "exampleCount": 2
}
```

## Use integration keys

Integration keys are created in **Settings › Integrations › n8n**, or with a session token by an organization admin through `POST /integrations/n8n/keys` (`name`, optional `scopes`, optional `expiresInDays` from 1 to 3650). `GET /integrations/n8n/keys` lists them and `DELETE /integrations/n8n/keys/:id` revokes one. The full key is shown once. They act for the whole agency, not for one person, and allow 120 requests a minute each. Scopes are listed on [Integration reporting](https://www.fondaro.com/docs/api/integrations.md#scopes).

## Plan for expiry

A key with an expiry returns `401` after `expiresAt`. Fondaro emails a reminder about a week before it expires: to the key's owner for a member's key, and to the organization's notification address for an admin's key or one whose owner left. Rotate the key or extend its expiry before then. `GET /api-keys` shows each key's `expiresAt`.

## Look up a key error

On `/v1` an error body is the envelope in [Errors and limits](https://www.fondaro.com/docs/api/errors-and-limits.md). Key and `/api-keys` errors:

| Status | Code | When |
|--------|------|------|
| `401` | `UNAUTHENTICATED` | On `/v1`: missing, malformed, unknown, revoked or expired key, or a session token or MCP key. On other routes the `401` has no code |
| `403` | `API_KEY_SCOPE_MISSING` | The key lacks the route's scope |
| `403` | `API_KEY_HAS_NO_OWNER` | The key has no owner. Create a new key. Rotating an older key does not help |
| `403` | `API_KEY_LEGACY` | `/v1` refuses keys made before 4 October 2026. Create a new key |
| `403` | `API_KEY_HAS_WEBSITES` | `/v1` refuses a key that lists allowed websites. Create a key without them |
| `409` | `API_KEY_LIMIT_REACHED` | Creating a key would pass 10 active keys for you or 50 for your organization. Revoke one first |
| `403` | `API_KEY_OWNER_NOT_IN_ORGANIZATION` | The key's owner left the agency |
| `403` | `API_KEY_ORIGIN_NOT_ALLOWED` | A browser request from an origin the key does not list, or from any browser with a server key |
| `403` | `API_KEY_SCOPE_NOT_GRANTABLE` | A member asked for `properties:write` or `media:write` |
| `403` | `API_KEY_NOT_YOURS` | A member changed another person's key, or someone other than the owner widened a key or removed every website from it |
| `400` | `API_KEY_ORIGINS_NOT_ALLOWED_FOR_SERVER_SCOPES` | Websites together with a server scope |
| `400` | `API_KEY_UPDATE_EMPTY` | `PATCH` with nothing to change |
| `400` | `API_KEY_EXPIRY_IN_PAST`, `API_KEY_EXPIRY_TOO_FAR` | `expiresAt` is not in the future, or is more than 3650 days away |
| `400` | `API_KEY_SCOPE_RESERVED` | `agents:read` is reserved and cannot be added |
| `409` | `API_KEY_NOT_ACTIVE`, `API_KEY_ALREADY_ROTATED` | The key cannot be edited or rotated |
| `404` | | The key does not exist, or is set up by Fondaro and cannot be changed |
| `429` | | Rate limit reached; wait `Retry-After` seconds |

## Keep keys safe

- Keep keys in environment variables or a secrets manager, never in code or a repository.
- Give each integration its own key with only the scopes it needs.
- Never put a key with write access, or with Leads and CRM, Brochures or Documents access, in a web page.
- Set an expiry on short-lived integrations, and revoke keys you no longer use.
- Watch `lastUsedAt` and `lastUsedIp` for activity you do not expect.

Source: https://www.fondaro.com/docs/api/authentication
