API reference

Everything the product does, as an API.

Write a document, send it for legally binding signature, publish a tracked link behind whatever gates the deal needs, and read back who read which paragraph and for how long. One REST API, 135 endpoints, scoped keys, signed webhooks.

Quick start

Create a key under Settings → Developer, choosing the scopes it needs. It is shown once. Then check it, and write something.

Check the key
curl "https://xdocs.io/api/v1/me" \
  -H "Authorization: Bearer $XDOCS_API_KEY"
Create a document and send it for signature
# 1. Write it, with the people who will sign
curl -X POST "https://xdocs.io/api/v1/documents" \
  -H "Authorization: Bearer $XDOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Consulting agreement",
    "markdown": "# Consulting agreement\n\nThe parties agree…",
    "recipients": [{ "name": "Dana Reyes", "email": "dana@example.com" }]
  }'

# 2. Give each signer somewhere to sign. Markdown has no fields in it,
#    and an envelope refuses a recipient with nothing to complete.
#    (Starting from a template instead? It already has them — skip this.)
curl -X POST "https://xdocs.io/api/v1/documents/doc_…/signature-blocks" \
  -H "Authorization: Bearer $XDOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "heading": "Consultant" }'

# 3. Send it — recipients and fields come from the document
curl -X POST "https://xdocs.io/api/v1/envelopes" \
  -H "Authorization: Bearer $XDOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "documentId": "doc_…", "sequential": false }'
Publish a tracked link with gates
curl -X POST "https://xdocs.io/api/v1/links" \
  -H "Authorization: Bearer $XDOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "documentId": "doc_…",
    "name": "Acme — Series B",
    "requireEmailVerification": true,
    "requireNda": true,
    "watermark": true,
    "expiresAt": "2026-12-31T23:59:59Z"
  }'
Read back who read what
curl "https://xdocs.io/api/v1/views?documentId=doc_…&limit=20" \
  -H "Authorization: Bearer $XDOCS_API_KEY"

# Per-section attention for one document
curl "https://xdocs.io/api/v1/analytics/documents/doc_…" \
  -H "Authorization: Bearer $XDOCS_API_KEY"

How it behaves

Authentication

Authorization: Bearer xdk_… on every request. Only a hash of the key is stored, so a lost key is replaced rather than recovered. A key can carry an expiry, and can be revoked at any time.

Scopes

A key holds only what it needs. A reporting integration can be given one that cannot send anything; a CRM writeback can publish links without being able to read every visit. A call outside a key’s scopes answers 403 naming the scope it wanted.

Pagination

Lists take ?limit= (max 100) and ?cursor=, and return nextCursor. Cursors are keyset rather than offset, because a list of visits moves under you while you page through it — offsets skip and repeat, keysets do not. Treat the value as opaque.

Filters

A filter value that is not recognised answers 400, naming what was expected. A mistyped ?status= would otherwise come back as an empty page, and a ?active=yes as an unfiltered one — neither distinguishable from a true answer.

Rate limit

120 requests a minute per key, counted in the database so every instance sees the same total. Every reply carries x-ratelimit-limit, x-ratelimit-remaining and x-ratelimit-reset (Unix seconds); a 429 carries retry-after.

Shapes

One resource comes back as { data }, a list as { data, nextCursor }, a failure as { error }. Timestamps are ISO 8601 in UTC and durations are milliseconds. Deletes answer 204 with no body.

What is never returned

A signing token, a link password, an assignee’s upload token or a storage URL — each is a capability, and handing one to a caller would hand over the thing it protects. Files are streamed through the API instead, and a password is reported as hasPassword.

Scopes

documents:read
Read documents, folders, versions, comments and templates. Also the visits CSV export, and webhook subscriptions with their deliveries and sample payloads.
documents:write
Create and edit documents, folders and templates, upload and replace files, and manage webhook subscriptions.
envelopes:read
Read envelopes, executed PDFs, certificates and returned files.
envelopes:write
Send for signature, remind, void and reassign, and read signing links.
rooms:read
Read data rooms, their state, their folders and what is filed in them.
rooms:write
Create rooms, file documents, manage folders, and freeze or archive a room.
links:read
Read tracked links and the agreements library.
links:write
Publish and change links, manage agreements, answer viewer questions.
views:read
Read visits, visitors, analytics and viewer questions.
audit:read
Read the workspace audit trail.
requests:read
Read file requests and what has arrived.
requests:write
Create file requests and manage who is asked.
compliance:read
Read legal holds — matter names and reasons included — and retention rules.
compliance:write
Place and widen legal holds, and create, change and delete retention rules. Releasing a hold is done in the app.

When something goes wrong

400
The request could not be understood. A failed body carries details with the exact fields.
401
No key, or a key that is revoked, expired or not valid.
402
Payment needed: a send beyond the plan's monthly allowance that the credit balance will not cover. Nothing was charged.
403
A key outside its scopes. The message names the scope that was wanted.
404
No such row — or one belonging to somebody else, which answers the same way on purpose.
409
A conflict with the state: editing a document that is out for signature, voiding an envelope that has ended, changing a frozen room.
422
Understood, and refused: a plan ceiling, an unknown webhook event, a document whose signers have nothing to sign.
429
The rate limit. retry-after says how long to wait.
500
A failure of ours. The body carries a requestId, as every reply's x-request-id header does — quote it when you report the problem, and it finds the exact request in our logs.
503
Something we depend on — billing, file storage — did not answer. Nothing was done; safe to retry.

Endpoints

Base URL https://xdocs.io/api/v1. Every path below is relative to it. Each resource has its own page with every parameter, response and refusal in full.

Documents API41

Create documents from Markdown or HTML, upload PDFs, edit spreadsheet rows, columns and sheets, add signature blocks and export files with the Documents API.

Documents reference

Virtual data room API21

Create virtual data rooms by API: file documents into folders, set viewer groups with per-file permissions, freeze it at closing and share it by link.

Data rooms reference

Webhooks

Rather than polling, subscribe a URL and hear about things as they happen. Each delivery is a POST of { id, event, createdAt, data } carrying x-xdocs-event, x-xdocs-event-id, x-xdocs-timestamp and x-xdocs-signature, a hex HMAC-SHA256 of {timestamp}.{body} using the secret returned when you subscribe. Verify the raw body before parsing it. Answer with any 2xx within eight seconds. A delivery is tried up to three times with a short backoff — a 4xx other than 429 is taken as a refusal and not retried — and an endpoint that fails 10 deliveries in a row is switched off with its log kept.

Subscribe
curl -X POST "https://xdocs.io/api/v1/hooks" \
  -H "Authorization: Bearer $XDOCS_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/hooks/xdocs",
    "events": ["link.viewed", "envelope.completed"]
  }'
Verify a delivery (Node)
import { createHmac, timingSafeEqual } from "node:crypto";

const timestamp = req.headers["x-xdocs-timestamp"];
const signature = req.headers["x-xdocs-signature"];
const expected = createHmac("sha256", process.env.XDOCS_WEBHOOK_SECRET)
  .update(`${timestamp}.${rawBody}`)
  .digest("hex");

const ok =
  signature.length === expected.length &&
  timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

Events

envelope.sent
A document went out for signature.
recipient.notified
A signing email left for a recipient.
recipient.bounced
A signing email could not be delivered.
recipient.viewed
A recipient opened their signing page.
recipient.authenticated
A recipient passed the access code.
recipient.signed
A recipient completed their fields.
recipient.declined
A recipient declined to sign, with a reason.
recipient.delegated
A recipient reassigned signing to someone else.
recipient.attached
A recipient returned a file.
envelope.reminded
Reminders went to whoever is holding things up.
envelope.completed
Every signer has signed; the PDF is final.
envelope.voided
The sender cancelled the envelope.
envelope.expired
The envelope passed its expiry unsigned.
consent.accepted
A signer agreed to sign electronically.
consent.withdrawn
A signer withdrew that consent.
document.created
A new document was created.
document.edited
A document's body changed.
document.redacted
Text was permanently removed from a document.
document.trash_refused
Moving a document to the trash, or deleting it, was refused because it is under a legal hold.
document.retention_trashed
A retention rule moved a document to the trash.
legal_hold.placed
A legal hold was placed.
legal_hold.changed
A legal hold was widened to cover more.
legal_hold.released
A legal hold was released.
retention_rule.changed
A retention rule was created, changed or deleted.
link.created
A tracked link was published.
link.updated
A link's gates or settings changed.
link.enabled
A link was switched back on.
link.disabled
A link was switched off.
link.deleted
A link was removed.
link.viewed
A visitor cleared every gate and opened the link.
link.nda_accepted
A visitor accepted the link's confidentiality terms.
link.downloaded
A visitor downloaded a PDF copy.
link.bulk_downloaded
A visitor downloaded a room or folder as a zip.
link.access_requested
Somebody turned away by a link, or by a document they are not on, asked for access.
link.access_approved
An access request was approved and the person let in.
link.access_denied
An access request was denied.
ping
A test delivery sent from the settings page.

For AI agents

Give a model the whole API

All 135 endpoints, the authentication, the pagination and the rules that are not expressible in a schema — as one block of text to paste, a URL to fetch, a spec to generate a client from, or a tool server your agent can call directly.

The complete reference, as text

Roughly what a model needs and nothing it does not: the base URL, the header, the scopes, the pagination, and every endpoint with its parameters. Paste it into a system prompt or a context file.

Loading the reference…

There is also a command line — npx xdocs documents list — and a Zapier app, both thin clients over exactly these endpoints. Anything they can do, curl can do.

Build it against the same API the product runs on.