# API requests: pagination and idempotency

> The rules every SignWith API endpoint shares: where requests go, what they look like, how to page through lists, and how to retry without doing anything twice.

Source: https://signwith.co/docs/api/requests · Updated 2026-10-02

## Base URL and format

All requests go to `https://app.signwith.co/api/v1`. They accept and return JSON (`Content-Type: application/json`) in UTF-8, except two uploads that also accept `multipart/form-data`: creating a template and verifying a document.

- Timestamps are ISO 8601 strings, such as `2026-09-27T09:00:01Z`.
- IDs are integers, except template field IDs, which are UUID strings, and checkout IDs, which are UUIDs.
- Where an endpoint takes `PATCH`, `PUT` is accepted as an alias.
- A path that doesn't exist returns `404 not_found` with the method and path in the message.

Every request needs an [API key](https://signwith.co/docs/api/authentication).

## Pagination

List endpoints return the newest items first, a page at a time, in this envelope:

```json
{ "object": "list", "data": [ ... ], "has_more": true, "next_cursor": 4812 }
```

- `limit` sets the page size, from 1 to 100 (default 20). Values below 1 fall back to 20, and values above 100 to 100.
- `cursor` gets the next page: pass the `next_cursor` from the page before.
- Keep going while `has_more` is `true`. On the last page `next_cursor` is `null`.

This fetches every signature request, 100 at a time:

```js
const items = []
let cursor = null
do {
  const url = new URL('https://app.signwith.co/api/v1/signature_requests')
  url.searchParams.set('limit', '100')
  if (cursor) url.searchParams.set('cursor', String(cursor))
  const response = await fetch(url, {
    headers: { Authorization: `Bearer ${process.env.SIGNWITH_API_KEY}` },
  })
  const page = await response.json()
  items.push(...page.data)
  cursor = page.has_more ? page.next_cursor : null
} while (cursor)
```

```python
import os

import requests

items, cursor = [], None
while True:
    params = {"limit": 100}
    if cursor:
        params["cursor"] = cursor
    page = requests.get(
        "https://app.signwith.co/api/v1/signature_requests",
        headers={"Authorization": f"Bearer {os.environ['SIGNWITH_API_KEY']}"},
        params=params,
    ).json()
    items.extend(page["data"])
    if not page["has_more"]:
        break
    cursor = page["next_cursor"]
```

The list endpoints are [`GET /templates`](https://signwith.co/docs/api/templates#list-templates), [`GET /signature_requests`](https://signwith.co/docs/api/signature-requests#list-signature-requests) and [`GET /credit_packs`](https://signwith.co/docs/api/billing#list-credit-packs), which always returns every pack on one page.

## Idempotency

Networks fail. To retry a `POST` without doing it twice, for example sending the same document to the same people again, send a unique `Idempotency-Key` header. A UUID v4 works well:

```http
Idempotency-Key: 5f1c6c1e-0b1a-4d38-9d0e-8a7a1f2f8d11
```

- The first successful (2xx) response is stored for **24 hours** per API key and path. A retry with the same key gets that stored response back, with the header `Idempotent-Replayed: true`.
- Failed responses aren't stored, so you can retry a failed request with the same key.
- If the first request is still running, a retry returns `409 idempotency_key_in_use`. Wait a moment and try again.
- Reusing a key with a different request body returns `422 idempotency_key_reused`. Use a new key for each different request.

Every `POST` endpoint accepts the header; the [API reference](https://signwith.co/docs/api) marks them. The code samples in these docs generate a new key for each run.

## Retrying safely

| Response | Retry? |
| --- | --- |
| `429 rate_limited` | Yes, after the `Retry-After` seconds when the header is present. See [rate limits](https://signwith.co/docs/api/rate-limits). |
| `409 idempotency_key_in_use` | Yes, with the same key, after a short wait. |
| `500 internal_error` | Yes, with the same `Idempotency-Key` for a `POST`. Contact support if it keeps happening. |
| `502 payment_provider_error` | Later. Nothing was charged. |
| Other `4xx` | No. Fix the request first; the `error.code` says what's wrong. |
