# Documents

> Store, organize, share and attach your agency's documents and files: written documents, uploads, folders, the bin and share links.

The Documents API is your organization's drive: written (Markdown) documents, uploaded files, folders and a bin, plus share links and attachments to leads and listings.

These endpoints take a signed-in member's session token (`Authorization: Bearer <session-token>`); API keys do not open them. See [Authentication](https://www.fondaro.com/docs/api/authentication.md). The two `/public/documents` routes need no credential.

Every request is scoped to the caller's organization, which is never a parameter. Two rules apply throughout:

- **Reading follows visibility.** A member sees `organization` and `public` documents plus their own `private` ones; an organization admin sees every document. A document you cannot see answers `404`.
- **Editing follows visibility.** Anyone who can see a Team document can edit its title and body; a private document only its creator and organization admins. Sharing, revoking, moving and binning stay with the creator and organization admins. Attaching only needs read access to the document and access to the lead or listing.

Documents need an active subscription. Without one, every route answers `403` with `code: "SUBSCRIPTION_REQUIRED"` and share links answer `404`. Nothing is deleted; subscribing again restores the library and its links.

## The document object

List endpoints return the summary fields. Single-document responses add the detail fields.

| Field | Type | Description |
|---|---|---|
| `id` | string | UUID |
| `title` | string | Up to 200 characters |
| `kind` | string | `markdown`, `whiteboard`, `pdf` or `file`. Tolerate unknown values |
| `visibility` | string | `private`, `organization` or `public` |
| `sizeBytes` | number or null | Uploaded documents only |
| `createdBy` | string | User id of the creator |
| `assigneeIds` | string[] | User ids, up to 50 |
| `shareUrl` | string or null | Set only while a share link is live |
| `attachedLeadCount` | number | |
| `attachedListingCount` | number | |
| `pinnedAt` | string or null | Pinned for the whole organization |
| `folderId` | string or null | `null` is the top level |
| `contentType` | string or null | Uploads only: `application/pdf`, an `image/*` type, or `application/octet-stream` |
| `fileExtension` | string or null | Uploads only, lower case |
| `deletedAt` | string or null | Set only on rows read from the bin |
| `createdAt`, `updatedAt` | string | ISO 8601 |
| `body` | string or null | Detail only. Markdown source of a `markdown` document; the Excalidraw scene JSON of a `whiteboard` (the public API, MCP and Ask Fondaro return a plain-text outline instead) |
| `shareRevokedAt` | string or null | Detail only |
| `shareExpiresAt` | string or null | Detail only. `null` means the link never expires |
| `attachedLeadIds` | number[] | Detail only |
| `attachedListingIds` | string[] | Detail only |

## List documents

`GET /documents`

Lists the documents you can see, most recently edited first by default.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `kind` | Query, string | No | `markdown`, `whiteboard`, `pdf` or `file` |
| `folderId` | Query, string | No | A folder id, or `root` for the top level. Omit to list every document wherever it lives |
| `scope` | Query, string | No | `shared` (created by someone else) or `pinned` |
| `visibility` | Query, string | No | Narrows what you can already see; never widens it |
| `assigneeId` | Query, string | No | Documents with this assignee |
| `mine` | Query, boolean | No | `true` for documents you created |
| `createdBy` | Query, string[] | No | Creator user ids, up to 50. Repeat the parameter |
| `q` | Query, string | No | Title search, up to 128 characters |
| `sort` | Query, string | No | `updated` (default), `title` (A to Z) or `size` (largest first) |
| `offset` | Query, integer | No | Default `0` |
| `limit` | Query, integer | No | 1 to 50, default 20 |

```bash
curl -G https://api.fondaro.com/documents \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  --data-urlencode "kind=pdf" \
  --data-urlencode "q=guide"
```

```json
{
  "items": [
    {
      "id": "3f1c0d0e-0000-4000-8000-000000000000",
      "title": "Buyer guide",
      "kind": "pdf",
      "visibility": "public",
      "sizeBytes": 2214592,
      "createdBy": "user_2abc",
      "assigneeIds": [],
      "shareUrl": "https://www.fondaro.com/d/9pQ2rB7wKx1vT0sN4mH6cA",
      "attachedLeadCount": 3,
      "attachedListingCount": 0,
      "pinnedAt": null,
      "folderId": null,
      "contentType": "application/pdf",
      "fileExtension": "pdf",
      "deletedAt": null,
      "createdAt": "2026-08-01T09:12:00.000Z",
      "updatedAt": "2026-08-20T14:03:00.000Z"
    }
  ],
  "total": 1,
  "offset": 0,
  "limit": 20
}
```

## Create a written document

`POST /documents`

Creates a Markdown document. It starts as `private`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `title` | Body, string | Yes | 1 to 200 characters |
| `body` | Body, string | No | GitHub-flavoured Markdown, up to 1,000,000 bytes |
| `assigneeIds` | Body, string[] | No | Up to 50 user ids |
| `folderId` | Body, string | No | Folder to create it in; omit for the top level |

```bash
curl -X POST https://api.fondaro.com/documents \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"title": "Buyer guide", "body": "# Buyer guide\n\nWhat buyers ask us most."}'
```

```json
{
  "id": "3f1c0d0e-0000-4000-8000-000000000000",
  "title": "Buyer guide",
  "kind": "markdown",
  "visibility": "private",
  "body": "# Buyer guide\n\nWhat buyers ask us most.",
  "shareUrl": null,
  "attachedLeadIds": [],
  "attachedListingIds": []
}
```

## Upload a file

`POST /documents/upload`

Uploads any file as `multipart/form-data`. The file's bytes decide its kind: a PDF becomes `pdf`, anything else `file`. Only PDFs and images (JPEG, PNG, WebP, GIF) are ever shown inline; other files only download.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `file` | Form, file | Yes | A PDF up to 25 MB, any other file up to 50 MB |
| `title` | Form, string | No | Defaults to the file name |
| `folderId` | Form, string | No | Folder to upload into |
| `assigneeIds` | Form, string[] | No | Repeat the field or send a JSON array |

```bash
curl -X POST https://api.fondaro.com/documents/upload \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -F "file=@price-list.xlsx" \
  -F "folderId=6f1c0b8e-2a1d-4f7e-9a51-0c9d3c2b7e11"
```

```json
{
  "id": "8a2d4c6e-1b3f-4a5c-9e7d-0f1a2b3c4d5e",
  "title": "price-list.xlsx",
  "kind": "file",
  "contentType": "application/octet-stream",
  "fileExtension": "xlsx",
  "sizeBytes": 48213
}
```

Each organization has 10 GB of storage, bin included. An upload that would go over it answers `400` with `code: "DOCUMENT_STORAGE_FULL"` and stores nothing.

## Read, update and delete a document

`GET /documents/:id` returns the detail object.

`PATCH /documents/:id` changes any of these fields:

| Parameter | Type | Required | Description |
|---|---|---|---|
| `title` | Body, string | No | 1 to 200 characters |
| `body` | Body, string | No | `markdown` and `whiteboard` documents only. Replaces the whole body. A whiteboard body must be an Excalidraw scene (`type: "excalidraw"`, an `elements` array, `files` with metadata only and no `dataURL`), up to 8 MB; a written document is capped at 1,000,000 bytes. The public API, MCP and Ask Fondaro refuse a whiteboard body with `DOCUMENT_KIND_READ_ONLY` |
| `baseUpdatedAt` | Body, string | No | The `updatedAt` your edit started from. With `title` or `body`, a document changed since answers `409` with `code: "DOCUMENT_CHANGED_ELSEWHERE"` |
| `assigneeIds` | Body, string[] | No | Replaces the list. Up to 50 |
| `teamIds` | Body, string[] | No | Up to 20 teams; their current members join the assignees. See [Teams](https://www.fondaro.com/docs/api/teams.md) |
| `visibility` | Body, string | No | `private` or `organization`. Use [share](#share-a-document) for `public` |
| `pinned` | Body, boolean | No | Pin or unpin for the organization |
| `folderId` | Body, string or null | No | Move to a folder; `null` is the top level |

```bash
curl -X PATCH https://api.fondaro.com/documents/$DOC_ID \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"visibility": "organization", "pinned": true}'
```

```json
{ "id": "3f1c0d0e-0000-4000-8000-000000000000", "visibility": "organization", "pinnedAt": "2026-10-02T09:00:00.000Z" }
```

A `public` document cannot change visibility with `PATCH`: revoke its link first.

`DELETE /documents/:id` moves the document to the bin and answers `{ "deleted": true }`. It disappears from every list and its share link answers `404` until it is restored.

## Whiteboards

`POST /documents/whiteboards` creates an empty private whiteboard. Body: optional `title` and `folderId`. It answers `400` with `code: "DOCUMENT_STORAGE_FULL"` when the organization's storage is full.

`POST /documents/:id/inline-images` also accepts a whiteboard: image bytes are stored with the board and counted toward storage, and the scene's `files` hold only their metadata.

`PUT /documents/:id/preview` stores the board's preview (multipart `file`, a PNG up to 2 MB, edit rights). It is not counted toward storage. `GET /documents/:id/preview` answers `302` to a five-minute link, or `404` when there is no preview. `GET /public/documents/:slug/preview` is the keyless twin for a shared board.

## Version history

Anyone who can edit a document can read its history. A version is the body as it was before a change: one per working session for a person typing, always before Ask Fondaro, MCP or the public API replaces a body, and always before a restore. Fondaro keeps the newest 50 per document and none older than 90 days. Versions do not count toward storage.

| Endpoint | Answers |
|---|---|
| `GET /documents/:id/versions` | `{ items }`, newest first, at most 50: `id`, `createdAt`, `createdBy` (null for a live-editing save), `source` (`person`, `ask`, `api` or `restore`), `sizeBytes` |
| `GET /documents/:id/versions/:versionId` | One version with its `title`, `kind` and `body` |
| `POST /documents/:id/versions/:versionId/restore` | The document, with that body written back. The body it replaced is saved as a new version first, and an open editor receives the restored text |

## Download or view a file

`GET /documents/:id/download` and `GET /documents/:id/view`

Both answer `302` to a link that works for five minutes. `download` saves the file under a name based on the title; `view` opens it inline and works for PDFs and images only. A `markdown` or `whiteboard` document has no file and answers `400`.

```bash
curl -L -o guide.pdf https://api.fondaro.com/documents/$DOC_ID/download \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

## Share a document

`POST /documents/:id/share`

Makes the document `public` behind an unguessable link and returns the detail object with `shareUrl` set. Sharing again while the link is live returns the same link.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `expiresAt` | Body, string | No | ISO 8601 time in the future. Omit for a link that never expires |

```bash
curl -X POST https://api.fondaro.com/documents/$DOC_ID/share \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{}'
```

```json
{
  "id": "3f1c0d0e-0000-4000-8000-000000000000",
  "visibility": "public",
  "shareUrl": "https://www.fondaro.com/d/9pQ2rB7wKx1vT0sN4mH6cA",
  "shareExpiresAt": null,
  "shareRevokedAt": null
}
```

`POST /documents/:id/revoke` takes the link down and sets `visibility` back to `organization`. Sharing after a revoke or an expiry creates a new link, so addresses already sent stay dead.

## Attach to leads and listings

| Endpoint | Body | Purpose |
|---|---|---|
| `POST /documents/:id/leads` | `{ "leadIds": number[] }`, 1 to 50 | Attach to leads you can open |
| `DELETE /documents/:id/leads/:leadId` | | Detach one lead |
| `POST /documents/:id/property-listings` | `{ "listingIds": string[] }`, 1 to 50 | Attach to your organization's listings |
| `DELETE /documents/:id/property-listings/:listingId` | | Detach one listing |

All four return the document detail object. If any lead or listing is out of reach, the request answers `404` and nothing is attached. Attaching again is harmless.

```bash
curl -X POST https://api.fondaro.com/documents/$DOC_ID/leads \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"leadIds": [4821, 4822]}'
```

```json
{ "id": "3f1c0d0e-0000-4000-8000-000000000000", "attachedLeadIds": [4821, 4822], "attachedLeadCount": 2 }
```

## List a lead's or listing's documents

`GET /crm/leads/:id/documents` and `GET /documents/by-listing/:listingId`

Return `{ "documents": [...] }`, most recently attached first. Attaching is not a permission: a private document someone else attached stays hidden from you.

```bash
curl https://api.fondaro.com/crm/leads/4821/documents \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{
  "documents": [
    {
      "id": "3f1c0d0e-0000-4000-8000-000000000000",
      "title": "Buyer guide",
      "kind": "pdf",
      "visibility": "public",
      "sizeBytes": 2214592,
      "createdBy": "user_2abc",
      "attachedBy": "user_2def",
      "attachedAt": "2026-08-20T14:05:00.000Z",
      "updatedAt": "2026-08-20T14:03:00.000Z"
    }
  ]
}
```

### Count documents for many listings

`POST /documents/by-listing/counts`

Counts the documents you can see on each listing in one request. Every requested id is in the answer, `0` included; an id outside your organization also counts `0`.

| Parameter | Type | Required | Description |
|---|---|---|---|
| `listingIds` | Body, string[] | Yes | 1 to 100 listing UUIDs |

```bash
curl -X POST https://api.fondaro.com/documents/by-listing/counts \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"listingIds": ["7b0e6c1a-1d2e-4f3a-9b4c-5d6e7f8a9b0c"]}'
```

```json
{ "7b0e6c1a-1d2e-4f3a-9b4c-5d6e7f8a9b0c": 3 }
```

## Save a lead's attachment to Documents

`POST /crm/leads/:id/emails/:emailId/attachments/:attachmentId/save-to-documents` saves a file from a lead's email, and `POST /crm/leads/:id/messages/:messageId/attachments/:index/save-to-documents` saves one from a lead's message. Both answer `201` with the new document, already attached to the lead.

```bash
curl -X POST https://api.fondaro.com/crm/leads/4821/emails/$EMAIL_ID/attachments/$ATTACHMENT_ID/save-to-documents \
  -H "Authorization: Bearer $SESSION_TOKEN"
```

```json
{ "id": "5c7e9a1b-2d4f-4b6a-8c0e-1f3a5b7d9e2c", "kind": "pdf", "attachedLeadIds": [4821] }
```

## Send a document in a lead email

`POST /crm/leads/:id/emails` accepts `documentIds`: up to ten uploaded documents (a PDF or any other file), sent as real attachments, 20 MB in total. To attach a file from a computer, upload it first with `POST /documents/upload` and send its id. A written document, or one with no stored file, answers `422` with `code: "CRM_EMAIL_DOCUMENT_NOT_ATTACHABLE"`; going over the size cap answers `422` with `code: "CRM_EMAIL_ATTACHMENTS_TOO_LARGE"`. To send a written document, put its `shareUrl` in the message.

```bash
curl -X POST https://api.fondaro.com/crm/leads/4821/emails \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"subject": "The guide I mentioned", "bodyText": "Here it is.", "documentIds": ["3f1c0d0e-0000-4000-8000-000000000000"]}'
```

See [Connected inboxes](https://www.fondaro.com/docs/api/connected-accounts.md) for the rest of this endpoint.

## Folders

| Endpoint | Purpose |
|---|---|
| `GET /documents/folders` | Every folder, as `{ "items": [...] }` |
| `POST /documents/folders` | Create a folder: `name` (1 to 200 characters), optional `parentId` |
| `PATCH /documents/folders/:folderId` | Change `name`, `parentId` (`null` for the top level) or `pinned` |
| `DELETE /documents/folders/:folderId` | Move the folder and everything in it to the bin |

Every member sees every folder and may create one; changing a folder is limited to its creator and admins. A folder's `itemCount` and `sizeBytes` count only what you can see inside it.

```bash
curl -X POST https://api.fondaro.com/documents/folders \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"name": "Marbella listings"}'
```

```json
{
  "id": "6f1c0b8e-2a1d-4f7e-9a51-0c9d3c2b7e11",
  "name": "Marbella listings",
  "parentId": null,
  "createdBy": "user_2abc",
  "pinnedAt": null,
  "itemCount": 0,
  "sizeBytes": 0,
  "deletedAt": null,
  "createdAt": "2026-10-02T09:00:00.000Z",
  "updatedAt": "2026-10-02T09:00:00.000Z"
}
```

## Move items and use the bin

These endpoints take `documentIds` and `folderIds` (each up to 100; at least one item in total). Every item must be one you may change, or the whole request is refused.

| Endpoint | Body | Answer |
|---|---|---|
| `POST /documents/move` | Items plus `targetFolderId` (`null` for the top level) | `{ "moved": n }` |
| `POST /documents/bin` | Items | `{ "binned": n }` |
| `GET /documents/bin` | | `{ "folders", "documents", "retentionDays" }`: what you may restore (admins see the whole organization's bin) |
| `POST /documents/bin/restore` | Items | `{ "restored": n }` |
| `POST /documents/bin/purge` | Items | `{ "purged": n }`, deleted for good |
| `POST /documents/bin/empty` | | `{ "documents": n, "folders": n }`, everything you could restore, deleted for good |
| `GET /documents/storage` | | `{ "usedBytes", "quotaBytes" }` for the organization |

Binning a folder takes everything inside it, and restoring the folder brings back exactly that set. Items stay in the bin for 30 days, then are deleted for good.

```bash
curl -X POST https://api.fondaro.com/documents/bin/restore \
  -H "Authorization: Bearer $SESSION_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"documentIds": ["2b9c1c55-6d7e-4f3a-8e21-9a0b1c2d3e4f"]}'
```

```json
{ "restored": 1 }
```

## Open a share link without a key

`GET /public/documents/:slug`

Needs no credential: the slug is the access. Answers the document without anything about the organization or its author.

```bash
curl https://api.fondaro.com/public/documents/9pQ2rB7wKx1vT0sN4mH6cA
```

```json
{
  "slug": "9pQ2rB7wKx1vT0sN4mH6cA",
  "title": "Buyer guide",
  "kind": "pdf",
  "body": null,
  "viewUrl": "https://…",
  "contentType": "application/pdf",
  "fileExtension": "pdf",
  "sizeBytes": 2214592,
  "updatedAt": "2026-08-20T14:03:00.000Z"
}
```

`body` holds the Markdown of a written document. For a whiteboard it holds the scene with every object that is not a property replaced by a hidden placeholder, so no lead, deal or person id leaves the server. `viewUrl` is a five-minute link for a PDF or image; use it at once and never store it. The lasting address is `https://www.fondaro.com/d/{slug}`.

`GET /public/documents/:slug/download` answers `302` to a five-minute download of an uploaded file, checking the link again on every request.

## Errors and limits

| Status | When |
|---|---|
| `400` | Invalid input, a folder moved into itself, an empty bulk request, a body on a document that has none, an invalid whiteboard scene (`DOCUMENT_WHITEBOARD_INVALID`), `DOCUMENT_KIND_READ_ONLY`, `view` on a file that is not a PDF or image, or `DOCUMENT_STORAGE_FULL` |
| `403` | `SUBSCRIPTION_REQUIRED`, or changing a document or folder you neither created nor administer |
| `404` | A document, lead or listing you cannot see; an unknown share slug, or one whose organization is disabled or not subscribed |
| `409` | `DOCUMENT_CHANGED_ELSEWHERE` on an edit sent with `baseUpdatedAt` |
| `410` | A share link that was revoked or expired; `code` is `revoked` or `expired` |

The `/public/documents` routes allow 60 requests a minute per IP address. The same library is available through the [MCP server](https://www.fondaro.com/docs/api/mcp.md) with the `documents:read` and `documents:write` scopes; uploading and binning stay on this API.

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