Keepable
Foundations

Conventions

The headers, idempotency rules, and pagination model that are constant across every Keepable API. Learn them once.

A handful of conventions hold across the Keepable Sender API. Learn them once here and the per-endpoint guides stay short.

Base URL

The Sender API is served from:

https://api.keepable.co/sender/v2

Paths in this documentation are written relative to it.

API version

The v2 in that URL is the version. There is nothing else to set: no header, no date to keep in step with a release, nothing to get wrong.

Inside a major version the API only ever grows. We add operations, optional request fields and response fields. We do not remove or rename anything, make an optional field required, narrow a type, tighten a limit, or change the status code of an outcome you already handle. So an integration that works today keeps working, and you upgrade when you want something new, not because we shipped.

When a change cannot be made that way, /v3 opens next to /v2 and both serve. You move by changing one segment in a URL, at a time you choose, and /v2 keeps running for twelve months after /v3 is announced. Watch the changelog for that.

One thing we ask in return: handle values you have not seen before. New kinds of correspondence, new states and new document types arrive inside a version, so a client that treats an unfamiliar enum value as a fatal error will break on something we added for somebody else. Keep unknown values rather than rejecting them.

Idempotency

Most mutating operations accept an Idempotency-Key header, a client-generated string of 1-255 visible ASCII characters, unique per logical operation. Send one on any create you want to be safely retryable (content delivery requires it); a UUID is the natural choice:

Idempotency-Key: 9f1c8e2a-7b3d-4f10-9a2e-6c5b4d3e2f1a

This makes retries safe. If a request times out and you do not know whether it landed, replay it with the same key:

  • Same key, identical request fingerprint → you get the original result back, and the operation runs exactly once.
  • Same key, different body → the API rejects the replay with 409 Conflict, protecting you from accidentally double-charging the key for a different payload.

Generate one key per logical action and persist it before you make the call, so a crash-and-retry reuses the same key. Generating a fresh key on every attempt defeats the protection: each attempt looks like a new operation.

Pagination

List endpoints return a page plus an opaque cursor, and take two query parameters:

ParameterMeaning
limitPage size, 1-200. Defaults to 50.
cursorOpaque cursor from the previous response's next_cursor.

Every list response carries a next_cursor. When it is null, you have reached the end.

# First page
GET https://api.keepable.co/sender/v2/correspondence?limit=50
Authorization: Bearer {{KEEPABLE_TOKEN}}

###

# Next page: pass the previous next_cursor as `cursor`
GET https://api.keepable.co/sender/v2/correspondence?limit=50&cursor=eyJvZmZzZXQiOjUwfQ
Authorization: Bearer {{KEEPABLE_TOKEN}}

Draining the whole list is the same two calls in a loop:

async function* allCorrespondence(token: string) {
  let cursor: string | undefined;
  do {
    const url = new URL("https://api.keepable.co/sender/v2/correspondence");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("cursor", cursor);

    const res = await fetch(url, {
      headers: {
        Authorization: `Bearer ${token}`,
      },
    });
    const page = await res.json();
    yield* page.correspondence;
    cursor = page.next_cursor ?? undefined;
  } while (cursor);
}

A cursor is opaque and short-lived. Do not parse one, store one, or build a permalink from one: it encodes a position in a result set that is still moving.

Rate limits

Responses carry RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset. Read them rather than discovering the ceiling by hitting it, and back off on 429 using RateLimit-Reset rather than a fixed sleep.

POST /recipient-checks is throttled per identifier on top of the ordinary limits, which is what keeps verification costs sane. It is a check, not an enumeration tool. See Recipients.

Errors

Every non-2xx response is an RFC 7807 application/problem+json document. The full catalogue (every problem type, what triggers it, and whether to retry) is in Errors.

On this page