Files
Files & Folders
Structured storage where files are participants in workflows, not passive attachments. Every file traces back to the email or workflow write that created or updated it. Spreadsheets accumulate rows from extracted email data. Documents are revised by AI on each new trigger. Contacts become collaborators and viewers. The inbox is the hub — email is the interface.
Scopes:
files:read for GET operations and signed download link generation. files:write permits creating files/folders, updating editable metadata/content, and appending rows. Destructive actions require owner or tenant-admin authority; collaborators can edit but cannot delete, move, or re-share. File download links are public endpoints authenticated by a signed token — no auth header required.Files vs attachments: Message attachments are message-scoped upload/download objects. They are not Files records by default. Promote an attachment into Files when it needs owner, folder, version, viewer, or collaborator metadata.
File types:
spreadsheet — row-level API, AI-populated from email context. document — text content, AI-revised per trigger. image — binary, optional AI description. video — binary, store/serve only. template — email template with variable rendering. skill — agent skill definition (JSON/YAML). other — anything else.Versioning: Every content write (upload or workflow update) creates a new
FileVersion. The file record always points to the current version. Previous versions remain accessible via GET /v1/files/{file_id}/versions/.GET /v1/files/reference/ List file types and role capabilities no auth required
200 OK
{
"version": "2026-06-06",
"resource": "files",
"payload": {
"create_file": { "method": "POST", "path": "/v1/files/" },
"download_link": { "method": "POST", "path": "/v1/files/{file_id}/link/" }
},
"fields": [...],
"access_model": {
"tenant_owned_default": "tenant_members_can_read",
"collaborator": "edit_only_no_delete_move_reshare"
},
"file_types": ["document", "image", "other", "skill", "spreadsheet", "template", "video"],
"scopes": ["inbox", "tenant"],
"roles": [
{
"role": "collaborator",
"can_read": true,
"can_download": true,
"can_edit": true,
"can_delete": false,
"can_share": false
}
],
"thread_share_roles": ["viewer", "collaborator"]
}
This is the builder contract for Files clients and keeps legacy option arrays for compatibility. Tenant-scoped files are readable by tenant members by default. Collaborators can edit but cannot delete, move, or re-share.
Folders
GET /v1/files/folders/ List folders scope: files:read
Query params
| Parameter | Description | |||
|---|---|---|---|---|
| scope | Inbox (default) | tenant. | |||
| inbox_id | required | String | required | // required. |
| scope | string | optional | Inbox (default) | tenant. | |
| inbox_id | string | required | Required for scope=inbox. | |
| parent_id | null | optional | Nest under another folder. |
PATCH /v1/files/folders/{folder_id}/ Rename or reparent scope: files:write
{ "name": "Renamed", "parent_id": "<folder-id>" }
Reparenting stays inside the same inbox or tenant folder tree. A folder cannot be moved under itself or one of its descendants.
DEL /v1/files/folders/{folder_id}/ Delete folder scope: files:write
Request body (optional)
{
"contents": "unparent",
"target_folder_id": null
}
contents defaults to unparent, which keeps direct files and child folders by moving them to the root. Use delete to recursively delete child folders and files in the subtree, or move with target_folder_id to move direct files and child folders before deleting the folder. Move targets must be in the same tree and cannot be inside the deleted folder's subtree.Files
GET /v1/files/ List files scope: files:read
Query params
| Parameter | Description | |
|---|---|---|
| scope | Inbox (default) | tenant. | |
| inbox_id | required | Filter to a specific folder. |
POST /v1/files/ Upload a file → file_id scope: files:write
Multipart upload (binary files)
# Content-Type: multipart/form-data
# Fields: file (binary), name, file_type, scope, inbox_id, folder_id, instructions
JSON body (text content)
{
"name": "vendor-notes.txt",
"file_type": "document",
"content": "Plain text content...",
"scope": "inbox",
"inbox_id": "<inbox-id>",
"folder_id": null,
"instructions": "Track vendor communications and key decisions.",
"thread_id": null
}
201 Created
{
"file_id": "<file-id>",
"name": "vendor-notes.txt",
"file_type": "document",
"scope": "inbox",
"inbox_id": "<inbox-id>",
"tenant_id": "<tenant-id>",
"folder_id": null,
"size": 1024,
"content_type": "text/plain",
"version": 1,
"row_count": 0,
"schema": null,
"instructions": "Track vendor communications...",
"thread_id": null,
"source_message_id":null,
"viewers": [],
"collaborators": [],
"created_at": "2026-05-31T00:00:00Z",
"updated_at": "2026-05-31T00:00:00Z"
}
Max upload size: 100 MB. Returns 413 if exceeded.
PATCH /v1/files/{file_id}/ Update file metadata scope: files:write
{
"name": "renamed.csv",
"folder_id": "<folder-id>",
"instructions": "Updated LLM instructions for this file",
"viewers": ["<contact-id>"],
"collaborators": ["<contact-id>"]
}
Move a file by setting
folder_id; set it to null to unparent the file. Changing folder_id, viewers, or collaborators requires owner or tenant-admin authority. Content updates are uploaded to POST /v1/files/{file_id}/versions/; PATCH only affects metadata.DEL /v1/files/{file_id}/ Delete file scope: files:write
# 204 No Content. Deletes stored file versions, all FileVersion records, and all rows.
GET /v1/files/{file_id}/versions/ List all versions of a file, newest first scope: files:read
200 OK
[{
"version_id": "<version-id>",
"file_id": "<file-id>",
"version_number": 3,
"size": 204800,
"source_message_id": "<message-id>",
"created_at": "2026-05-20T08:14:00Z"
}]
| Field | Type | Required | Description |
|---|---|---|---|
| source_message_id | string | optional | Set when version came from an email attachment; null otherwise. |
POST /v1/files/{file_id}/versions/ Upload a new current version scope: files:write
Multipart upload
# Content-Type: multipart/form-data
# Fields: file (binary), content_type, source_message_id
JSON text upload
{
"content": "Updated text content...",
"content_type": "text/plain",
"source_message_id": null
}
Creates a new
FileVersion, updates the file record to point at the new current content, and leaves older versions available from the versions list. Collaborators can upload content versions; viewers cannot.Download
POST /v1/files/{file_id}/link/ Generate signed download link scope: files:read
Request
{ "expires_seconds": 604800 }
| Field | Type | Required | Description |
|---|---|---|---|
| expires_seconds | number | optional | Default 7 days, max 1 year. |
200 OK
{
"url": "https://api.gent.mx/v1/files/<file-id>/download/?token=...&expires=...",
"expires_at": "2026-06-07T00:00:00Z"
}
The download URL requires no authentication — the signed token is sufficient. Embed it in emails, share with contacts, or use as a public link. The token is HMAC-signed and expires at the stated time. Max expiry is 1 year.
GET /v1/files/{file_id}/download/ Download file content no auth — token-gated
Query params
| Parameter | Description | |
|---|---|---|
| token | Required — from POST /link/. | |
| expires | required | Default 100, max 1000. |
| offset | Default 0. |
Response
{
"schema": ["Vendor", "Amount", "Date", "Invoice #"],
"rows": [{
"row_id": "<row-id>",
"row_number": 1,
"data": { "Vendor": "Acme", "Amount": "$1,200", "Date": "2026-05-30", "Invoice #": "INV-441" },
"source_message_id":"<message-id>",
"created_at": "2026-05-30T14:00:00Z"
}]
}
POST /v1/files/{file_id}/rows/ Append rows scope: files:write
Request
{
"rows": [
{ "Vendor": "Acme", "Amount": "$1,200", "Date": "2026-05-30" },
{ "Vendor": "Globex", "Amount": "$850", "Date": "2026-05-31", "Notes": "Net 30" }
],
"source_message_id": "<message-id>"
}
201 Created
{
"appended": 2,
"rows": [{
"row_id": "row-1",
"row_number": 1
}]
}
New column names (e.g.
Notes above) are automatically merged into the file's schema. Columns are additive — existing columns are never renamed or removed. Row numbers are assigned sequentially and are stable.Workflow writes
| Surface | Additional fields | Notes |
|---|---|---|
writes[] | "source": {"from": "trigger.file|trigger.email|trigger.attachment|trigger.embedded_image|event.payload"}, "target": {"type": "file.spreadsheet.rows", "file_id": "<spreadsheet-id>"} or any target from /v1/workflows/reference/; "policy" is optional | Public workflow path for extracted data and deterministic side effects. Gent reads the target contract, extracts values from the source, validates required fields, and either commits the result or holds it for review. Attachment and embedded-image sources use safe references and promote the selected blob into Files before extraction. Requires workflows:full. |
source.extract | "task": "extract_invoice", optional "engine", "ocr_engine", and "config" | Optional preparation inside a write. Use it when the source needs OCR or structured extraction before the target can be committed. |
source.map | Optional explicit field overrides such as {"Amount": "total_amount"} | Most targets infer mappings from their contract. Use this only when a client needs controlled overrides. |
Persistent instructions: Setting
instructions on a file acts as a long-running prompt that shapes every AI interaction with that file. For spreadsheets: "Extract invoice number, vendor name, amount, and payment terms." For documents: "This is a vendor profile — track their pricing, key contacts, and any commitments made." Instructions set once apply to all future workflow writes targeting the file.Provenance: Every row (
source_message_id) and every version (source_message_id) traces back to the email that caused the write. Follow the chain: file → version → message to reconstruct why any piece of data exists.Collaborators & events: The
collaborators field is a list of contact IDs. Every file mutation (file.created, file.updated, file.version_added, file.rows_appended, file.deleted) fires a webhook event that includes the collaborator list. The inbox webhook consumer — or a workflow triggered by file_version_added — can read this list and email collaborators a fresh download link, giving them access without requiring accounts.Skill files (
file_type: "skill"): Markdown files (SKILL.md) containing structured instructions for how an agent should handle specific scenarios — persona definitions, domain-specific playbooks, tool usage guides, tone guidelines. An agent can read a skill file at runtime to load context-specific instructions. Store skills at tenant scope to share across all inboxes; store at inbox scope for per-inbox specialization.Email templates: Email templates (
GET/POST /v1/templates/) can be downloaded as files via this API. Set file_type: "template" when creating a file from template content. Editing a template file and re-uploading it does not automatically update the original template record — use the templates endpoint for active template management.GDPR Subject Access Request:
GET /v1/compliance/export/ returns a JSON data export and, when an inbox_id is available, also saves it as a file_type: "other" file and includes a _download key with a signed link valid for 7 days. The file is retrievable later via the Files API.Labels
Create and manage labels, then apply them to messages or contacts. Labels carry a scope that controls what agents can do with them — set at creation time. Labels with type contact or any can also be used to broadcast a message to every contact in the group.
GET /v1/labels/reference/ List label scopes and type metadata no auth required
200 OK
{
"version": "2026-06-06",
"resource": "labels",
"payload": {
"create_label": { "method": "POST", "path": "/v1/labels/" },
"apply_message_label": { "method": "POST", "path": "/v1/messages/{message_id}/labels/" },
"apply_context_label": { "method": "POST", "path": "/v1/context/{label_id}/entities/" }
},
"fields": [...],
"label_scopes": [
{
"scope": "read_apply",
"can_apply": true,
"can_remove": false
}
],
"label_types": [
{
"type": "contact",
"client_create_supported": false
}
]
}
Current create/update payloads expose
label_scope. label_type is returned as metadata for system-created and inferred labels, including contact and calendar contexts.GET /v1/labels/ List labels for an inbox scope: labels:read
[
{
"id": "<label-id>",
"inbox_id": "<inbox-id>",
"name": "priority",
"label_scope": "read_write",
"color": "#e8a045",
"created_at": "2026-05-01T10:00:00Z",
"updated_at": "2026-05-22T10:00:00Z"
}
]
POST /v1/labels/ Create a label scope: labels:write → id
Request
{
"inbox_id": "you@example.com",
"name": "priority",
"label_scope": "read_write",
"color": "#e8a045"
}
| Field | Type | Required | Description |
|---|---|---|---|
| inbox_id | string | required | — |
| name | string | required | — |
| label_scope | string | optional | Read_only · read_apply · read_write (default: read_write) |
| color | string | optional | — |
201 Created
{
"id": "<label-id>",
"inbox_id": "<inbox-id>",
"name": "priority",
"label_scope": "read_write",
"color": "#e8a045",
"created_at": "2026-05-22T10:00:00Z",
"updated_at": "2026-05-22T10:00:00Z"
}
POST /v1/messages/{id}/labels/ Apply a label to a message scope: labels:write
Request
{
"label_id": "<label-id>"
}
204 No Content
# Empty body — success indicated by 204 status code.
DEL /v1/messages/{id}/labels/{label_id}/ Remove a label scope: labels:write
# 204 No Content — empty body
GET /v1/labels/{label_id}/ Get a label scope: labels:read
{
"id": "<label-id>",
"inbox_id": "<inbox-id>",
"name": "priority",
"label_scope": "read_write",
"color": "#e8a045",
"created_at": "2026-05-01T10:00:00Z",
"updated_at": "2026-05-22T10:00:00Z"
}
PATCH /v1/labels/{label_id}/ Update a label scope: labels:write
Request — all fields optional
{
"name": "urgent",
"label_scope": "read_apply",
"color": "#c23b2a"
}
| Field | Type | Required | Description |
|---|---|---|---|
| name | string | optional | Omit fields to leave unchanged. |
| label_scope | string | optional | — |
| color | string | optional | — |
200 OK
{
"id": "<label-id>",
"inbox_id": "<inbox-id>",
"name": "urgent",
"label_scope": "read_apply",
"color": "#c23b2a",
"created_at": "2026-05-22T10:00:00Z",
"updated_at": "2026-05-22T10:00:00Z"
}
DEL /v1/labels/{label_id}/ Delete a label scope: labels:write
# 204 No Content — empty body
Note: For
POST /v1/labels/ with token auth, inbox_id must still be included in the request body — this is the one write endpoint where it is not auto-derived.Label CRUD and apply/remove accept agent token or session auth. Token calls require
labels:write (or labels:read for GET). Apply and remove also require labels:write. The label's own scope then further gates what the agent can do: read_only blocks both apply and remove; read_apply allows apply only; read_write allows both.
Label broadcast —
POST /v1/messages/broadcast/ sends a message to every contact tagged with a label.
- Label
label_typemust becontactorany— broadcasting to amessage-only label returns 400. - Asynchronous — returns 202 with a
broadcast_id; pollGET /v1/messages/broadcasts/{broadcast_id}/for aggregate delivery status. - The inbox's signature is appended to each message (if set).
- Each recipient gets a unique unsubscribe link as a footer and via
List-Unsubscribe/List-Unsubscribe-Postheaders (RFC 2369 + RFC 8058). - Contacts who previously unsubscribed from this label, or whose address has hard-bounced, are skipped automatically (
suppressed_countin the status response).