# stapel-docs 0.6.1

Google-Drive-style workspace documents: a folder tree, one-entity-per-type documents over an open type registry, content-addressed snapshot storage with an update journal and revision history, optimistic-lock editing (v1), trash with irreversible purge, a swappable object-storage seam, comm ingest (docs.create_document) and registry-driven export (PDF built in).

Contract: axes 1 · surface 89 · extension points 7 · operations 44 · error codes 84.
Generated from docs/capabilities.json by `stapel-llms-txt` — do not edit; drift-gated by `make contract-check`.

## Configuration axes — what a product switches on
Settings keys; `default` is what you get by saying nothing. Turning an axis off unmounts the operations it gates.
- SHARING [enum, default {"MODES": [], "RESOLVERS": {}, "LINK": {"ANONYMOUS": false, "MAX_LEVEL": "view", "TTL_DAYS": 30}}] — Document sharing beyond the workspace
  Composite axis over STAPEL_DOCS['SHARING']: MODES (additional grant sources, subset of whitelist|link), RESOLVERS (subject-reference resolvers), LINK.ANONYMOUS, LINK.MAX_LEVEL and LINK.TTL_DAYS. Both grant sources are IMPLEMENTED since 0.6.0 and the axis still ships CLOSED (MODES=[], no anonymous links, view ceiling, 30-day TTL): a host opens it deliberately, one settings line, and configuring what this version does not sanction is a loud system-check error (stapel_docs.E010-E014), never a silent no-op.

## Usage surface — call these before writing your own
This is the answer to "does Stapel already have something for X?". `instead of` names the outside symbol this one displaces.
### gate_function
- append_updates — stapel_docs.services.append_updates
  Append opaque commutative updates (crdt-discipline types only) at ++head_seq each, with client_id/client_seq retry dedup. Journal appends deliberately do NOT emit document.updated (bus economy).
- assert_body_mutable — stapel_docs.services.assert_body_mutable
  Refuse a generic body write to a type that owns its own write path: file bodies come from an upload session (where size/MIME/quota policy lives) and a vanished type is read-only. Skipping it re-opens the content PUT as a second, unpoliced door into blob bodies.
- assert_body_size — stapel_docs.services.assert_body_size
  Refuse a snapshot body above MAX_BODY_BYTES (413). Every path that accepts client bytes calls it before touching storage — a body checked after the write has already cost the store.
- assert_quota — stapel_docs.services.assert_quota
  Refuse a write that would push the workspace past WORKSPACE_QUOTA_BYTES (507). Call it before accepting bytes, not after storing them.
- authorize — stapel_docs.authz.authorize
  instead of: stapel_core.comm.call, stapel_core.django.workspaces.require_capability
  THE access decision for anything document-shaped — HTTP, presigned URLs, a future realtime stream. Returns allow|deny|unavailable (503, never 403-on-outage); fail-closed via workspaces.check_capability by comm name. Every new read or write path routes through this — a second membership check is how a share mode later ships half-enforced.
- check_share_capability — stapel_docs.authz.check_share_capability
  instead of: stapel_core.django.workspaces.require_capability
  allow|deny|unavailable for docs.share.whitelist / docs.share.link — who may GIVE access. Deliberately outside authorize()'s action vocabulary: minting a grant is a workspace mandate, not a level on the document, and conflating them turns 'shared with me' into 'may share with others'.
- create_document — stapel_docs.services.create_document
  instead of: stapel_docs.models.Document.objects.create
  THE document mint — HTTP, the docs.create_document Function and the INGEST seam all land here. Refuses unknown types loudly, materializes folder_path idempotently, rides the initial body through the regular snapshot path and announces document.created in the same transaction.
- create_folder — stapel_docs.services.create_folder
  Mint a folder under the depth limit with live-sibling name uniqueness enforced in code (SQL cannot compare NULL parents). A bare Folder.objects.create skips both invariants.
- create_link — stapel_docs.services.create_link
  Mint one bearer link (secrets.token_urlsafe, mandatory expiry, level ≤ LINK['MAX_LEVEL'] refused loudly rather than clamped). Emits document.share.link_created. The second cap — never above the granter's own level — is applied by the caller through authorize().
- empty_trash — stapel_docs.services.empty_trash
  Purge the listed trashed items — or the whole workspace trash. A non-trashed or unknown id refuses the WHOLE request: partial purges are how a client deletes more than the user confirmed.
- finalize_upload — stapel_docs.services.finalize_upload
  Promote the uploaded object to the document's first version (HEAD-verified size, seq 1, first revision, pending marker cleared) and announce document.created + the byte delta. Emitting the events yourself skips every one of those steps.
- grant_access — stapel_docs.services.grant_access
  Create or raise one whitelist grant (upsert — re-granting is the sheet's ordinary gesture). Refuses a disabled mode, an unknown level, a half-named subject, and a ref whose kind has no registered resolver: a row that could only ever deny is never stored. Emits document.share.granted in the mutating transaction.
- granted_level — stapel_docs.authz.granted_level
  The MAXIMUM level the enabled grant sources give one principal on one document, plus an outage flag — the same number authorize() decided with. Present so the presentation layer never re-derives 'what may this bearer do': a second answer to that question is how a share mode ships half-enforced.
- may_write — stapel_docs.authz.may_write
  Whether a principal may ever author a write. An anonymous presenter never writes, whatever level their link carries: the journal and revision history are attributed by design, and an authorless edit is vandalism with no subject to name.
- mode_enabled — stapel_docs.authz.mode_enabled
  Kill-switch predicate for one mode. The share sheet marks rows of a disabled mode 'suspended' with it — never hides them, because an admin who cannot see an inert grant believes it was revoked.
- move_document — stapel_docs.services.move_document
  Move a document to a folder (or the workspace root), with the target folder resolved live and workspace-pinned.
- move_folder — stapel_docs.services.move_folder
  Re-parent a folder with cycle, depth (subtree height counted) and sibling-name checks — the three ways a hand-written parent assignment corrupts the tree.
- purge_document — stapel_docs.services.purge_document
  instead of: stapel_docs.models.Document.delete
  Irreversible destruction of one document: every distinct storage key of its history + journal + rows, O(document), idempotent, with document.deleted and the negative byte delta announced inside the transaction. Deleting the row alone leaks every object its history points at.
- purge_expired — stapel_docs.services.purge_expired
  Retention expiry: purge everything soft-deleted longer than TRASH_RETENTION_DAYS ago — the body of the docs_purge_expired management command; call it from a host cron/beat instead of re-deriving the cutoff.
- purge_folder — stapel_docs.services.purge_folder
  Purge a trashed folder with its trashed contents; live documents still pointing into the subtree survive at the workspace root (FK SET_NULL), never destroyed by a folder purge.
- redeem_link — stapel_docs.services.redeem_link
  Stamp first_redeemed_at once and emit document.share.link_redeemed. Call only AFTER authorize() allowed the request: the stamp is evidence somebody got in, so a refused presentation must not write it. Guarded by a conditional UPDATE, so two simultaneous first openings emit exactly one event.
- rename_folder — stapel_docs.services.rename_folder
  Rename with the sibling-name check — the same duplicate-name refusal creation applies.
- resolve_ref — stapel_docs.authz.resolve_ref
  Point-query a host resolver: is this user a member of this external container? Fail-closed on every unhappy path (unknown kind, raising resolver), answers cached ~30 s so revocation in the container reaches the document in seconds rather than never. Never copy a foreign module's membership instead of calling this.
- restore_document — stapel_docs.services.restore_document
  Untrash one document, re-announced via document.created.
- restore_folder — stapel_docs.services.restore_folder
  Untrash a subtree; restored documents are re-announced via document.created so downstream indexes come back consistent.
- restore_revision — stapel_docs.services.restore_revision
  Restore-as-new-head: the revision's bytes ride the same save path as a PUT (always minting a revision), so history is never rewritten. Copying bytes onto the row by hand loses the revision trail and the storage accounting.
- revoke_access — stapel_docs.services.revoke_access
  Delete one whitelist grant and emit document.share.revoked. Works while the mode is off: taking access away must never be the thing nobody is allowed to do.
- revoke_link — stapel_docs.services.revoke_link
  Withdraw a bearer link; idempotent (an already-revoked link keeps its first revoked_at and emits once). Revocation is terminal — revoked beats expired beats active, and a revoked link never revives.
- save_content — stapel_docs.services.save_content
  instead of: stapel_docs.storage.DocsStorage.put_bytes
  The optimistic-lock snapshot save: expected_seq is checked under select_for_update, the snapshot is content-addressed, an auto revision is minted on the configured cadence, the orphaned previous snapshot dies, the journal compacts, and document.updated + document.storage_changed leave in the same transaction. There is exactly one save path — restore and create-with-body use it too.
- storage_transaction — stapel_docs.services.storage_transaction
  instead of: stapel_docs.storage.DocsStorage.delete_object, stapel_docs.storage.DocsStorage.put_bytes
  Wrap a mutation whose object-store effects must follow the database outcome: writes are compensated when the block fails, deletes wait for the commit. Calling put_bytes/delete_object inside a transaction instead is how a rollback leaves an orphan object — or worse, deletes bytes a surviving row still points at.
- trash_document — stapel_docs.services.trash_document
  Soft-delete one document, emitting document.deleted inside the transaction — consumers learn it left the visible corpus.
- trash_folder — stapel_docs.services.trash_folder
  Soft-delete the whole live subtree, documents included, announcing each document.deleted in the same transaction. Setting deleted_at by hand strands live documents inside a trashed tree.
- update_document — stapel_docs.services.update_document
  Patch title/metadata — and strip the reserved upload-pending marker no caller outside the upload flow may set.
### factory
- attach_star — stapel_docs.services.attach_star
  Set is_starred on a single already-fetched Folder/Document — the detail-envelope counterpart of with_stars, for rows a queryset annotation can no longer reach.
- available — stapel_docs.thumbnails.available
  Is the thumbnail renderer installed? For host diagnostics and system checks that want to say so at deploy time instead of at the first 503.
- blob_key — stapel_docs.storage.blob_key
  A content-addressed key with an extension hint, for auxiliary blobs a custom flow stores next to a document under the same prefix discipline.
- content_hash — stapel_docs.storage.content_hash
  The sha256 content address of a body — the identity under which snapshots dedup. Any custom write path hashes through this or dedup silently stops.
- content_mime — stapel_docs.services.content_mime
  The Content-Type a body must be served with: the spec's mime (+charset for text) for editable types, the stored original's for file/vanished types. Hand-picking a mime per call site is how a utf-8 markdown body ships as latin-1.
- create_named_revision — stapel_docs.services.create_named_revision
  Name the CURRENT head snapshot; naming the same head twice renames the existing named revision instead of stacking rows.
- create_upload — stapel_docs.services.create_upload
  Open a presigned direct-to-storage upload (type=file): the Document row exists immediately but stays hidden from listings until finalize, and the ticket's key/TTL are what finalize later validates. A bare presigned_put_url gives you the URL and none of that.
- document_download_url — stapel_docs.services.document_download_url
  Mint a download URL for a document's current body AND record the reach in recents — the document-level wrapper of download_url, which stays the raw key-level mint for revisions.
- document_prefix — stapel_docs.storage.document_prefix
  The object-key prefix every object of a document lives under ({STORAGE_PREFIX}/{workspace}/{document}) — what makes purge O(document) and byte accounting per-workspace. Keys built outside it escape both.
- download_url — stapel_docs.services.download_url
  Presigned GET URL for a stored object key with the configured lifetime — opaque to clients; DjangoStorageBackend degrades it to a served URL, so never assume S3 URL shape.
- effective_modes — stapel_docs.authz.effective_modes
  Grant sources actually in force: deployment MODES ∩ what this version implements (∩ per-workspace narrowing once workspaces exposes settings). The one place the axis is read, so a mode can never be enabled by a code path that forgot to filter.
- effective_spec — stapel_docs.services.effective_spec
  The document's DocTypeSpec, or None when the type vanished from the registry — the degrade-to-file signal. Read specs through this per document rather than get_doc_type(), which raises on the vanished-type case the API deliberately survives.
- get_access — stapel_docs.services.get_access
  One grant of THIS document or a 404-mapped DocsError — scoped by document, so an id from another workspace addresses nothing.
- get_doc_type — stapel_docs.doc_types.get_doc_type
  One slug's spec, or DocTypeNotRegistered — the loud lookup for flows that must refuse an unknown type (ingest does). For presentation of existing rows use effective_spec, which degrades instead of raising.
- get_doc_types — stapel_docs.doc_types.get_doc_types
  instead of: stapel_docs.doc_types.BUILTIN_DOC_TYPES
  The effective {slug: DocTypeSpec} registry — builtins, then the DOC_TYPES overlay, then runtime registrations. Render type pickers and dispatch editors from this; BUILTIN_DOC_TYPES answers only "which types shipped".
- get_exporter — stapel_docs.exporters.get_exporter
  Instantiate the exporter registered for a format (builtins + EXPORTERS overlay) or raise ExportFormatUnknown — the dispatch any custom export endpoint or batch job goes through.
- get_link — stapel_docs.services.get_link
  One link of THIS document or a 404-mapped DocsError.
- get_link_by_token — stapel_docs.services.get_link_by_token
  Resolve a presented token to (document, link), 404 when it names nothing. Liveness and level are decided by authorize(), never here — and the bearer views render every refusal as 404 so the endpoint is not an oracle for guessing tokens.
- get_live_document — stapel_docs.services.get_live_document
  Fetch a non-trashed document or raise the 404-mapped DocsError — the lookup every normal endpoint starts with (trashed rows are 404 outside the trash surface).
- get_live_folder — stapel_docs.services.get_live_folder
  Fetch a non-trashed folder or 404, optionally pinned to a workspace — the workspace pin is what stops a cross-workspace folder_id from being honored.
- get_ref_resolver — stapel_docs.authz.get_ref_resolver
  The registered SHARING['RESOLVERS'] callable for a ref kind, or None. An unregistered kind and an unimportable path answer the same way — nothing to ask — and the caller turns that into deny.
- get_revision — stapel_docs.services.get_revision
  Fetch one revision of a given document or 404 — scoped through document.revisions so a revision id can never be read across documents.
- get_storage — stapel_docs.storage.get_storage
  instead of: boto3.client, django.core.files.storage.default_storage
  THE object-store handle for anything touching document bytes — the configured DocsStorage (Django default_storage, the bundled S3/MinIO backend, or the host's own), memoized. An export script or admin action goes through this rather than building its own boto3 client: the STORAGE seam makes the bucket, credentials and presign policy one decision instead of one per call site.
- get_thumbnail — stapel_docs.services.get_thumbnail
  (image bytes, mime) for an image file document at a fixed tier — renders once through Pillow, caches under the document's own storage prefix and registers the object so purge takes it along. 503-mapped when Pillow is absent, 400 for a non-image or an unknown tier.
- get_trashed_document — stapel_docs.services.get_trashed_document
  Fetch a trashed document or 404 — only restore/purge flows see trashed rows.
- get_trashed_folder — stapel_docs.services.get_trashed_folder
  Fetch a trashed folder or 404 — the restore/purge counterpart of get_live_folder.
- get_upload_session — stapel_docs.services.get_upload_session
  Fetch an upload session by id or 404 — the finalize endpoint's lookup.
- link_expiry — stapel_docs.services.link_expiry
  The deadline a link minted now would carry (LINK['TTL_DAYS']). TTL_DAYS=None means perpetual and becomes a century, not a null: the column is NOT NULL by the invitation canon, and a date every reader renders beats an absence every reader special-cases.
- link_settings — stapel_docs.authz.link_settings
  SHARING['LINK'] with its closed defaults merged in (ANONYMOUS/MAX_LEVEL/TTL_DAYS) — the two locks on the leakiest door, read in one place.
- list_access — stapel_docs.services.list_access
  Whitelist grants of one document, newest first, already marked suspended when the mode is off.
- list_documents — stapel_docs.services.list_documents
  Live documents of a workspace (folder/type/title-substring filters), with pending uploads excluded — the invariant a raw queryset forgets, leaking half-uploaded rows into listings.
- list_folders — stapel_docs.services.list_folders
  Live folders of a workspace — whole tree, workspace roots (parent_id=None) or one folder's children. The one listing that respects soft-deletion.
- list_links — stapel_docs.services.list_links
  Bearer links of one document, newest first, marked suspended when link sharing is off. The envelope carries live tokens — gate the caller on docs.share.link.
- mark_sharing_suspended — stapel_docs.services.mark_sharing_suspended
  Stamp is_suspended on share-sheet rows whose mode is switched off. The kill-switch is a display state, not a filter: rows stay listed so an operator can see and revoke what a re-enabled mode would restore.
- pending_uploads — stapel_docs.services.pending_uploads
  Open (pending, unexpired) upload sessions of a workspace — the set the per-workspace ceiling counts and the one a cleanup job should sweep.
- read_content — stapel_docs.services.read_content
  (body, mime, head_seq) of the current head — the spec's empty body at seq 0 before any save. The only sanctioned body read; get_bytes on a guessed key bypasses the empty-body and vanished-type semantics.
- read_updates — stapel_docs.services.read_updates
  The replay feed since a sequence number, with honest resync semantics: a since that fell out of the compaction window returns ("resync", None) instead of a silently incomplete list.
- recent_documents — stapel_docs.services.recent_documents
  Documents one user reached most recently in a workspace, newest first, live only — the read side of the recents surface.
- ref_kind — stapel_docs.authz.ref_kind
  The resolver-registry key of a subject reference: everything before the LAST colon ('chat:conversation:<id>' -> 'chat:conversation'). One rule at every namespacing depth, so a host's registered key is exactly what it wrote.
- register_doc_type — stapel_docs.doc_types.register_doc_type
  instead of: stapel_docs.doc_types.BUILTIN_DOC_TYPES
  Register (or replace) a document type at runtime — the programmatic equivalent of the DOC_TYPES settings overlay for apps that register from AppConfig.ready(). Mutating BUILTIN_DOC_TYPES reaches the same map from the wrong side and loses to the overlay.
- render — stapel_docs.thumbnails.render
  Resize image bytes so the longest edge fits a tier, as JPEG (alpha composited onto white, never upscaled). Raises ThumbnailsUnavailable without Pillow and ThumbnailSourceUnusable for bytes it cannot decode — a caller's bad input is a 400, not a 500.
- reset_storage_cache — stapel_docs.storage.reset_storage_cache
  Drop the memoized backend so the next get_storage() re-resolves the STORAGE setting — the supported way to make an override_settings block take effect. Overwhelmingly a test-harness concern.
- resource_limit — stapel_docs.services.resource_limit
  Read one integer ceiling out of the STAPEL_DOCS namespace (0 = that ceiling is off). Host code that hardcodes a byte limit ships a number the operator cannot tune and the audit cannot find.
- revision_content — stapel_docs.services.revision_content
  A revision's FULL bytes — every revision is a self-contained snapshot (I1), so one get_bytes suffices; no delta chain to walk.
- search — stapel_docs.services.search
  Workspace-scoped name search over live folders and documents, returning (kind, row, breadcrumb) triples with ancestor chains resolved in one query. Use it instead of two icontains queries plus a parent walk — the walk is the N+1 this function exists to avoid.
- set_star — stapel_docs.services.set_star
  Star or unstar one document/folder for one user, idempotently; returns whether anything changed. The only supported writer of the Star table — a direct create() skips the exactly-one-target contract the endpoint relies on.
- sharing_settings — stapel_docs.authz.sharing_settings
  The effective SHARING dict with closed defaults merged in (never a partial overlay). Read the axis through this, not settings.STAPEL_DOCS — a half-merged LINK sub-dict is how ANONYMOUS silently reads as absent rather than False.
- snapshot_key — stapel_docs.storage.snapshot_key
  The content-addressed snapshot key for a body hash — identical bodies collapse to one stored object for free.
- starred_listing — stapel_docs.services.starred_listing
  (folders, documents) one user starred in a workspace, live rows only — trashed items drop out of the listing but keep their star until purge, so a restore brings the bookmark back.
- thumbnail_key — stapel_docs.services.thumbnail_key
  Storage key of a cached thumbnail, carrying head_seq so a saved document addresses a different image instead of a stale cache. Read it to reason about the bucket; get_thumbnail is what writes it.
- touch_recent — stapel_docs.services.touch_recent
  Record that a user just reached a document (upsert + per-user trim). Call it from any NEW read path you add over document bytes; the shipped paths (content read, download-URL issuance, accepted save) already do.
- trash_listing — stapel_docs.services.trash_listing
  Everything soft-deleted in a workspace — (folders, documents) for the trash surface.
- unregister_doc_type — stapel_docs.doc_types.unregister_doc_type
  Remove one runtime registration — the teardown that stops one test's type from leaking into the next.
- with_stars — stapel_docs.services.with_stars
  Annotate is_starred on a Folder/Document queryset for one user — None (not False) when there is no user, because 'not applicable' and 'not starred' are different answers. Reach for it in any custom listing that presents docs rows; hand-writing the Exists() subquery is how the anonymous case silently becomes False.
- workspace_usage — stapel_docs.services.workspace_usage
  Everything docs.usage reports for one workspace: live/trash/total bytes, item counts and a per-type breakdown. bytes_total is the same sum the 507 quota refuses against, so a meter and a refusal can never disagree. Host billing composes an entitlement ceiling from it; docs owns the measurement, never the price.
- workspace_usage_bytes — stapel_docs.services.workspace_usage_bytes
  Stored bytes charged to a workspace (heads + revision snapshots) — the figure the quota compares against and the one to show in a usage UI.

## Extension points — what a product replaces, fork-free
- DOC_TYPES [merge_registry]
  Open document-type registry ({slug: dotted-path to a DocTypeSpec | None to remove}, merged over the txt/md/csv/file builtins) or register at runtime via register_doc_type(). A type carries its editor hint, write discipline (snapshot|crdt), mime, empty body and text extractor — sheet/slides/office are later registry entries, not schema changes.
- EXPORTERS [merge_registry]
  Export-format registry ({format: dotted-path | None}, merged over the built-in pdf). An exporter renders (document, body, spec) to (bytes, mime) — add docx/html/srt without touching the module.
- INGEST [merge_registry]
  Event-driven ingest ({action_name: dotted-path mapper}): docs subscribes to the named host actions and routes each payload through the mapper into create_document — the host never writes a bus subscriber, and docs never learns a foreign event schema.
- PRESENTERS [swap_keys]
  Every DTO is built by a presenter resolved through STAPEL_SWAP — swap keys DOCS_FOLDER_PRESENTER / DOCS_DOCUMENT_PRESENTER / DOCS_REVISION_PRESENTER reshape the folder/document/revision envelopes of every endpoint without forking a view.
- SHARING_RESOLVERS [merge_registry]
  SHARING['RESOLVERS'] ({ref_kind: dotted-path}) — the whitelist subject-reference resolver registry. A resolver is a point query, resolve(ref, user_id) -> bool: the host answers 'is this person in that container' from its own data or a comm call, and docs never imports the source module or copies its membership. Fail-closed on both boundaries: an unregistered kind is refused at MINT (no dead rows), and an unknown kind or a raising resolver denies at READ.
- STORAGE [dotted_path]
  Swap the object-storage backend (DocsStorage implementation, REPLACE): any Django storage by default, bundled S3/MinIO backend ([s3] extra), or your own. Every byte of document content I/O goes through it — there is no second I/O path.
- serializer_seams [class_override]
  Every view declares request/response serializer seams (SerializerSeamMixin) — subclass the view, override the attribute, remount the URL.

## Fits with — fleet dependencies
- stapel-auth (optional) — every endpoint requires an authenticated non-anonymous user (IsNotAnonymousUser); stapel-auth is the shelf's session issuer — any stapel-core-compatible JWT issuer satisfies the check
- stapel-core (required) — comm bus (docs.create_document Function, document.* Actions, the workspaces.check_capability client), JWT authentication, unified error envelope, GDPR provider registry, AppSettings config layer
- stapel-workspaces (optional) — every HTTP authorization asks the workspaces.check_capability comm Function (fail-closed, deny-by-default) — stapel-workspaces is the shelf's answerer; any provider of that Function satisfies the contract, and without one every HTTP request denies (comm ingest still works)

## HTTP operations (44) — call by operationId, never by a typed path
Paths are relative to `/docs/api/v1/`.
### Docs / content
- GET /documents/{document_id}/content — docs_api_v1_documents_content_retrieve
- PUT /documents/{document_id}/content — docs_api_v1_documents_content_update
- GET /documents/{document_id}/download — docs_api_v1_documents_download_retrieve
- GET /documents/{document_id}/export — docs_api_v1_documents_export_retrieve
- GET /documents/{document_id}/thumbnail — docs_api_v1_documents_thumbnail_retrieve
### Docs / documents
- POST /documents — docs_api_v1_documents_create
- DELETE /documents/{document_id} — docs_api_v1_documents_destroy
- GET /documents — docs_api_v1_documents_list
- PATCH /documents/{document_id} — docs_api_v1_documents_partial_update
- POST /documents/{document_id}/restore — docs_api_v1_documents_restore_create
- GET /documents/{document_id} — docs_api_v1_documents_retrieve
### Docs / folders
- POST /folders — docs_api_v1_folders_create
- DELETE /folders/{folder_id} — docs_api_v1_folders_destroy
- GET /folders — docs_api_v1_folders_list
- PATCH /folders/{folder_id} — docs_api_v1_folders_partial_update
- POST /folders/{folder_id}/restore — docs_api_v1_folders_restore_create
- GET /folders/{folder_id} — docs_api_v1_folders_retrieve
### Docs / recents
- GET /recents — docs_api_v1_recents_list
### Docs / revisions
- GET /documents/{document_id}/revisions/{revision_id}/content — docs_api_v1_documents_revisions_content_retrieve
- POST /documents/{document_id}/revisions — docs_api_v1_documents_revisions_create
- GET /documents/{document_id}/revisions/{revision_id}/download — docs_api_v1_documents_revisions_download_retrieve
- GET /documents/{document_id}/revisions — docs_api_v1_documents_revisions_list
- POST /documents/{document_id}/revisions/{revision_id}/restore — docs_api_v1_documents_revisions_restore_create
### Docs / search
- GET /search — docs_api_v1_search_list
### Docs / sharing
- POST /documents/{document_id}/access — docs_api_v1_documents_access_create
- DELETE /documents/{document_id}/access/{access_id} — docs_api_v1_documents_access_destroy
- GET /documents/{document_id}/access — docs_api_v1_documents_access_list
- POST /documents/{document_id}/links — docs_api_v1_documents_links_create
- DELETE /documents/{document_id}/links/{link_id} — docs_api_v1_documents_links_destroy
- GET /documents/{document_id}/links — docs_api_v1_documents_links_list
- GET /shared/{token}/content — docs_api_v1_shared_content_retrieve
- GET /shared/{token}/download — docs_api_v1_shared_download_retrieve
- GET /shared/{token} — docs_api_v1_shared_retrieve
### Docs / starred
- POST /documents/{document_id}/star — docs_api_v1_documents_star_create
- DELETE /documents/{document_id}/star — docs_api_v1_documents_star_destroy
- POST /folders/{folder_id}/star — docs_api_v1_folders_star_create
- DELETE /folders/{folder_id}/star — docs_api_v1_folders_star_destroy
- GET /starred — docs_api_v1_starred_retrieve
### Docs / trash
- POST /trash/empty — docs_api_v1_trash_empty_create
- GET /trash — docs_api_v1_trash_retrieve
### Docs / updates
- POST /documents/{document_id}/updates — docs_api_v1_documents_updates_create
- GET /documents/{document_id}/updates — docs_api_v1_documents_updates_retrieve
### Docs / uploads
- POST /uploads — docs_api_v1_uploads_create
- POST /uploads/{upload_id}/finalize — docs_api_v1_uploads_finalize_create

## Error codes (84) — the StapelError envelope
Render `t(code, params)`; branch UX on the remediation. Localized text lives in docs/errors.<lang>.md, not here.
- error.400.bad_request [400] fix_input
- error.400.captcha_invalid [400] retry
- error.400.captcha_required [400] retry
- error.400.docs_bad_since [400] fix_input
- error.400.docs_duplicate_name [400] fix_input
- error.400.docs_export_format [400] fix_input
- error.400.docs_folder_cycle [400] fix_input
- error.400.docs_folder_depth [400] fix_input
- error.400.docs_not_trashed [400] fix_input
- error.400.docs_share_level [400] fix_input
- error.400.docs_share_mode_disabled [400] fix_input
- error.400.docs_share_ref_kind [400] fix_input
- error.400.docs_share_subject [400] fix_input
- error.400.docs_thumbnail_tier [400] fix_input
- error.400.docs_thumbnail_unsupported [400] fix_input
- error.400.docs_too_many_updates [400] fix_input
- error.400.docs_too_many_uploads [400] fix_input
- error.400.docs_type_not_editable [400] fix_input
- error.400.docs_unknown_type [400] fix_input
- error.400.docs_updates_not_crdt [400] fix_input
- error.400.docs_upload_expired [400] retry
- error.400.docs_upload_mime [400] fix_input
- error.400.docs_upload_mismatch [400] fix_input
- error.400.docs_upload_state [400] fix_input
- error.400.docs_upload_unmeasurable [400] fix_input
- error.400.expected_list [400] fix_input
- error.400.field.blank [400] fix_input {field}
- error.400.field.does_not_exist [400] fix_input {field}
- error.400.field.invalid [400] fix_input {field}
- error.400.field.invalid_choice [400] fix_input {field}
- error.400.field.max_length [400] fix_input {field,max_length}
- error.400.field.max_value [400] fix_input {field,max_value}
- error.400.field.min_length [400] fix_input {field,min_length}
- error.400.field.min_value [400] fix_input {field,min_value}
- error.400.field.null [400] fix_input {field}
- error.400.field.required [400] fix_input {field}
- error.400.field.unique [400] fix_input {field}
- error.400.invalid_ad_id [400] fix_input
- error.400.validation_error [400] fix_input
- error.400.verification_failed [400] verify
- error.400.verification_invalid_factor [400] verify
- error.401.docs_share_auth_required [401] reauthenticate
- error.401.unauthorized [401] reauthenticate
- error.402.payment_required [402] retry
- error.403.docs_forbidden [403] retry
- error.403.docs_upload_owner [403] retry
- error.403.forbidden [403] retry
- error.403.network_blocked [403] contact_support
- error.403.verification_enrollment_required [403] verify
- error.403.verification_required [403] verify
- error.404.ad_not_found [404] retry
- error.404.docs_access_not_found [404] retry
- error.404.docs_document_not_found [404] retry
- error.404.docs_folder_not_found [404] retry
- error.404.docs_link_not_found [404] retry
- error.404.docs_revision_not_found [404] retry
- error.404.docs_upload_not_found [404] retry
- error.404.not_found [404] retry
- error.404.verification_challenge_not_found [404] verify
- error.405.method_not_allowed [405] retry
- error.406.not_acceptable [406] retry
- error.408.request_timeout [408] retry
- error.409.conflict [409] fix_input
- error.409.docs_seq_conflict [409] fix_input
- error.410.gone [410] retry
- error.412.docs_missing_if_match [412] retry
- error.413.docs_body_too_large [413] retry
- error.413.docs_export_too_large [413] retry
- error.413.docs_update_too_large [413] retry
- error.413.docs_upload_too_large [413] retry
- error.413.payload_too_large [413] retry
- error.415.unsupported_media_type [415] retry
- error.422.unprocessable_entity [422] wait_and_retry
- error.423.locked [423] wait_and_retry
- error.423.verification_locked [423] wait_and_retry
- error.429.rate_limit [429] wait_and_retry {retry_after_minutes}
- error.429.too_many_requests [429] wait_and_retry
- error.500.internal [500] contact_support
- error.503.docs_download_url_unavailable [503] retry
- error.503.docs_exporter_unavailable [503] retry
- error.503.docs_thumbnails_unavailable [503] retry
- error.503.docs_workspaces_unavailable [503] retry
- error.503.mandate_unavailable [503] retry
- error.507.docs_workspace_quota [507] retry
