Keepable
Sender API

Exports

Pull form answers, a consent register, or claim codes out as a CSV. Same three steps every time, and the same first two columns.

An export is a copy of what the API would show you, taken as a CSV. It inherits the file's visibility rule rather than routing around it with a signed URL.

POST https://api.keepable.co/sender/v2/correspondence/cor_01J8ZQ4T/exports
Authorization: Bearer {{KEEPABLE_TOKEN}}
Content-Type: application/json

{ "kind": "responses" }
{ "export_id": "exp_01J8ZQ4T", "kind": "responses", "status": "queued", "created_at": "2026-07-14T09:12:00Z" }

Poll GET /exports/{export_id} until status is ready; it runs queuedrunningready, or failed. There is no webhook for this one: an export is something you asked for and are waiting on, not something that happens to you.

A ready export carries a signed download_url with a download_expires_at. It is minted per poll, so re-poll rather than storing it, and fetch it server-side: it carries no identity, and this one is a spreadsheet of other people's data.

Three kinds

kindWhat it holds
responsesForm answers, one row per participant, files as document ids.
registerThe consent register a DPO keeps: a projection over the sealed decisions.
claimsCode, declared name, ref, state, expiry: the fulfillment file.

All three lead with the same two columns, in the same order: the person, then your reference for them. Uniform because a reader who downloads two of these for the same audience will join them to each other, and because the reference is the only column that rejoins any of them to a contact you hold. A row with an empty reference is one you sent without one.

Files do not travel

An export never bundles bytes or hot-links them. A row carries three distinct things, and conflating them is the mistake this format is shaped to prevent:

  • Identity: the filename.
  • Verification: the receipt code. It proves a file somebody already holds, and fetches nothing, which is why it is safe in a spreadsheet.
  • Retrieval: a link back into the portal, which resolves only in an authenticated session and then streams the bytes.

Scopes

Every export needs correspondence.read, and a responses or register one needs responses.read on top: the kind of thing being copied decides what it takes to copy it.

Sensitive answers

Masked by default. include_sensitive: true exports them in the clear, which needs responses.sensitive, needs reader rights on the file, and is written to the minute ledger.

An export is the bulk form of a reveal, so it is held to the same gate. Otherwise the cheaper route is the way around the gate and the gate is decoration. Collecting a sensitive export is minuted too, not only creating it: holding the strong scope to create and a weaker one to collect would have been the same hole one step later.

The masked default is usually what you want. Most downstream reconciliation needs the last four digits and nothing else.

On this page