Node SDK
@exalto/proof wraps the REST API for Node.js servers. Every method maps to one endpoint, takes the same fields and returns the same objects, so the REST reference describes the SDK too. Use it only on your server: it holds your secret key.
Install
Section titled “Install”npm install @exalto/proofIt needs Node.js 20 or later and has no dependencies. Version 1.x of the package talks to /v1 of the API.
Create a client
Section titled “Create a client”import Proof from "@exalto/proof";
export const proof = new Proof(process.env.PROOF_SECRET_KEY);The key decides the mode: an sk_test_ key works on test data, an sk_live_ key on live data. proof.platform.retrieve() says which, as mode.
The second argument takes options:
| Option | Default | What it does |
|---|---|---|
maxRetries |
2 |
How many times a request answered with 429 or 5xx is tried again. |
baseUrl |
https://api.proof.exalto.ai/v1 |
Where requests go. Change it only for a proxy, or a Proof you run yourself. |
const proof = new Proof(process.env.PROOF_SECRET_KEY, { maxRetries: 4 });Field names
Section titled “Field names”The SDK uses the API’s own field names, in snake_case, in both directions. What you send to proof.documents.update is the body of PATCH /v1/documents/:id, and what comes back is the document object as the REST reference shows it.
await proof.documents.update("doc_8Hq2Lm4R", { link_access: "suggest" });Methods
Section titled “Methods”Each resource is a property of the client. Path parameters come first, then the body or query as one object.
See Users.
| Method | Endpoint |
|---|---|
proof.users.upsert(externalId, params) |
PUT /v1/users/external/:external_id |
proof.users.retrieve(userId) |
GET /v1/users/:user_id |
proof.users.retrieveByExternalId(externalId) |
GET /v1/users/external/:external_id |
proof.users.update(userId, params) |
PATCH /v1/users/:user_id |
proof.users.delete(userId) |
DELETE /v1/users/:user_id |
proof.users.merge(userId, { into }) |
POST /v1/users/:user_id/merge |
proof.users.listDocuments(userId, { limit?, ids? }) |
GET /v1/users/:user_id/documents |
proof.users.listTrash(userId) |
GET /v1/users/:user_id/documents?trashed=true |
proof.users.emptyTrash(userId) |
DELETE /v1/users/:user_id/trash |
proof.users.deleteFromTrash(userId, documentId) |
DELETE /v1/users/:user_id/trash/:document_id |
proof.users.listAgents(userId, { document? }) |
GET /v1/users/:user_id/agents |
proof.users.endSessions(userId, { group? }) |
DELETE /v1/users/:user_id/sessions |
proof.users.retrieveCheckins(userId) |
GET /v1/users/:user_id/checkins |
proof.users.updateCheckins(userId, { every_minutes }) |
PUT /v1/users/:user_id/checkins |
Documents
Section titled “Documents”See Documents.
| Method | Endpoint |
|---|---|
proof.documents.create(params) |
POST /v1/documents |
proof.documents.retrieve(documentId) |
GET /v1/documents/:id |
proof.documents.update(documentId, params) |
PATCH /v1/documents/:id |
proof.documents.trash(documentId, { user? }) |
POST /v1/documents/:id/trash |
proof.documents.restore(documentId, { user? }) |
POST /v1/documents/:id/restore |
proof.documents.delete(documentId) |
DELETE /v1/documents/:id |
proof.documents.export(documentId, { format }) |
GET /v1/documents/:id/export |
proof.documents.provenance(documentId) |
GET /v1/documents/:id/provenance |
export resolves to the Markdown as a string.
Members
Section titled “Members”See Members.
| Method | Endpoint |
|---|---|
proof.members.list(documentId) |
GET /v1/documents/:id/members |
proof.members.put(documentId, userId, { role }) |
PUT /v1/documents/:id/members/:user_id |
proof.members.delete(documentId, userId) |
DELETE /v1/documents/:id/members/:user_id |
Sessions
Section titled “Sessions”See Sessions.
| Method | Endpoint |
|---|---|
proof.sessions.create({ user, document, via?, group? }) |
POST /v1/sessions |
proof.sessions.create({ user, purpose: "presence", group? }) |
POST /v1/sessions |
Agents
Section titled “Agents”See Agents.
| Method | Endpoint |
|---|---|
proof.agents.retrieve(agentId) |
GET /v1/agents/:agent_id |
proof.agents.update(agentId, { access }) |
PATCH /v1/agents/:agent_id |
proof.agents.listDocuments(agentId) |
GET /v1/agents/:agent_id/documents |
proof.agents.putDocument(agentId, documentId, { role }) |
PUT /v1/agents/:agent_id/documents/:document_id |
proof.agents.deleteDocument(agentId, documentId) |
DELETE /v1/agents/:agent_id/documents/:document_id |
proof.agents.revoke(agentId) |
DELETE /v1/agents/:agent_id |
Agent requests
Section titled “Agent requests”See Agent requests.
| Method | Endpoint |
|---|---|
proof.agentRequests.retrieve(requestId) |
GET /v1/agent-requests/:id |
proof.agentRequests.approve(requestId, params) |
POST /v1/agent-requests/:id/approve |
proof.agentRequests.deny(requestId) |
POST /v1/agent-requests/:id/deny |
Reader requests
Section titled “Reader requests”See Reader requests.
| Method | Endpoint |
|---|---|
proof.readerRequests.retrieve(requestId) |
GET /v1/reader-requests/:id |
proof.readerRequests.approve(requestId, { user, email }) |
POST /v1/reader-requests/:id/approve |
See Seals.
| Method | Endpoint |
|---|---|
proof.seals.create(documentId, { user }) |
POST /v1/documents/:id/seals |
proof.seals.list(documentId) |
GET /v1/documents/:id/seals |
proof.seals.retrieve(code) |
GET /v1/seals/:code |
proof.seals.withdraw(code) |
DELETE /v1/seals/:code |
Webhook endpoints
Section titled “Webhook endpoints”See Webhook endpoints.
| Method | Endpoint |
|---|---|
proof.webhookEndpoints.create({ url, events }) |
POST /v1/webhook-endpoints |
proof.webhookEndpoints.list() |
GET /v1/webhook-endpoints |
proof.webhookEndpoints.rollSecret(endpointId) |
POST /v1/webhook-endpoints/:id/roll-secret |
proof.webhookEndpoints.delete(endpointId) |
DELETE /v1/webhook-endpoints/:id |
Platform
Section titled “Platform”See Platform.
| Method | Endpoint |
|---|---|
proof.platform.retrieve() |
GET /v1/platform |
proof.platform.update(params) |
PATCH /v1/platform |
proof.platform.setTools({ url, secret, tools, instructions? }) |
PUT /v1/platform/tools |
proof.platform.stats() |
GET /v1/platform/stats |
Lists and paging
Section titled “Lists and paging”Methods that list things return a page you can either await or iterate. Awaiting it gives you one page, as the API returns it:
const page = await proof.users.listDocuments("usr_5Kq2Lm8R", { limit: 20 });page.data; // up to 20 documents, newest edited firstpage.next; // a cursor, or null on the last pageIterating it with for await fetches the following pages as you go, so you see every item once:
for await (const doc of proof.users.listDocuments("usr_5Kq2Lm8R")) { console.log(doc.title);}Stop the loop with break whenever you have what you need; no further pages are fetched.
Errors
Section titled “Errors”A request the API refuses throws a ProofError. It carries the API’s error fields and the HTTP status:
| Property | Example | What it is |
|---|---|---|
type |
"permission" |
The kind of error, as listed in Errors. |
code |
"no_access" |
A specific reason you can branch on. |
message |
"usr_9Tb3Wc1X can't open doc_8Hq2Lm4R." |
A sentence for your logs, not for your users. |
param |
"role" |
The field at fault, for invalid_request errors. |
status |
403 |
The HTTP status. |
import Proof, { ProofError } from "@exalto/proof";
try { const session = await proof.sessions.create({ user: "usr_9Tb3Wc1X", document: "doc_8Hq2Lm4R" });} catch (error) { if (error instanceof ProofError && error.code === "no_access") { // Grace isn't a member, and the document's link access doesn't let Grace in. } else { throw error; }}Network failures throw the error fetch threw.
Retries
Section titled “Retries”The client retries a request that was rate limited (429), and one other than a POST that failed on our side (500 and above), maxRetries times. It waits longer before each try, and as long as Retry-After asks when the API sends it. A POST that failed may have been carried out, so it isn’t sent again: see Idempotency.
Every id the client puts in a path is encoded as one segment, and an id of . or .. throws a TypeError: an id that came from your users can’t name another endpoint.
Webhooks
Section titled “Webhooks”proof.webhooks.verify checks a delivery’s signature and returns the event, or throws an Error. Give it the body exactly as it arrived, before any JSON parsing:
import express from "express";import { proof } from "./proof";
const app = express();
app.post("/proof/webhooks", express.raw({ type: "application/json" }), (req, res) => { const event = proof.webhooks.verify(req.body, req.get("Proof-Signature") ?? "", process.env.PROOF_WEBHOOK_SECRET!); if (event.type === "mention.created") notifyMention(event.data); res.sendStatus(204);});It also refuses a delivery whose timestamp is more than five minutes from your clock. Webhook events lists every event and its payload.
Calls to your own agent tools are signed the same way. verifySignature(rawBody, header, secret) checks one and says whether it matched:
import { verifySignature } from "@exalto/proof";
if (!verifySignature(rawBody, req.get("Proof-Signature"), process.env.ACME_TOOLS_SECRET!)) return res.sendStatus(401);TypeScript
Section titled “TypeScript”The package ships its own types. Each object the API returns has a type of the same name:
import type { Agent, Document, Event, Member, Platform, Role, Seal, Session, User } from "@exalto/proof";Event<T> is the webhook envelope, with data typed as T; pass the payload type of the events you handle:
const event = proof.webhooks.verify<{ seal: Seal }>(rawBody, header, secret);if (event.type === "seal.created") await saveSeal(event.data.seal.code, event.data.seal.url);