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
status | Meaning |
|---|---|
draft | Authored but not yet sent. Still editable; not visible to the recipient. |
active | Sealed and delivered. The clock to expires_at is running and the recipient can decide. |
granted / declined | The recipient's terminal decision, bound to the exact sealed request version. |
withdrawn | You delivered a withdrawal acknowledgement before a decision. |
expired | The 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();{ "request_id": "cr_01HXP", "status": "draft", "purpose": "Share your verified identity profile with Acme Bank", "created_at": "2026-06-28T09:00:00Z" }Fields
| Field | Meaning |
|---|---|
recipient | { "kind": "nin" | "email", "value": "…" }. Resolved to a registered recipient and stored as a keyed-HMAC pseudonym; never echoed back. |
purpose | Required. The one specific thing you are asking permission for. |
requested_action / data_categories | What you want to do, and which categories it touches. |
use_mode | one_time or recurring. Carried to the recipient and into the receipt; Keepable does not enforce downstream use. |
withdrawal_instructions | How the recipient can later withdraw, in your own words. |
authority_name / authority_reference | The external authoritative system this relates to (e.g. NIMC) and its reference. Surfaced, not claimed as Keepable's. |
external_reference | Your own id for this request, echoed back to you. |
human_text | The 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 }'{ "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.
{
"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.
Agreements
Run e-signature workflows. Send a document to one or more signers, track signatures as they land, and download a tamper-evident PAdES-LTV covenant when everyone has signed.
Forms
Publish reusable form templates that recipients fill in from their inbox, then read the responses back. Covers field types, template lifecycle, and collecting responses.