Documents
Upload a file once and reference it by id wherever you need it. Or pull it straight from your own drive, copied only when you attach it.
A document is uploaded once and referenced by id wherever you need it: on a letter, as the thing an approval routes, as an attachment picked up midway through a file.
The upload is multipart, not JSON: the bytes travel as a file part rather than base64 in a body, so a 20 MB statement costs 20 MB rather than 27.
POST https://api.keepable.co/sender/v2/documents
Authorization: Bearer {{KEEPABLE_TOKEN}}
Content-Type: multipart/form-data; boundary=----keepable
------keepable
Content-Disposition: form-data; name="file"; filename="payslip-march.pdf"
Content-Type: application/pdf
%PDF-1.4…
------keepable
Content-Disposition: form-data; name="name"
March payslip
------keepable--The only required part is file. name is the display name, up to 120
characters, and defaults to the uploaded filename.
{
"document_id": "doc_01J8ZQ4T",
"name": "March payslip",
"media_type": "application/pdf",
"size_bytes": 184320,
"sha256": "9f2a1c…",
"created_at": "2026-07-14T09:12:00Z"
}From your own drive
If your organisation has connected its work storage, you can pull a file
straight from it instead of round-tripping through your own systems. The
storage_ref is the one GET /storage/items hands back:
POST https://api.keepable.co/sender/v2/documents/from-storage
Authorization: Bearer {{KEEPABLE_TOKEN}}
Content-Type: application/json
{ "storage_ref": "01BXYZ...", "name": "Facility memo" }This is the only operation that moves bytes across that boundary, and it does so exactly once, at the moment you attach. Nothing is mirrored, synced, or kept in step. See Work connections.
It needs documents.write and storage.read: the right to create over here and
the right to read over there.
Retrieval
GET /documents/{document_id}Returns metadata and a signed download_url that expires in about an hour. It
is minted fresh on every call, so re-fetch this resource rather than storing the
URL.
A signed URL is a bearer credential. It carries no identity, so whoever holds it is the reader until it expires. Keep it server-side: it should never reach a browser history, a log line, or a shared screen.
Retrieval inherits the correspondence's visibility. A caller who could not read
that file's contents gets a 403 here too, because a document id outlives every
screen it ever appeared on, and scoping the collection endpoint while leaving
this one open would be a lock on the door beside an open window.
A document that came back as a response is held to the sensitive gate as
well: retrieving one needs responses.sensitive. Minting a URL for a restricted
file's document is written to the minute ledger, because reading somebody's
identity documents is a disclosure and the record of it is the point.
Deleting one
DELETE /documents/{document_id}For the file you uploaded by mistake. It answers 204, and 409
(code: document_in_use) the moment anything references the document: a
correspondence, a participant, a queued send, a response, or any entry in a
minute ledger. Withdrawing a document from a file does not make it deletable,
because the withdrawal is itself minuted.
Unattached documents are swept automatically after seven days, so this is the
immediate version of something that happens anyway. There is deliberately no
GET /documents: a document is reached from the file that carries it.
Environments
Two data planes behind one API, chosen entirely by your key's prefix. There are no separate hostnames and no separate accounts.
Correspondence
One endpoint sends a letter, runs a signature ceremony, routes a file for approval, asks for a consent decision, or publishes a form. The kind decides which fields matter.