standards
Files and documentsRecords

Files and documents

Upload a file, attach it to a record as a document, and know which tool does each step.


A file is an uploaded blob: bytes plus a name, a MIME type and a size. A document is a title over an ordered pack of files, always attached to one record. There are no slots: a document attribute on an object only says what the pack accepts.

Upload a file

POST /files/upload takes multipart/form-data with the bytes in the files field, up to 10 files per request and 50 MiB per file. The MIME type, the extension and the file signature are checked before anything is stored; a mismatch is a 400.

curl https://api.standards.new/v1/files/upload \
  -H "Authorization: Bearer $STANDARDS_API_KEY" \
  -F "[email protected]"

The answer is an array of file objects with id, name, mimeType and size. Files are private unless uploaded public (see below). GET /files/:id/url returns a signed URL that expires after one hour; request a fresh one each time you need it.

To sign many files at once, POST /files/urls with { "ids": [...] } (at most 200) returns { "urls": { "<fileId>": "<signedUrl>" } }. A file you cannot read is simply absent from urls. Add "variant": "thumbnail" to get images resized to 72 × 72 for previews; other file types come back unchanged.

PUT /files/:id renames a file or moves it to another folder: its body takes name and folderPath and nothing else. Any other field — documentId, position, the OCR fields — is a 400 with SCHEMA_VALIDATION_FAILED, and nothing is written. A file joins a document through the document routes, never through this one.

A file a browser would execute — SVG, JavaScript, HTML — is served as a download rather than rendered: its signed URL carries Content-Disposition: attachment. Upload it, hand it to someone, store it in a Skill; it never runs in the browsing context.

Public files

Add public=true to the upload and anyone holding the file's link can open it, without signing in and without being a member of the workspace:

curl https://api.standards.new/v1/files/upload \
  -H "Authorization: Bearer $STANDARDS_API_KEY" \
  -F "[email protected]" \
  -F "public=true"

The file comes back with isPublic: true and a publicUrl, {publicFilesUrl}/{fileId}; every other file carries publicUrl: null. Opening the link answers a redirect to a signed URL. The link itself never names a bucket, a path or a storage provider, so it survives a storage move. From React, uploadFiles(files, { public: true }) does the same. public takes true or false only: any other value (yes, 1, True) is a 400 with SCHEMA_VALIDATION_FAILED, and nothing is stored.

A file is public because it was uploaded public. Nothing turns an existing file public, and a public file never joins a document: attaching it to a pack answers 404 like a missing file.

Pasting or dropping a file into a rich text field uploads it public, silently, whatever the record's visibility. The protection is the link: its id is a random UUID nobody can guess. Whoever obtains the link — a forwarded email, an export, a Referer header — can read the file, whether or not they can see the record. In a rich text, only this installation's public file images render; any other image shows its alt text.

A rich text value is markdown, and an image in it is ![alt](url). Once someone resizes an image in the editor, it is stored as <img src="url" alt="alt" width="320"> instead, with the width in pixels; an image nobody resized keeps the ![alt](url) form. Write either form through the API: the editor shows a width wider than the column at the column's width.

A public link is permanent: there is no route that revokes it, and removing an image from a rich text does not delete the file, since the same link may be shared elsewhere. Purging a file is an operator job on the installation. After a purge, the instance that ran it answers 404 at once; other instances, browsers and CDNs may keep redirecting for up to one hour, and a signed URL already obtained stays valid for up to 24 hours.

GET /files lists every file not attached to a document, public ones included, to every member with files:read — among them the files pasted into private records.

Group files into a document

A document attribute on an object constrains its pack with accepts (MIME globs, exact types or extensions) and maxFileSize. A file the attribute refuses is DOCUMENT_MIME_NOT_ACCEPTED. See Data model / Relations and documents.

POST /records/:object/:id/documents/attach uploads one file and creates the document in one multipart call. It checks update on the object, not a file permission. Pass attributeName to choose the attribute (default attachments), title for the document, or documentId to append to an existing pack.

curl https://api.standards.new/v1/records/contacts/$RECORD_ID/documents/attach \
  -H "Authorization: Bearer $STANDARDS_API_KEY" \
  -F "[email protected]" \
  -F "attributeName=identityDocument" \
  -F "title=Passport"

GET /records/:object/:id/documents lists a record's documents grouped by attribute. GET /documents/:id/content returns the extracted text of the whole pack, ready for an LLM. A PDF or an image gets its text by OCR. A plain-text file — text/plain, CSV, TSV, Markdown, HTML — gives its own content instead, with no OCR: UTF-16 when it starts with a UTF-16 byte-order mark, otherwise UTF-8, or Windows-1252 when the bytes are not valid UTF-8, HTML read as its text rather than its markup, up to its first million characters. Any other type — an Outlook .msg, a .docx, a .zip — has no text to extract: its ocrStatus is "unsupported", which is final and not a failure, and nothing is queued for it.

POST /documents/:id/rename changes a document's title, never its files: its body is { "name": string }, stored trimmed. A missing or non-string name, a blank one, or one containing a NUL character is a 400 with SCHEMA_VALIDATION_FAILED, and the title is unchanged. A folder id you may edit is also a 400 with SCHEMA_VALIDATION_FAILED and nothing is written: a folder renames through PATCH /folders/:id with { "title" }, which trims the title and refuses a blank one.

Drives

A drive is the folder tree a record's documents live in. Drives are records of the native drives object, so POST /records/drives creates one and every record route works on it: filters, search, permissions. A drive is shared or private through record visibility, exactly like any record; a private drive you cannot see answers the same 404 as a missing id. A private record's drive follows its shares: a reader lists and downloads its documents, and an editor also adds and edits them and decides who has access. A reader who tries to attach a document, create a folder, or rename, move or delete a document or folder gets 403, not 404 — the drive is visible, the action is not; the same 403 answers a POST /files create or a move that targets such a drive by id. Writes addressed by file id — including a packed-file re-link, attaching an already-uploaded file to another document — still answer 404 to a reader of the file's current drive, since they resolve that drive first. Someone who cannot see the record at all gets the same 404 as a missing id on every one of these routes. See Record access.

Listing a drive level in pages

A folder in a drive can hold thousands of items, so one level of a drive is also readable a page at a time, already sorted:

curl "https://api.standards.new/v1/folders/record-drive/<recordId>/children/page?parentId=<folderId>&sort=name-asc&limit=100" \
  -H "Authorization: Bearer stndrds_your_api_key"

Omit parentId for the drive's root. It answers { items, nextCursor }: each item is a document with its sub count and its files, and nextCursor is null on the last page. Pass it back as cursor to read the next page. A cursor is opaque and only valid for the sort that issued it; any other cursor is a 400.

Folders always come before files. Then each sort orders by:

  • manual (the default): the drag order.
  • name-asc or name-desc: the title, read numerically ("File 2" before "file 10") and ignoring case and accents. The comparison is locale-neutral, so a language's own alphabet rules do not apply.
  • date-desc or date-asc: the last update, to the millisecond.

Items that compare equal are ordered by id. A page resumes strictly after the previous page's last item, so an item added while you page is never listed twice. limit defaults to 100 and is capped at 500. Visibility is the same as for every other drive read: a drive you cannot see answers 404 below its root, and its root lists nothing.

Searching a drive by name

The open drive and a record's Documents tab carry a loupe that searches the whole drive — every folder and file, at any depth — by title, and lists each hit under the path that places it. Picking a folder navigates into it; picking a file opens it. The same walk is exposed to integrations:

curl "https://api.standards.new/v1/folders/record-drive/<recordId>/search?q=acps" \
  -H "Authorization: Bearer stndrds_your_api_key"

It answers { recordId, hits, truncated? }: each hit is { id, kind: "folder" | "file", title, path, parentId, depth } with path the /-joined titles from the drive root down to the hit and depth its distance from that root (0 for an entry at the root). Hits are listed shallowest first. The match is a case-insensitive substring on the title — it does not fold accents, unlike record search. The walk stops five levels deep and at 200 hits, and truncated says when not every match is listed. An empty q answers no hits; a drive you cannot see answers 404 like any other read, and the route needs the documents read permission, as listing a drive's archived items does.

A record's Documents tab lists, beside its own drive, the drives of the records it relates to. Drag a file or a folder, or a selection of them, onto one of those drives to move it to that drive's root. It is the same move as "Move to…" with another record's drive picked: POST /folders/:id/move with toRecordId. The drive already open is not a drop target, and a drive you can only read refuses the move with 403, as every write to it does.

Agent browser screenshots

An agent's browser_computer tool (its screenshot and zoom actions, and a browser_batch step that captures one) stores each captured image as a file under agent-screenshots/<session>/ rather than inline in the conversation. See Browser devices for the full tool set. That file is readable only by whoever can already read the agent session that took it — the same visibility rule as the session's messages. Anyone else's GET /files/:id/url (or any other read) answers 404, exactly as for a file that does not exist, and the screenshot never appears in that person's file listings either.

These files cannot be modified, moved or deleted through the files API: every write intent — attaching, renaming, moving into another drive, deletion — is refused regardless of who is asking, since a screenshot is a session artifact, not a document someone owns.

Which tool uploads

Upload with one of these:

  • SDK: standards.files.upload for a standalone file, public or private, and standards.documents.attachToRecord for the one-call record attach.
  • CLI: standards records attach-document <object> <id> --file ./contract.pdf --attribute contracts. See CLI / Documents and files.
  • MCP: organize with action: "attach", for a file already in Standards. See MCP / Tools.
  • REST: the one-call record attach above (POST /records/:object/:id/documents/attach).

Next steps