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. 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
organizationandpublicdocuments plus their ownprivateones; an organization admin sees every document. A document you cannot see answers404. - 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 |
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 |
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 |
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 |
visibility | Body, string | No | private or organization. Use share 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 |
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.
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 |
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.
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.
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 |
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.
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.
See Connected inboxes 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.
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.
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.
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 with the documents:read and documents:write scopes; uploading and binning stay on this API.