# Entries

**Entries** are what viewers save from a page: votes, RSVPs, sign-ups, answers, comments. [Page data](/docs/page-data.md) is written by you; entries are written by the people viewing the page.

```diagram entries
caption: A viewer's entry goes from your page through the DocPin page, which adds who they are. Other viewers see only the name before the @; your workspace sees the email.
Viewer (signed in) ─clicks─▶ your page code: docpin.shared.put() ─asks─▶ DocPin page (adds the sign-in) ─saves─▶ Entries (value + email)
Entries ──name only (sam)──▶ other viewers, via docpin.shared.list()
Entries ──full email───────▶ your workspace: Data tab, REST API, MCP
```

## At a glance

| | |
| --- | --- |
| **Off by default** | Turn on per page: **Data → Let viewers save entries** in the app, `PATCH /v1/pages/{id}` with `{"entries": "viewers"}`, or the MCP tool `set_page_entries` |
| **Two kinds** | Shared collections everyone can read (`docpin.shared`), and each person's private object (`docpin.mine`) |
| **Who can save** | Viewers signed in to DocPin who can open the page. One email is one person |
| **Who can read** | Anyone who can open the page reads shared entries, with authors' names (the part of the email before the @) |
| **Emails** | Only your workspace sees them, in the app, the API and MCP |
| **Removing** | Viewers delete their own entries, or everything they saved with **Remove my entries** in the DocPin panel. Your workspace can delete any entry |

## Who can save

| Page access | Read shared entries | Save entries |
| --- | --- | --- |
| `team` | Members | Members |
| `people` | Members and listed people | Members and listed people |
| `password` | Anyone with the password | People signed in to DocPin on that site. The password screen offers your team a sign-in |
| `public` | Anyone | Only people already signed in to DocPin on that site |
| A secret-link (unlisted) `public` or `password` page, at its `docpin.it` short link | Anyone | Nobody: viewers stay anonymous there |

- Public pages don't offer a sign-in yet, and password pages offer it only to your team, so in practice the people saving are your team. Anyone with a DocPin account can sign in on the site, though, so don't rely on this to keep a public page's entries to your team.
- Drafts, archived pages and version previews don't take entries.
- When entries are on, the page's DocPin panel says "This page saves what you enter. People in its workspace can see it."

## Use them in the page

Read first, then save when the person does something, such as pressing a button. Saving can fail (they may not be signed in), so catch it and say why.

```html
<button id="going">I'm going</button>
<p id="status"></p>
<ul id="rsvps"></ul>
<script type="module">
  const status = document.querySelector("#status");
  const show = async () => {
    const { entries } = await docpin.shared.list("rsvps");
    const list = document.querySelector("#rsvps");
    list.replaceChildren();
    for (const e of entries) {
      const li = document.createElement("li");
      li.textContent = e.author.name;
      list.append(li);
    }
  };
  document.querySelector("#going").onclick = async () => {
    try {
      await docpin.shared.put("rsvps", { going: true });
      await show();
    } catch (e) {
      status.textContent = e.message;
    }
  };
  show().catch((e) => (status.textContent = e.message));
</script>
```

> **Always show entries as text, never as HTML.** Use `textContent` or `createTextNode`, not `innerHTML`. An entry is whatever a viewer typed; inserting it as HTML would let one viewer run code in other viewers' browsers.

## Page API

```ts
docpin.viewer(): Promise<{ signedIn: true; name: string } | { signedIn: false }>

docpin.mine.get(): Promise<object>                    // {} until something's saved
docpin.mine.set(changes: object): Promise<object>     // merges; a null value removes that key

docpin.shared.list(collection: string, options?: { cursor?: string }):
  Promise<{ entries: Entry[]; count: number; cursor: string | null }>
docpin.shared.put(collection: string, value: unknown): Promise<Entry>
docpin.shared.add(collection: string, value: unknown): Promise<Entry>
docpin.shared.remove(id: string): Promise<{ removed: true; id: string }>

type Entry = {
  id: string;
  collection: string;
  author: { name: string; you: boolean };   // name: the part of the email before the @
  value: unknown;
  createdAt: string;
  updatedAt: string;
};
```

| Call | What it does |
| --- | --- |
| `viewer()` | Whether the person is signed in, and their name. Pages never get a viewer's full email |
| `mine.get()` / `mine.set(changes)` | This person's private object on this page. Only they read it through the page. Up to 64 KB |
| `shared.list(collection, { cursor })` | Up to 500 entries, oldest first. When `cursor` isn't `null`, pass it back for the next 500 |
| `shared.put(collection, value)` | This person's one entry in the collection, created or replaced. For votes, RSVPs and scores: nobody can vote twice |
| `shared.add(collection, value)` | A new entry every time. For comments and logs |
| `shared.remove(id)` | Deletes one of this person's own entries |

Rules:
- Collection names are 1 to 40 of `a-z 0-9 - _`.
- Use a collection with either `put` or `add`, not both. A collection keeps the kind of its first entry, so a one-each poll can't be padded with extra entries.
- A value is any JSON except `null`, up to 16 KB.
- To count votes, count entries. There's deliberately no shared counter.
- To show other people's new entries, call `list` again, after saving or on a timer of 15 seconds or more.

## Errors

A failed call rejects with an `Error` whose `code` is:

| `code` | Meaning |
| --- | --- |
| `off` | Entries aren't on, the page is a draft or archived, or it isn't open at its DocPin link (a version preview, or a file opened on its own) |
| `signin` | The person needs to be signed in to DocPin to save |
| `forbidden` | They can't open the page, or it isn't their entry |
| `not_found` | That entry doesn't exist any more |
| `limit` | A [limit](#limits) was reached |
| `rate` | Too many calls. Wait a moment |
| `invalid` | Something about the call is wrong; the message says what |
| `busy` | DocPin was busy. Try again |
| `timeout` | No answer within 15 seconds. The save may still have worked: read before saving again |
| `network` | The browser couldn't reach DocPin |
| `error` | Something went wrong on DocPin's side |

## Limits

| | Free | Pro |
| --- | --- | --- |
| All entries on a page | 1 MB, 2,000 entries | 10 MB, 50,000 entries |
| Shared entries one person can have on a page | 100, 100 KB | 1,000, 1 MB |
| One entry | 16 KB | 16 KB |
| Private object per person per page | 64 KB | 64 KB |
| Saving | 30 a minute per person, 300 per page | Same |
| Reading (`viewer`, `mine.get`, `list`) | 300 a minute per person on a page (per network address if not signed in), and 3,000 a minute per page in total | Same |

Making an entry smaller always works, even over a limit.

## For your workspace

Everyone in the workspace can see entries with each author's email: in the app on the page's **Data** tab (with CSV and JSON export), or with the REST API and MCP.

### `PATCH /v1/pages/{id}`

Body `{ "entries": "viewers" }` turns entries on; `{ "entries": "off" }` closes the page to new ones, keeping what's there. Returns `{ "entries": "viewers" | "off" }`. You can also pass `entries` to `POST /v1/pages`; it's ignored for drafts.

### `GET /v1/pages/{id}/entries`

Query: `collection` (a name, or empty for everyone's private objects; leave it out for all), `offset`. Returns the newest 500 entries, newest first:

```json
{
  "setting": "viewers",
  "usage": { "bytes": 2140, "entries": 12, "maxBytes": 1000000, "maxEntries": 2000 },
  "collections": [{ "name": "rsvps", "mine": false, "count": 9 }, { "name": "", "mine": true, "count": 3 }],
  "entries": [{ "id": "…", "kind": "put", "collection": "rsvps", "author": "sam@acme.com", "value": { "going": true }, "createdAt": "…", "updatedAt": "…" }],
  "count": 12,
  "next": null
}
```

`kind` is `put`, `add`, or `mine` for a private object (whose `collection` is `null`). When `next` isn't `null`, pass it as `offset`.

### `DELETE /v1/pages/{id}/entries`

Deletes a collection (`?collection=rsvps`), everyone's private objects (`?collection=` with nothing after it), or every entry (no `collection`). Returns `{ "removed": 9 }`.

### `DELETE /v1/pages/{id}/entries/{entryId}`

Deletes one entry. Returns `{ "removed": 1 }`, or `404`.

### MCP

| Tool | What it does |
| --- | --- |
| `set_page_entries` | `on: true` or `false` for a page |
| `get_page_entries` | Totals, collections and the newest entries with emails. Optional `collection` |

Deleting the page, or deleting its board along with the board's pages, deletes its entries. Renaming or moving it, or publishing a new version, keeps them. When someone deletes their DocPin account, their entries are deleted.
