Authentication
One organisation-scoped API key, sent as a bearer token. Its prefix chooses the data plane, and its scopes decide which operations it may call.
The Sender API authenticates with an API key: an opaque bearer credential
for server-to-server calls, with no user in the loop. There is no token
exchange and no client_id / client_secret; the key is the credential.
POST /sender/v2/correspondence HTTP/1.1
Host: api.keepable.co
Authorization: Bearer kp_live_aZ8mQqWRtY2I4eK7hPnLrXv...
Idempotency-Key: 9f1c8e2a-7b3d-4f10-9a2e-6c5b4d3e2f1a
Content-Type: application/jsonA key is scoped to your organisation. There is no tenant id in the path; the key identifies who you are.
The prefix is the environment
kp_sandbox_ keys act on the free synthetic plane. kp_live_ keys act on real
recipients and debit your prepaid wallet. Same host, same paths, same contract:
the only thing that changes is which key you send. See
Environments.
Getting a key
Keys are minted in the portal, not over this API. Minting is what creates access, so it is an act of a person with authority in your organisation rather than of a credential that already exists, and there is no bootstrap regress to solve.
Sandbox, in about a minute. Sign up with your organisation's name and a
work email, then mint a kp_sandbox_ key: no contract and no KYB. Start at
Sandbox.
Live, once approved. After your organisation accepts the
Sender Agreement, clears KYB, and
funds its wallet, mint a kp_live_ key from the dashboard. See
Going live.
Getting into the portal to do that is a passkey, a one-time email link, or your work account once an administrator has connected your work directory (see Work connections). There is no password to store, rotate, or leak, and none of those three is an API credential: a session signs a person in, a key signs a server in, and neither substitutes for the other.
What you can do over the API is manage the keys you already hold: list them, read their metadata, rotate a secret you think has leaked, and revoke one. Those are incident response, they belong in automation, and none of them widens anything.
POST https://api.keepable.co/sender/v2/api-keys/key_01J8ZQ4T/rotate
Authorization: Bearer {{KEEPABLE_TOKEN}}
Content-Type: application/json
{ "grace_seconds": 3600 }A secret is returned only when the key is created or rotated. Keepable stores a hash, never the secret. List responses redact it to the prefix. If you lose one, rotate rather than mint.
Confirm your wiring
GET /me reflects the caller the auth middleware derived from your key. It
touches no business state, so it is the cheapest way to check a bearer is
wired correctly.
GET https://api.keepable.co/sender/v2/me
Authorization: Bearer {{KEEPABLE_TOKEN}}{
"organization_id": "org_01J8ZQ4T",
"organization_name": "Anchor Microfinance Bank",
"plane": "live",
"tier": "standard",
"approved": true,
"scopes": ["correspondence.write", "responses.read", "webhooks.read"]
}Scopes
A key carries a set of scopes fixed at mint, and every operation names the one
it needs. A call whose key lacks it is refused with
403.
| Scope | Grants |
|---|---|
org.read | The organisation profile and its work connections. |
members.read | Members and routing groups: the desks a route can address. |
directory.read | Search the connected work directory, live. |
storage.read | Browse and read the connected work storage, live. |
documents.read | Documents already in Keepable. |
documents.write | Upload a document, or pull one from work storage. |
plans.read | Read plans. |
plans.write | Author, update, and archive plans. |
correspondence.read | Files, participants, minutes, covenants, exports. |
correspondence.write | Send, route, run a plan, attach, remind, close, withdraw. |
recipients.check | Ask whether one identifier is deliverable. |
responses.read | Collected answers, with sensitive values masked. |
responses.sensitive | Unmask what came back: an answer, an export, a document. |
responses.erase | Erase one participant's answers, for an erasure request. |
claims.read | Claim codes on a tokenized send. |
claims.write | Void and reissue claim codes. |
metrics.read | Delivery and workflow rollups. |
wallet.read | Prepaid balance and ledger. |
invoices.read | Invoices and their PDFs. |
webhooks.read | Endpoints and delivery history. |
webhooks.write | Create, change, test, and re-secret endpoints. |
audit.read | Your organisation's audit log. |
apikeys.read | Key metadata. |
apikeys.write | Rotate and revoke keys. |
Ask for the least each integration needs. Four of these repay a second look, because in each case two acts that look alike turned out to have different consequences:
responses.sensitiveis separate fromresponses.read. Reading an answer and unmasking it are different acts. A key minted to pull form answers into a CRM should not thereby be able to read every applicant's BVN.recipients.checkis separate fromcorrespondence.read. It is the cheap identity oracle here, and a send-only integration has no need of one.directory.readandstorage.readare separate frommembers.readanddocuments.read. Those are people and files already in Keepable; these reach live into your own directory and drive.webhooks.readis separate fromwebhooks.write. Watching deliveries should not carry the right to re-point an endpoint at another host.responses.eraseis granted by nothing else. Deleting what somebody answered is not a stronger form of reading it, so it is not implied byresponses.reador byresponses.sensitive.
Two things a scope is not:
A scope is not reader rights. A restricted correspondence is scoped to its participants and named readers. No key scope overrides that.
Some request shapes need a second scope. Collecting an export made with
include_sensitive, or retrieving a document that came back as a response,
needs responses.sensitive on top of the scope that reaches the operation at
all. The operation's own description says when.
mTLS for tier-1 senders
Tier-1 senders (the central bank, CBN-licensed banks, federal agencies) present a client certificate in addition to the key. It is verified at the gateway and layered on top, not instead: a tier-1 grade keeps sending disabled until the certificate is live, and there is no weaker interim path. Partner Engineering provisions it during onboarding.
Treat an API key like a password. It belongs in back-end services and server-side secret storage, never in a browser, a mobile app, or any distributed client. If one is exposed, rotate or revoke it immediately.
The model
Two primitives carry the whole API. A plan is a reusable definition of a piece of work; a correspondence is the record of one instance of it. Letters, agreements, approvals, consent drives, and forms are kinds of correspondence, not separate APIs.
Conventions
The headers, idempotency rules, and pagination model that are constant across every Keepable API. Learn them once.