Keepable
Sender API

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.

On this page