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.

The public v2 contract is currently a pre-launch preview. There are no external production integrators yet, so its operation set may still be narrowed before general availability. The changelog will announce when the compatibility commitment begins. After that point, breaking changes use the normal major-version and sunset process.

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.

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