Keepable docs
Sender API

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:

statusMeaning
activeCreated and awaiting signatures. The clock to expires_at is running.
completedEvery signer has signed. The covenant is available.
revokedYou cancelled it before completion.
expiredThe 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();
201 Created
{ "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

roleMeaning
signerMust apply a signature for the agreement to complete.
delegateReceives 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"
200 OK
{
  "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"
200 OK
{ "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.