Conventions
A few rules apply to the whole API. Knowing them up front saves debugging later.
Requests and responses
- Base URL:
https://api.readyink.ai/v1. HTTPS only. - Send JSON bodies with
Content-Type: application/json. Upload PDFs withmultipart/form-data, or as raw bytes withContent-Type: application/pdfon the upload endpoint. - Responses are JSON. Each object has an
objectfield that names its type, such as"document","recipient","field","quote","event"or"list". - Most changes to a document return the whole updated document, so you always have its current state.
IDs
IDs are strings with a prefix that shows what they point to. Treat them as opaque and don't parse them.
| Prefix | Object |
|---|---|
doc_ |
Document |
rcp_ |
Recipient |
fld_ |
Field |
Timestamps
Times are ISO 8601 strings in UTC, like 2026-11-09T15:04:05Z. Fields that haven't happened yet, such as completed_at on a document that's still out, are null.
Pagination
GET /documents returns the newest documents first, 20 at a time by default. Ask for up to 100 with limit. When has_more is true, pass next_cursor as starting_after to get the next page.
curl "https://api.readyink.ai/v1/documents?limit=100&status=completed" \
-H "Authorization: Bearer $READYINK_API_KEY"
# next page
curl "https://api.readyink.ai/v1/documents?limit=100&status=completed&starting_after=doc_b81kz2" \
-H "Authorization: Bearer $READYINK_API_KEY" async function* allDocuments(params = {}) {
let cursor;
do {
const q = new URLSearchParams({ limit: "100", ...params, ...(cursor && { starting_after: cursor }) });
const page = await (await fetch(`${BASE}/documents?${q}`, { headers })).json();
yield* page.data;
cursor = page.has_more ? page.next_cursor : null;
} while (cursor);
}
for await (const d of allDocuments({ status: "completed" })) console.log(d.id, d.title);def all_documents(**params):
cursor = None
while True:
q = {"limit": 100, **params}
if cursor:
q["starting_after"] = cursor
page = requests.get(f"{BASE}/documents", headers=H, params=q).json()
yield from page["data"]
if not page["has_more"]:
break
cursor = page["next_cursor"]
for d in all_documents(status="completed"):
print(d["id"], d["title"])You can also filter by status and by created_after (an ISO 8601 time). An invalid limit returns 422 invalid_limit, and an invalid date returns 422 invalid_date.
Idempotency keys
Sending and cancelling need an Idempotency-Key header, so a network retry never sends or charges twice.
- Use any unique string up to 200 characters. A UUID works well.
- Use a new key for each new action, and the same key when you retry that action.
- A retry with the same key and the same request returns the first response, with the header
Idempotent-Replayed: true. - The same key with a different request returns
409 idempotency_conflict. - Keys are remembered per API key for 24 hours.
- A missing key returns
400 idempotency_key_required; a key over 200 characters returns400 idempotency_key_too_long.
All-or-nothing requests
Creating a document with recipients, and adding many fields at once, either fully succeed or change nothing. If one recipient or field is invalid, you get an error and nothing is created, so you can fix the input and try again without cleaning up.
Rate limits
Each key can make 60 requests a minute, and up to 10 of them can be sends. Over the limit, you get 429 rate_limited with a Retry-After header that says how many seconds to wait. Wait the number of seconds in Retry-After before trying again, and add some randomness to retries when you run many workers.
Limits at a glance
| Limit | Value |
|---|---|
| PDF size | 25 MB |
| PDF uploads per document | 1 |
| Recipients when creating a document | 50 |
| Fields per request | 200 |
| Documents per page | 1–100 (default 20) |
| Document title | 1–200 characters |
| Requests | 60 a minute per key |
| Sends | 10 a minute per key |
| Idempotency key | 200 characters, remembered 24 hours |
| Download links | Valid for 10 minutes |
