Core concepts

Pagination

List endpoints return a fixed envelope and page with opaque cursors rather than offsets, so a page never shifts under you as new documents arrive.

The list envelope

json
{
  "object": "list",
  "data": [
    { "object": "document", "id": "…", "title": "Framing Agreement", "…": "…" }
  ],
  "has_more": true,
  "next_cursor": "ZG9jXzAxSk…"
}
FieldWhat it is
objectAlways the string list.
dataThe items, newest first.
has_moreWhether another page exists after this one. This is the value to loop on.
next_cursorPass as starting_after to fetch the next page. It is null whenever has_more is false.

Parameters

ParameterDefaultNotes
limit20How many items to return, 1–100. Anything outside that range is rejected rather than clamped.
starting_afterA cursor from a previous response. Returns the items after it.
ending_beforeA cursor from a previous response. Returns the items before it.

Sending both starting_after and ending_before is an error (conflicting_cursors) rather than one silently winning.

Cursors are opaque

A cursor is a token we issue. Do not decode it, construct one, derive it from a document id, or store it as a permanent bookmark — its format is not part of the contract and anything we did not issue is rejected with invalid_cursor. Pass back exactly the string you were given.

Iterating a whole collection

javascript
async function* allDocuments(apiKey) {
  let cursor = null;

  do {
    const url = new URL("https://xosign.ai/api/v1/documents");
    url.searchParams.set("limit", "100");
    if (cursor) url.searchParams.set("starting_after", cursor);

    const res = await fetch(url, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (!res.ok) throw new Error(`${res.status} ${await res.text()}`);

    const page = await res.json();
    yield* page.data;

    // has_more is the loop condition. next_cursor is null once it is false,
    // so trusting the cursor alone would end the loop one page early.
    cursor = page.has_more ? page.next_cursor : null;
  } while (cursor);
}
  • Loop on has_more, not on whether data is non-empty.
  • Use limit=100 for bulk reads — it is the maximum, and it is four times fewer round trips than the default.
  • Long walks can meet a 429. Combine this with the retry helper from rate limits.

Filtering documents by status

GET /documents also accepts a comma-separated status filter, which composes with pagination:

curl
curl "https://xosign.ai/api/v1/documents?status=sent,partially_signed&limit=100" \
  -H "Authorization: Bearer $XOSIGN_API_KEY"

Valid values are draft, sent, partially_signed, completed, voided, declined, expired, in_review. A single unrecognised value rejects the whole request with invalid_status rather than being ignored, so a typo cannot quietly return the wrong set.

Not every list is paginated

GET /documents/{id}/recipients returns the same envelope but never pages: a document has a bounded number of recipients, so has_more is always false and next_cursor always null. Reading the envelope the same way everywhere still works.

Pagination · XOsign API docs