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.
When a recipient needs to sign something (an employment contract, a loan agreement, a consent form), you use an agreement rather than plain content. An agreement wraps a document, a set of participants, and the signatures collected against it, and produces a tamper-evident covenant PDF when complete.
Agreements need agreement.write to create and revoke, agreement.read to read
and download.
Lifecycle
An agreement moves through four states:
status | Meaning |
|---|---|
active | Created and awaiting signatures. The clock to expires_at is running. |
completed | Every signer has signed. The covenant is available. |
revoked | You cancelled it before completion. |
expired | The expires_at deadline passed before all signers signed. |
Each transition fires a webhook:
agreement.created, agreement.signed (once per signature), agreement.completed,
agreement.revoked, agreement.expired, so you can drive your own workflow off
the events instead of polling.
Create an agreement
Post the document and the participants. The document is a single
content part (base64). Each
participant needs at minimum an email and a role:
curl https://api.keepable.co/tenants/ten_01HXP/agreements \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"subject": "Employment contract",
"document": {
"name": "contract.pdf",
"content_type": "application/pdf",
"data": "JVBERi0xLjcK..."
},
"participants": [
{ "nin": "12345678901", "name": "Ada Eze", "email": "ada@example.ng", "role": "signer" }
]
}'const res = await fetch(
"https://api.keepable.co/tenants/ten_01HXP/agreements",
{
method: "POST",
headers: {
Authorization: `Bearer ${token}`,
"Keepable-Version": "2026-05-24",
"Idempotency-Key": crypto.randomUUID(),
"Content-Type": "application/json",
},
body: JSON.stringify({
subject: "Employment contract",
document: {
name: "contract.pdf",
content_type: "application/pdf",
data: contractBase64,
},
participants: [
{ nin: "12345678901", name: "Ada Eze", email: "ada@example.ng", role: "signer" },
],
}),
},
);
const { agreement_id, status, expires_at } = await res.json();{ "agreement_id": "agr_01HXP", "status": "active", "expires_at": "2026-06-23T09:00:00Z" }The create response is a lazy view: just the id, status, and expiry. Fetch the full agreement (below) to see participants and signatures.
Participant roles
role | Meaning |
|---|---|
signer | Must apply a signature for the agreement to complete. |
delegate | Receives and can act on the agreement on someone's behalf, without being a required signatory. |
email and role are required per participant; nin and name are optional
but recommended: a NIN ties the signature to a verified identity.
Track signatures
Fetch an agreement to see who has signed. The full view includes the
participants and a signatures array that grows as people sign:
curl https://api.keepable.co/tenants/ten_01HXP/agreements/agr_01HXP \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24"{
"agreement_id": "agr_01HXP",
"status": "completed",
"participants": [
{ "nin": "12345678901", "name": "Ada Eze", "email": "ada@example.ng", "role": "signer" }
],
"signatures": [
{
"nin": "12345678901",
"full_name": "Ada Eze",
"signed_at": "2026-05-25T10:00:00Z",
"signature_id": "sig_01HXP"
}
]
}In practice you would not poll this: you would listen for agreement.signed and
agreement.completed webhooks and fetch the full agreement only when an event
tells you something changed.
You can also list every agreement for a tenant with pagination:
curl "https://api.keepable.co/tenants/ten_01HXP/agreements?limit=50" \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24"Download the covenant
Once an agreement is completed, download its covenant, the signed document
as a PAdES-LTV PDF. PAdES-LTV ("Long-Term Validation") embeds the signature
validation material in the PDF itself, so the signature stays verifiable years
later without contacting Keepable.
curl https://api.keepable.co/tenants/ten_01HXP/agreements/agr_01HXP/covenant \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24"{ "name": "covenant.pdf", "data": "JVBERi0xLjcK...", "sha256": "9f86d0818..." }Verify the sha256. Decode data from base64 and check its SHA-256
against the sha256 field before you archive or display the covenant. It is a
cheap guard that the bytes arrived intact.
Revoke
Cancel an active agreement before it completes, for example if the terms
changed. Revocation is terminal:
curl -X POST https://api.keepable.co/tenants/ten_01HXP/agreements/agr_01HXP/revoke \
-H "Authorization: Bearer $KEEPABLE_TOKEN" \
-H "Keepable-Version: 2026-05-24" \
-H "Idempotency-Key: $(uuidgen)"A successful revoke returns 204 No Content and fires agreement.revoked. You
cannot revoke an agreement that has already completed or expired.
Content types in depth
The content types the send endpoint accepts, their stable vs preview status, and the structured attributes each carries. Grounded in the Nigerian market.
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.