# Node SDK

> The @exalto/proof package for calling the Proof API from your server, with typed methods for every endpoint.

`@exalto/proof` wraps the [REST API](/reference/api/overview/) 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

```bash
npm install @exalto/proof
```

It 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

```ts title="proof.ts"

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()`](#platform) 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. |

```ts
const proof = new Proof(process.env.PROOF_SECRET_KEY, { maxRetries: 4 });
```

## 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.

```ts
await proof.documents.update("doc_8Hq2Lm4R", { link_access: "suggest" });
```

## Methods

Each resource is a property of the client. Path parameters come first, then the body or query as one object.

### Users

See [Users](/reference/api/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

See [Documents](/reference/api/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

See [Members](/reference/api/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

See [Sessions](/reference/api/sessions/).

| Method | Endpoint |
| --- | --- |
| `proof.sessions.create({ user, document, via?, group? })` | `POST /v1/sessions` |
| `proof.sessions.create({ user, purpose: "presence", group? })` | `POST /v1/sessions` |

### Agents

See [Agents](/reference/api/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

See [Agent requests](/reference/api/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

See [Reader requests](/reference/api/seals/#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` |

### Seals

See [Seals](/reference/api/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

See [Webhook endpoints](/reference/api/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

See [Platform](/reference/api/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

Methods that list things return a page you can either await or iterate. Awaiting it gives you one page, as the API returns it:

```ts
const page = await proof.users.listDocuments("usr_5Kq2Lm8R", { limit: 20 });
page.data; // up to 20 documents, newest edited first
page.next; // a cursor, or null on the last page
```

Iterating it with `for await` fetches the following pages as you go, so you see every item once:

```ts
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

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](/reference/api/overview/). |
| `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. |

```ts

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

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](/reference/api/overview/#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

`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:

```ts title="webhooks.ts"

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](/reference/webhooks/) lists every event and its payload.

Calls to [your own agent tools](/guides/your-own-agent-tools/) are signed the same way. `verifySignature(rawBody, header, secret)` checks one and says whether it matched:

```ts

if (!verifySignature(rawBody, req.get("Proof-Signature"), process.env.ACME_TOOLS_SECRET!)) return res.sendStatus(401);
```

## TypeScript

The package ships its own types. Each object the API returns has a type of the same name:

```ts
```

`Event<T>` is the webhook envelope, with `data` typed as `T`; pass the payload type of the events you handle:

```ts
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);
```