# Seals

> Seal a document, list and look up its seals, and withdraw one.

A seal is a snapshot of a document as it stood, with the record of how it was written and the numbers that come from it: where its words came from, whether a person was present, how many prompts and sittings it took. Each seal has a short page at `proof.exalto.ai` that anyone with the link can open, unless its owners [made its seals private](#who-reads-seals), and a seal line people paste wherever the text goes. A seal never changes once it's made; sealing again adds a new version. See [Seals](/concepts/seals/).

People can also seal from the Seal button in the editor. Either way, Proof sends `seal.created` to your [webhook endpoints](/reference/webhooks/).

## The seal object

| Attribute | Type | Description |
| --- | --- | --- |
| `code` | string | Nine capital letters and digits. Test seals start with `T`. |
| `url` | string | The seal's page: `https://proof.exalto.ai/` and the code. |
| `version` | integer | 1 for the document's first seal, then 2, 3 and so on. |
| `sealed_at` | timestamp | When it was sealed. |
| `sealed_by` | string | The Proof id of the owner who sealed it. |
| `document` | string or null | The document's id, or `null` once the document was deleted. |
| `method` | string or null | The version of the method its numbers were worked out with. They never change; under a newer method, the seal page shows a labeled reassessment beside them. This and the numbers below are `null` once the seal is withdrawn. |
| `line` | string or null | The seal line, ready to paste. |
| `composition` | object | Percentages of the document by where its words came from, summing to 100: `typed`, `shaped`, `ai` and `external`. |
| `presence` | object | `share`: percent of session time with a person present. `verified_share`: the part vouched for by check-ins. `verified`: `true` when every sitting people wrote in was verified. |
| `prompts` | integer | Rounds of back-and-forth with agents. |
| `sittings` | integer | Separate stretches of work. The seal line calls them sessions. |
| `duration` | string | Session time across those sittings, as the seal line writes it, such as `1hr48min`. |
| `attested_by` | object | Who vouches for the people named in it. `platform`: your platform's slug. `key_ids`: the ids of the secret keys that started its writing sessions. |
| `withdrawn_at` | timestamp or null | When it was withdrawn, or `null`. |

```json
{
  "code": "AH271NSHS",
  "url": "https://proof.exalto.ai/AH271NSHS",
  "document": "doc_8Hq2Lm4R",
  "version": 1,
  "sealed_at": "2026-10-13T23:31:04Z",
  "sealed_by": "usr_5Kq2Lm8R",
  "method": "v0.2",
  "line": "proof.exalto.ai/AH271NSHS · 87% typed/shaped · 8% AI · 5% external · 4 prompts · 2 sessions, 1hr48min",
  "composition": { "typed": 71, "shaped": 16, "ai": 8, "external": 5 },
  "presence": { "share": 97, "verified_share": 0, "verified": false },
  "prompts": 4,
  "sittings": 2,
  "duration": "1hr48min",
  "attested_by": { "platform": "acme", "key_ids": ["key_2Lq9Wd4T"] },
  "withdrawn_at": null
}
```

When every sitting people wrote in was verified by check-ins, the line ends `(verified)`; when some were, it ends with the share, such as `(73% verified)`.

## Seal a document

`POST /v1/documents/{document_id}/seals`

Seals the document as it stands now, as a new version. A document with pending suggestions can't be sealed: a seal says what the document says, so they're accepted or rejected first.

| Parameter | Type | Description |
| --- | --- | --- |
| `document_id` | string, path, required | The document's id. |
| `user` | string, required | The Proof id of the person sealing it. They must be one of the document's owners. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/documents/doc_8Hq2Lm4R/seals \
  -H "Authorization: Bearer $PROOF_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user": "usr_5Kq2Lm8R"}'
```

</TabItem>
<TabItem label="Node">

```ts
const seal = await proof.seals.create("doc_8Hq2Lm4R", { user: "usr_5Kq2Lm8R" });
console.log(seal.line);
```

</TabItem>
</Tabs>

Returns the [seal](#the-seal-object) with `201`.

| Error | When |
| --- | --- |
| `403 not_owner` | `user` isn't one of the document's owners. |
| `409 document_empty` | Nothing has been written in the document. |
| `409 document_trashed` | The document is in the trash. |
| `409 open_suggestions` | The document has pending suggestions. |
| `409 unchanged` | It reads exactly as its newest seal still up. |

## List a document's seals

`GET /v1/documents/{document_id}/seals`

Every seal made from a document, newest first, withdrawn ones included.

| Parameter | Type | Description |
| --- | --- | --- |
| `document_id` | string, path, required | The document's id. |
| `limit` | integer | 1 to 100. Default 50. |
| `after` | string | The `next` cursor from the previous page. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/documents/doc_8Hq2Lm4R/seals \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const page = await proof.seals.list("doc_8Hq2Lm4R");
```

</TabItem>
</Tabs>

```json
{
  "data": [
    {
      "code": "AH271NSHS",
      "url": "https://proof.exalto.ai/AH271NSHS",
      "document": "doc_8Hq2Lm4R",
      "version": 1,
      "sealed_at": "2026-10-13T23:31:04Z",
      "sealed_by": "usr_5Kq2Lm8R",
      "method": "v0.2",
      "line": "proof.exalto.ai/AH271NSHS · 87% typed/shaped · 8% AI · 5% external · 4 prompts · 2 sessions, 1hr48min",
      "composition": { "typed": 71, "shaped": 16, "ai": 8, "external": 5 },
      "presence": { "share": 97, "verified_share": 0, "verified": false },
      "prompts": 4,
      "sittings": 2,
      "duration": "1hr48min",
      "attested_by": { "platform": "acme", "key_ids": ["key_2Lq9Wd4T"] },
      "withdrawn_at": null
    }
  ],
  "next": null
}
```

## Retrieve a seal

`GET /v1/seals/{code}`

Looks a seal up by its code, such as one a reader pasted into your product. You can only retrieve seals made on your platform; anyone can open a public seal's page at `proof.exalto.ai`.

| Parameter | Type | Description |
| --- | --- | --- |
| `code` | string, path, required | The seal's code. Case doesn't matter. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/seals/AH271NSHS \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const seal = await proof.seals.retrieve("AH271NSHS");
```

</TabItem>
</Tabs>

Returns the [seal](#the-seal-object), or `404 seal_not_found`.

## Withdraw a seal

`DELETE /v1/seals/{code}`

Takes a seal down. Its page then says it was withdrawn, and when, instead of showing the text. A withdrawn seal can't be brought back, and other versions of the same document stay up. Seals can be withdrawn but never changed.

| Parameter | Type | Description |
| --- | --- | --- |
| `code` | string, path, required | The seal's code. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/seals/AH271NSHS \
  -X DELETE \
  -H "Authorization: Bearer $PROOF_SECRET_KEY"
```

</TabItem>
<TabItem label="Node">

```ts
const seal = await proof.seals.withdraw("AH271NSHS");
```

</TabItem>
</Tabs>

Returns the [seal](#the-seal-object) with `withdrawn_at` set, and Proof sends `seal.withdrawn`. Withdrawing a seal that's already withdrawn returns it unchanged.

While a document is in its owners' trash, its seals are down: their pages show nothing until it's restored.

## Who reads seals

A document's owners choose who reads its seals, every version alike, in the editor's seal dialog (the [`openSeal` method](/reference/embed/#methods) opens it):

- **Anyone with the link**, the default. Only these seals have a link preview.
- **Private**: the document's members; readers the owners list by email, or `*@domain` for everyone at a domain; and, if they set one, whoever has its password.

A private seal's page asks the reader for the password, or to sign in with your platform, which says who they are with a [reader request](#reader-requests). Signing in works only when you've set a reader URL (`reader_url` on the [platform](/reference/api/platform/)).

## Reader requests

A reader request is someone asking to read a private seal, sent to your reader URL with its id, as an agent request is sent to your consent page:

```
https://acme.com/proof/read?request=srq_4Wn8Kd2P
```

Your page signs the person in, then approves the request from your server with who they are and their verified email, and sends the browser to the `redirect_url` it returns. Proof lets them in if they're a member or their email is one of the readers, and remembers them in that browser for 30 days. A request lasts 10 minutes.

### Retrieve a reader request

`GET /v1/reader-requests/{request_id}`

```json
{
  "id": "srq_4Wn8Kd2P",
  "seal": "AH271NSHS",
  "title": "On the Analytical Engine",
  "status": "pending",
  "expires_at": "2026-10-30T18:30:00Z"
}
```

`status` is `pending`, `answered` once you've approved it, or `finished` once the reader is back at the seal. Returns `404 request_not_found` for an id that isn't one of yours.

### Approve a reader request

`POST /v1/reader-requests/{request_id}/approve`

| Parameter | Type | Description |
| --- | --- | --- |
| `request_id` | string, path, required | The request's id. |
| `user` | string, required | The Proof id of the person signed in to your product. |
| `email` | string or null | Their email, only if you've verified it: Proof lets readers in by it. |

<Tabs syncKey="lang">
<TabItem label="curl">

```bash
curl https://api.proof.exalto.ai/v1/reader-requests/srq_4Wn8Kd2P/approve \
  -H "Authorization: Bearer $PROOF_SECRET_KEY" \
  -H "Content-Type: application/json" \
  -d '{"user": "usr_9Tb3Wc1X", "email": "grace@acme.com"}'
```

</TabItem>
<TabItem label="Node">

```ts
const { redirect_url } = await proof.readerRequests.approve("srq_4Wn8Kd2P", {
  user: "usr_9Tb3Wc1X",
  email: "grace@acme.com",
});
// then redirect the browser to redirect_url
```

</TabItem>
</Tabs>

```json
{ "redirect_url": "https://proof.exalto.ai/AH271NSHS/continue?state=Vq2Lk7Tz1Mx9Pw3H" }
```

| Error | When |
| --- | --- |
| `404 user_not_found` | The user doesn't exist. |
| `409 request_expired` | The request was answered already, or more than 10 minutes have passed. The reader starts again from the seal. |