Keepable docs
Sender API

Consent requests

Ask an identity-addressed recipient to grant or decline something specific, get the decision back over a webhook, and keep a verifiable receipt on both sides.

A consent request lets you ask a recipient, in writing, for permission for one specific thing, and get an attributable grant or decline back. It is a typed, returned workflow over the same machinery as content and agreements: you author the request, Keepable delivers it to the recipient's inbox, the recipient decides from their authenticated session, and the decision returns to you over a webhook with a verifiable receipt.

It is not a consent dashboard or a national consent authority. You remain the system of record: Keepable carries the exact request and returns the decision, and names the external authority your request relates to without claiming to control it. The workflow carries request and decision metadata only; it does not collect or broker the underlying personal data.

Consent requests need consent.write to author, send, and acknowledge, and consent.read to read and export receipts.

Lifecycle

statusMeaning
draftAuthored but not yet sent. Still editable; not visible to the recipient.
activeSealed and delivered. The clock to expires_at is running and the recipient can decide.
granted / declinedThe recipient's terminal decision, bound to the exact sealed request version.
withdrawnYou delivered a withdrawal acknowledgement before a decision.
expiredThe validity window passed before the recipient decided.

Each transition fires a webhook: consent.requested, consent.decided (carrying action), consent.replaced, consent.expired, and consent.withdrawn, so you can drive your own workflow off the events instead of polling.

Author a request

Create the request as a draft. You address the recipient by nin or email, state the purpose, and describe what you are asking for. Authoring does not deliver anything yet.

curl https://api.keepable.co/sender/tenants/ten_01HXP/consent-requests \
  -H "Authorization: Bearer $KEEPABLE_TOKEN" \
  -H "Keepable-Version: 2026-05-24" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{
    "recipient": { "kind": "nin", "value": "12345678901" },
    "purpose": "Share your verified identity profile with Acme Bank",
    "requested_action": "read",
    "data_categories": ["identity", "address"],
    "use_mode": "one_time",
    "withdrawal_instructions": "Reply STOP or withdraw in the Acme app",
    "authority_name": "NIMC",
    "authority_reference": "nimc-ref-123",
    "external_reference": "acme-req-9",
    "human_text": "Acme Bank is asking to read your identity profile to open your account."
  }'
const res = await fetch(
  "https://api.keepable.co/sender/tenants/ten_01HXP/consent-requests",
  {
    method: "POST",
    headers: {
      Authorization: `Bearer ${token}`,
      "Keepable-Version": "2026-05-24",
      "Idempotency-Key": crypto.randomUUID(),
      "Content-Type": "application/json",
    },
    body: JSON.stringify({
      recipient: { kind: "nin", value: "12345678901" },
      purpose: "Share your verified identity profile with Acme Bank",
      requested_action: "read",
      data_categories: ["identity", "address"],
      use_mode: "one_time",
      authority_name: "NIMC",
      external_reference: "acme-req-9",
    }),
  },
);

const { request_id, status } = await res.json();
201 Created
{ "request_id": "cr_01HXP", "status": "draft", "purpose": "Share your verified identity profile with Acme Bank", "created_at": "2026-06-28T09:00:00Z" }

Fields

FieldMeaning
recipient{ "kind": "nin" | "email", "value": "…" }. Resolved to a registered recipient and stored as a keyed-HMAC pseudonym; never echoed back.
purposeRequired. The one specific thing you are asking permission for.
requested_action / data_categoriesWhat you want to do, and which categories it touches.
use_modeone_time or recurring. Carried to the recipient and into the receipt; Keepable does not enforce downstream use.
withdrawal_instructionsHow the recipient can later withdraw, in your own words.
authority_name / authority_referenceThe external authoritative system this relates to (e.g. NIMC) and its reference. Surfaced, not claimed as Keepable's.
external_referenceYour own id for this request, echoed back to you.
human_textThe human-readable explanation the recipient sees.

Send it

Sending seals the exact request: it computes a canonical hash, signs it with the Keepable seal key, stamps the validity window, and delivers it to the recipient's inbox. After this the request is immutable; any change is a new linked object, never an edit.

curl -X POST https://api.keepable.co/sender/tenants/ten_01HXP/consent-requests/cr_01HXP/send \
  -H "Authorization: Bearer $KEEPABLE_TOKEN" \
  -H "Keepable-Version: 2026-05-24" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "valid_for_seconds": 2592000 }'
200 OK
{ "request_id": "cr_01HXP", "status": "active", "version": 1, "sealed": true, "canonical_hash": "9f86d0818…", "expires_at": "2026-07-28T09:00:00Z" }

Omit valid_for_seconds to use the default validity window.

Get the decision

When the recipient grants or declines, consent.decided fires with the action in the payload. The decision is bound to the exact request version, so you know precisely what was agreed to.

co.keepable.consent.decided
{
  "type": "co.keepable.consent.decided",
  "data": {
    "tenant_id": "ten_01HXP",
    "request_id": "cr_01HXP",
    "status": "granted",
    "action": "grant",
    "occurred_at": "2026-06-28T10:15:00Z"
  }
}

You can also read the full request at any time. Once the recipient has decided, the response carries a nested decision object with the action and a branded receipt_code (KP-XXXX-…):

curl https://api.keepable.co/sender/tenants/ten_01HXP/consent-requests/cr_01HXP \
  -H "Authorization: Bearer $KEEPABLE_TOKEN" \
  -H "Keepable-Version: 2026-05-24"

Replace, expire, or withdraw

Once sent, a request never changes. To supersede or wind one down, deliver a linked acknowledgement that references the original by id without mutating it. A withdrawal also moves the request to withdrawn.

curl -X POST https://api.keepable.co/sender/tenants/ten_01HXP/consent-requests/cr_01HXP/acknowledge \
  -H "Authorization: Bearer $KEEPABLE_TOKEN" \
  -H "Keepable-Version: 2026-05-24" \
  -H "Idempotency-Key: $(uuidgen)" \
  -H "Content-Type: application/json" \
  -d '{ "kind": "withdrawal", "note": "No longer required" }'

kind is replacement (pass replacement_request_id to point at the new request), expiry, or withdrawal.

Export the evidence

GET …/consent-requests/{request_id}/receipt returns the verifiable evidence chain: the sealed request version and canonical hash, the recipient pseudonym, the returned decision, and any acknowledgements: the attributable record both you and the recipient can rely on.