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
{
"object": "list",
"data": [
{ "object": "document", "id": "…", "title": "Framing Agreement", "…": "…" }
],
"has_more": true,
"next_cursor": "ZG9jXzAxSk…"
}| Field | What it is |
|---|---|
object | Always the string list. |
data | The items, newest first. |
has_more | Whether another page exists after this one. This is the value to loop on. |
next_cursor | Pass as starting_after to fetch the next page. It is null whenever has_more is false. |
Parameters
| Parameter | Default | Notes |
|---|---|---|
limit | 20 | How many items to return, 1–100. Anything outside that range is rejected rather than clamped. |
starting_after | — | A cursor from a previous response. Returns the items after it. |
ending_before | — | A 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
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 whetherdatais non-empty. - Use
limit=100for 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 "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.