Overview
The Proof REST API is how your server tells Proof who your users are, which documents exist and who may open them, and how it reads back what happened: provenance, seals and agents. Browsers never call it. They get a short-lived session token from your server and talk to Proof through the embedded editor.
Base URL
Section titled “Base URL”https://api.proof.exalto.ai/v1Every request goes over HTTPS. Plain HTTP requests are refused, not redirected.
Authentication
Section titled “Authentication”Send your secret key as a bearer token on every request:
curl https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R \ -H "Authorization: Bearer $PROOF_SECRET_KEY"In Node, the SDK sends it for you:
import Proof from "@exalto/proof";
const proof = new Proof(process.env.PROOF_SECRET_KEY);Secret keys start with sk_test_ or sk_live_. Find them, and roll them, in the console. Each key also has an id (key_…) that is safe to log; seals record the id of the key that started each writing session, so a leaked key can be traced to the sessions it started. See Platforms and keys.
A request without a key, or with an unknown or revoked one, gets 401 with an error of type authentication.
Test and live data
Section titled “Test and live data”Test keys and live keys see separate data. A document created with a test key doesn’t exist for a live key, and the other way round. Test seals have codes that start with T, and their pages at proof.exalto.ai say “Test seal”. Use test keys in development and CI; nothing you do with them is billed.
Requests and responses
Section titled “Requests and responses”- Request and response bodies are JSON (
Content-Type: application/json), except exports. - Field names are
snake_case. - Timestamps are ISO 8601 in UTC, such as
2026-10-30T18:20:00Z. - Fields that have no value are present and
null, not left out. - Every response carries a
Proof-Request-Idheader. Include it when you contact us about a request.
New fields can appear in any response at any time (see Versioning), so read the fields you need and ignore the rest.
Everything Proof creates has an id with a prefix that says what it is, users included. Your own id for a user is kept on the user as external_id, and appears only there: everywhere else, in requests, responses, events and seals, users are named by Proof id. Store the Proof id beside your user, or look it up with GET /v1/users/external/{external_id}.
| Prefix | Object |
|---|---|
usr_ |
User |
doc_ |
Document |
est_ |
Session token |
agt_ |
Agent |
areq_ |
Agent request |
srq_ |
Reader request |
we_ |
Webhook endpoint |
whsec_ |
Webhook signing secret |
evt_ |
Webhook event |
key_ |
Secret key id |
Seals are identified by their code: nine capital letters and digits, such as AH271NSHS.
Ids are opaque: don’t parse them or rely on their length.
Errors
Section titled “Errors”Proof uses HTTP status codes, and every error response has the same body:
{ "error": { "type": "invalid_request", "code": "invalid_parameter", "param": "role", "message": "role must be one of owner, editor, suggester, viewer." }}type tells you what kind of problem it is, code names the specific one, param names the field at fault when there is one, and message is for people reading logs, not for your code to match on.
| Type | Status | Meaning |
|---|---|---|
invalid_request |
400 | The request is malformed or a parameter is missing or wrong. |
authentication |
401 | No key, or the key is unknown or revoked. |
permission |
403 | The key is fine, but the action isn’t allowed, such as a session for someone without access. |
not_found |
404 | The object doesn’t exist in this mode (test or live). |
conflict |
409 | The object’s state doesn’t allow it, such as sealing a document in the trash. |
rate_limited |
429 | Too many requests. Wait for Retry-After seconds. Reserved: the API doesn’t limit requests yet. |
server |
500 | Something went wrong on our side. Retry. |
Codes you’ll see most often:
| Code | Type | When |
|---|---|---|
missing_parameter |
invalid_request |
A required field is absent. |
invalid_parameter |
invalid_request |
A field has the wrong type or value. |
invalid_body |
invalid_request |
The body isn’t a JSON object. |
missing_key |
authentication |
No Authorization header. |
invalid_key |
authentication |
The key isn’t one of yours, or has been revoked. |
no_access |
permission |
The user can’t open the document. |
not_owner |
permission |
Only one of the document’s owners can do this. |
user_not_found |
not_found |
No user with that id. Create it with PUT /v1/users/external/{external_id}. |
document_not_found |
not_found |
No document with that id, or it was deleted. |
document_trashed |
conflict |
The document is in the trash. Restore it first. |
Each resource page lists the codes specific to its endpoints.
Pagination
Section titled “Pagination”Endpoints that list things return a page:
{ "data": [ ... ], "next": "c_2Fh8Kq"}Pass limit (1 to 100, default 50) to choose the page size and after with the previous page’s next to get the following page. next is null on the last page. Each endpoint says what order its list is in.
curl "https://api.proof.exalto.ai/v1/users/usr_5Kq2Lm8R/documents?limit=20&after=c_2Fh8Kq" \ -H "Authorization: Bearer $PROOF_SECRET_KEY"In Node, await a list method for one page, or loop over it with for await and the SDK follows next for you:
for await (const doc of proof.users.listDocuments("usr_5Kq2Lm8R")) { console.log(doc.title);}Idempotency
Section titled “Idempotency”Proof doesn’t take idempotency keys yet: a POST sent twice is carried out twice, and the Node SDK doesn’t retry one that failed. Retry a POST that failed without an answer only where doing it twice is harmless, such as starting a session, or check first, for example by listing a document’s seals before sealing again. PUT and DELETE are idempotent already.
Versioning
Section titled “Versioning”The version is in the path: /v1. Within /v1, changes are only additive:
- new endpoints, and new optional parameters on existing ones
- new fields in responses
- new error codes and new webhook event types
- new values for fields that are documented as open-ended, such as a seal’s
method
Anything that would break working code ships as /v2, and /v1 keeps working for at least 12 months after /v2 is released. The embed script follows the same rule: v1.js changes only in compatible ways. The changelog lists every change.
Rate limits
Section titled “Rate limits”The API doesn’t limit requests yet. When it does, a request over the limit will get 429 rate_limited with a Retry-After header saying how many seconds to wait, which the Node SDK already honours, retrying twice by default.
Writing in the editor isn’t rate limited per request: people’s edits always go first, and each agent request gets a short slice of work at a time per document, so a burst of agent edits never holds up someone’s typing.
Resources
Section titled “Resources”| Resource | What it’s for |
|---|---|
| Users | Tell Proof who your users are, merge and delete them, and set their check-ins. |
| Documents | Create documents, set link access, move them to the trash, export them and read their provenance. |
| Members | Decide who can open a document and with which role. |
| Sessions | Start the short-lived tokens the embedded editor runs on. |
| Agents | List, limit and revoke the agents your users connected. |
| Agent requests | Approve or deny an agent from your consent page. |
| Seals | Seal documents, look seals up, and let in the readers of private ones. |
| Webhook endpoints | Choose where Proof sends events. |
| Platform | Your consent, reader and document URLs, embed origins, and your own agent tools. |