Keepable docs
Foundations

Authentication

The Sender API uses OAuth2 client-credentials. Exchange your client_id and client_secret for a short-lived bearer token, scoped to exactly the operations a credential needs.

The Sender API authenticates with OAuth2 client-credentials, a pure server-to-server flow with no user in the loop. You exchange a client_id and client_secret for a short-lived bearer token, then send that token on every request.

Get a token

POST your credentials to the token endpoint with grant_type=client_credentials. Request only the scopes the credential actually needs (see Scopes).

curl https://api.keepable.co/oauth2/token \
  -H "Content-Type: application/x-www-form-urlencoded" \
  -d "grant_type=client_credentials" \
  -d "client_id=$KEEPABLE_CLIENT_ID" \
  -d "client_secret=$KEEPABLE_CLIENT_SECRET" \
  -d "scope=content.read content.write"
const res = await fetch("https://api.keepable.co/oauth2/token", {
  method: "POST",
  headers: { "Content-Type": "application/x-www-form-urlencoded" },
  body: new URLSearchParams({
    grant_type: "client_credentials",
    client_id: process.env.KEEPABLE_CLIENT_ID!,
    client_secret: process.env.KEEPABLE_CLIENT_SECRET!,
    scope: "content.read content.write",
  }),
});

const { access_token, expires_in } = await res.json();
form := url.Values{
    "grant_type":    {"client_credentials"},
    "client_id":     {os.Getenv("KEEPABLE_CLIENT_ID")},
    "client_secret": {os.Getenv("KEEPABLE_CLIENT_SECRET")},
    "scope":         {"content.read content.write"},
}
resp, err := http.PostForm("https://api.keepable.co/oauth2/token", form)
if err != nil {
    return err
}
defer resp.Body.Close()

var tok struct {
    AccessToken string `json:"access_token"`
    ExpiresIn   int    `json:"expires_in"`
}
json.NewDecoder(resp.Body).Decode(&tok)

The response is a standard OAuth2 token document:

{
  "access_token": "...",
  "token_type": "Bearer",
  "expires_in": 3600
}

Use the token

Send it as a bearer token on every business request, alongside the Keepable-Version header:

POST /tenants/ten_01HXP/contents HTTP/1.1
Host: api.keepable.co
Authorization: Bearer <access_token>
Keepable-Version: 2026-05-24
Idempotency-Key: 9f1c8e2a-7b3d-4f10-9a2e-6c5b4d3e2f1a
Content-Type: application/json

Tokens are short-lived. Cache the token until shortly before expires_in elapses and refresh it then, rather than requesting a new token per call. A request with a missing, malformed, or expired token returns 401 Unauthorized.

Confirm your wiring with whoami

GET /auth/whoami reflects the caller the auth middleware derived from your token. It touches no business state, so it is the cheapest way to confirm an SDK has its bearer wired correctly:

curl https://api.keepable.co/auth/whoami \
  -H "Authorization: Bearer $KEEPABLE_TOKEN" \
  -H "Keepable-Version: 2026-05-24"
{ "kind": "sender", "id": "ten_acme", "scopes": ["tenant.read", "content.send"] }

Scopes

A credential is granted a set of scopes, and the token it mints can only carry scopes the credential holds. Request the least privilege each integration needs: a delivery worker needs content.write, not tenant.write.

ScopeGrants
tenant.readRead tenants.
tenant.writeCreate and modify tenants, company IDs, and branding.
content.readRecipient matching and content reads.
content.writeDeliver content.
agreement.readRead agreements and download covenants.
agreement.writeCreate and revoke agreements.
forms.readRead form templates and responses.
forms.writeCreate and delete form templates and responses.
access.writeRequest and respond to access delegation.
webhooks.writeManage webhook endpoints and read deliveries.

A call whose token lacks the required scope returns 403 Forbidden.

mTLS for tier-1 senders

Tier-1 senders (federal agencies, the central bank, and the top-five banks) may additionally be required to present a client certificate at the edge. mTLS is enforced at the gateway and layered on top of the OAuth2 bearer above; it does not replace it. If your organisation is in scope, Partner Engineering provisions the certificate during onboarding.

Never embed a client_secret in a browser, mobile app, or any distributed client. The client-credentials flow is for back-end services only.