A .showpapers collection keeps original documents together with the information and organization built around them. It can contain:
- papers, their titles and types;
- the on-device reading;
- details, with evidence, confidence, origin and review;
- custom details and removed suggestions;
- dates and reminders;
- people;
- personal and built-in folders with their rules;
- pins;
- notes;
- relationships with their decisions.
It also marks which values came from an external AI agent, so that the apps can ask the person to review them.
The specification overview explains where to start and what a reader must check. The AI-app rules (media type, alternate names, companion, changes documents) are in AI interchange. A step-by-step guide for agents is in the agent guide.
Collections are readable ZIP archives: anyone who receives one can read its contents. The optional protected envelope encrypts the archive. A collection is a selected snapshot of records, not a full-vault backup.
Container Rules
Every collection follows these rules:
- One archive. A single
manifest.jsonplus the declared payloads. No other entries and no directories. - ZIP entries. Stored or deflated only. No extra fields, encryption, ZIP64, symlinks, backslashes, colons, absolute paths,
.or... - Paths. Each path is derived from its record ID. Paths are never extracted to a filesystem.
- IDs. The collection, originals, Notes, folders, people, agents and relationships use canonical lowercase UUIDs, unique across those records. Paper records and readings reuse their original’s ID. Detail IDs follow the origin-specific rules below.
- Payload checks. Every payload's size, CRC and SHA-256 are verified, and every original's media signature.
- Strict JSON. Valid UTF-8, no byte-order mark, no duplicate keys, no
NaN. Integers are canonical tokens, so1.0is refused. - Fail closed. A truncated, corrupt, oversized or partly unsupported archive is refused as a whole, never partially imported.
Specification 1.0.0 defines this collection contract. The collection is edited with edit + create or a changes document. Readers refuse unknown file versions rather than guessing.
Entries
| Entry | Content | Schema |
|---|---|---|
manifest.json | Collection, writer, agents, originals and the index of every payload, notes, folders, people, relationships, pins and category icons | urn:showpapers:format:manifest:4 |
originals/<id>.<pdf|png|jpg|webp> | Original bytes, one file per paper | media signature |
papers/<id>.json | Paper record: everything about one paper except its bytes and title | urn:showpapers:paper-record:1 |
readings/<id>.json | The app's on-device reading of that paper | urn:showpapers:reading:1 |
notes/<id>.json | Note record: rich content, saved rows and paper links; its title is in the manifest | urn:showpapers:scratch-record:1 |
A paper record and a reading use the ID of the original they describe. Both are optional, and each paper has at most one of each. In the manifest, papers[] and readings[] follow the order of originals[].
Optional Sections And Granularity
Only format, formatVersion, protection, generator, collection and originals are required in the manifest. Every other section may be left out:
- An omitted array means empty.
- An omitted
librarymeans no pins and no category icons. - An omitted paper record means the file says nothing about that paper beyond its original and title.
Inside a paper record only paperVersion is required. Every other member has two states:
- Omitted: the file does not describe it. Importing into an existing paper leaves the library's value unchanged. A new paper gets the app's default.
- Present: this is the complete value, and an empty list means "none".
This lets a writer describe only what the exchange needs: one detail, or a record with an existing reading, reviewed details, people, a reminder and folder rules. External agents preserve existing readings and review decisions; their own additions follow the agent-origin rules.
Order. These arrays are ordered, and readers keep their order:
originals,notes,foldersandlibrary.pinnedPaperIds;- everything inside a note record;
- reading pages, blocks, lines, words and polygon points;
- a detail's
evidence.
Every other array is a set: details, removedSuggestions, personIds, type.suggested, folder paperIds and excludedPaperIds, relationships, reasons, agents, people and categoryIcons. Readers must not give their order a meaning, and writers should emit a stable order.
Structured records are closed: an unknown member invalidates the file. The reading’s sdkAnalysis is an exception: it carries the reader’s own bounded result, as described under Readings. A member that is defined as an object (type, reminder, a detail) always carries all of its keys; a value that does not apply is null or [].
Manifest
| Member | Type | Rule |
|---|---|---|
format | string | showpapers |
formatVersion | integer | 4 |
protection | string | readable |
generator | object | name (1–80), platform (1–40), version (1–40). The program that wrote the file, for example {"name":"Example Writer","platform":"example","version":"1.0.0"}. It is not a signature. |
collection | object | id, revision (1–2⁵³−1), title (1–200, trimmed). Optional parent: revision (lower than revision) and archiveSha256 of the exact archive this revision was edited from. |
agents | array ≤32 | External AI agents that proposed values: id, name (1–80), platform (0–40), version (0–40) |
originals | array ≤100 | Each record declares id, title, path, mediaType, size and sha256. Titles are 1–100 characters, trimmed, no control characters, no / or \ |
papers | array ≤100 | id, path = papers/<id>.json, size (1–1 MiB), sha256 |
readings | array ≤100 | id, path = readings/<id>.json, size (1–8 MiB), sha256 |
notes | array ≤100 | id, title, path, size, sha256, encoding: "utf-8" and paperIds; one note record per note. paperIds is its first-occurrence reference union, as defined in Notes |
folders | array ≤64 | Personal folders, see Folders |
people | array ≤64 | id, name (1–80, trimmed, unique ignoring case), isMe (at most one true) |
relationships | array ≤5000 | See Relationships |
library | object | pinnedPaperIds (ordered pins) and categoryIcons (≤64: purpose, label, icon) |
Character counts are Unicode scalars unless a limit says UTF-16 units; the apps count folder names and purposes in UTF-16 units. Every collection JSON document (manifest, paper record, reading, changes document) nests at most 32 levels deep. Readers accept any JSON within the byte limits and this depth, with no separate count of values: an 8 MiB reading can hold hundreds of thousands of them.
Notes
A Note keeps its title and ID in the manifest and its saved content in notes/<id>.json. It can carry formatted text, checklists and paper references, along with record metadata. The note record has exactly these members: recordVersion, template, iconId, createdAt, updatedAt, automaticTitle, items and document.
recordVersionis1.templateandiconIduse the note-record schema's supported vocabulary.- Creation and update times are decimal millisecond strings; the update cannot precede creation.
automaticTitleis a Boolean. itemsholds ordered saved rows. Each row preserves its ID, label,checkedandcheckableflags, and orderedpaperIds.documentisnullor a rich-text object with exactlynoteVersion,text,marks,paperIdsandpaperLinks. A null document is distinct from an empty one. Rows and a document may coexist; readers preserve both.
In rich content, noteVersion is 1. Marks use half-open UTF-16 ranges: style 1 means bold and 2 italic. Inline paper links use the same ranges and name an original in the collection. Ranges must be nonempty, within the text, and at Unicode scalar boundaries; paper links cannot overlap. paperIds also retains attachment-only references, in first-occurrence order. The manifest's references are the first-occurrence union of saved rows and rich content.
Bullets and checklist lines use ordinary text prefixes: • , ☐ and ☑ . Text preserves whitespace, Unicode and line feeds; rich content allows tabs, but refuses carriage returns and other control characters. There is no Markdown conversion or separate checklist-state field for rich text. Complete vocabularies, byte limits and closed-object constraints are defined by the note-record and note-document JSON Schemas in the specification index.
Paper Records
| Member | Meaning |
|---|---|
paperVersion | 1 |
createdAt | When the paper was added, as a decimal string of milliseconds since 1970 (strings keep 64-bit values exact in JavaScript) |
automaticTitle | true while the title is still the one the app generated |
type | detected (the reading's type, null if never read), suggested (other types the reading considered), selected (the person's choice, null if none), custom (the person's own type name, only when selected is generic), agentSuggestion (an external agent's proposed type awaiting review, or null) |
details | Every detail of the paper, see Details |
removedSuggestions | Suggestions the person removed, remembered so they do not come back |
personIds | The people this paper belongs to |
builtInFolder | The built-in folder the person placed it in, or null for automatic placement |
reminder | The paper's reminder or null, see Reminders |
Types use these ids: generic, i20, i94, passport, visa, i797, employment-authorization, drivers-license, social-security, birth-certificate, marriage-certificate, tax, insurance. Use the exact identifiers above. For another type, use generic with a descriptive custom name in the appropriate person-selection or agent-suggestion field.
Details
Every detail has the same twelve members: id, origin, key, label, value, normalizedValue, valueType, concerns, evidence, confidence, agentId, review. key is one of 78 field keys (for example passport-number, expiration-date, other). valueType is text, date or duration-of-status.
origin | Who produced it | Rules |
|---|---|---|
reading | The app's on-device reading (OCR and on-device AI) | id matches [a-z][a-z0-9_]{0,95}; 1–16 evidence items, each a reading line {page, block, line} that exists in readings/<id>.json; label only for key other; confidence and agentId are null; review required |
person | A custom detail the person typed | key other, label (0–80), value (0–500 characters), valueType text; no evidence, concerns, confidence, agent or review |
agent | An external AI agent | id is a UUID; agentId names manifest.agents; value not blank; a label when the key is other; empty concerns; optional confidence (0–1); 0–16 evidence items, either reading lines or quotes {page, quote, region}; review required |
review is {"confirmed": true|false, "correctedValue": string|null}:
correctedValueis the person's correction. The effective value is the correction when present, otherwisevalue.confirmedmeans the person checked the detail against the original. A confirmed detail has a non-blank effective value.- An agent detail with
confirmed: falseis a pending suggestion.
Values and normalized values are at most 4096 UTF-8 bytes. An agent's date detail with a normalizedValue uses ISO YYYY-MM-DD. Reading concerns are invalid-date, ambiguous-date, conflicting-values and read-by-ai (the on-device AI model read this value from the page image; the on-device text recognizer did not independently confirm it, so review it against the original before relying on it).
There are no separate date fields. Dates are details with valueType: "date". A paper's end date is its earliest confirmed expiration-date, valid-until, program-end-date, admit-until or coverage-end-date. The apps derive it, so a file never states it separately.
Removed Suggestions
{"origin": "reading", "id", "identity"}remembers a reading suggestion the person removed.identityisnullor holds:keyandvalueType;pages: 1–16, ascending;sdkFieldIdandsdkDocumentType: bothnullor both set.
{"origin": "agent", "id", "agentId", "key", "label", "value"}remembers an agent suggestion the person rejected, so importing the same proposal again does not bring it back.
A removed suggestion never shares an ID with a current detail.
Readings
readings/<id>.json holds:
readingVersion1;extractorVersion(1–100,000);concerns: any ofno-text,unrecognized-document,conflicting-document-types,no-supported-fields,field-limit-reached;pages[];sdkAnalysis.
Each page has:
index: 0–199, unique;widthandheight: 1–20,000;engine: printable ASCII, at most 128 bytes;text: at most 256 KiB;blocks[]: at most 2,000.
Each block has text (≤64 KiB), region and lines[] (≤10,000). Each line has text (≤8 KiB), region, confidence and words[] (≤2,048). Each word has text (≤2,048 bytes), region and confidence.
A region is {"box": [left, top, right, bottom] | null, "polygon": [[x, y], ...]}:
- All numbers are 0–1, normalized to the rendered page.
- The box has left ≤ right and top ≤ bottom.
- The polygon has at most 8 points.
Confidence is null or 0–1. Words are optional detail: writers may leave words empty.
sdkAnalysis is the reader's own result: an object with a sdkVersion string, at most 64 members and 512 KiB as canonical JSON. Agents preserve an existing value unchanged. When creating a collection, agents do not invent a reading or reader result.
A paper's reading plus its reading and custom details must fit the apps' analysis store: 50,000 nodes and 2 MiB. The reference validator computes both exactly as the apps do, and counts sdkAnalysis at twice its compact size. It refuses a paper that does not fit with limit.
Folders
A personal folder has:
id: a canonical lowercase UUID;name: 1–80 UTF-16 units, trimmed, unique ignoring case;color:blue,teal,green,amber,coral,purpleorgray;icon: one of 13 ids, ornull;purpose: 0–240 UTF-16 units, trimmed;rule:nullor an automatic filing rule;paperIds.
Array order is the folder order. Folders hold papers only; the apps have no notes in folders. Across all folders there are at most 2,000 memberships and 2,000 exclusions.
A rule has:
documentTypes: up to 11 exact reader document type ids, for exampleice.sevp.issued.i-20;autoAdd: needs at least one document type;excludedPaperIds: papers the person removed from the folder. They are never also members.condition:null,{"caseReceipt": "EAC2512345678"}, or{"reviewedDetails": [1–3 clauses]}.
Each clause has a distinct field (16 names, six of them dates), an operator (equals, before, on-or-before, after, on-or-after) and a value (1–160). Only date fields use ordering operators, and their values are ISO dates.
Built-in folders are not stored as folders. A paper's builtInFolder records the person's explicit placement, one of: identity, visas, notices, applications, residency, work-authorization, travel-entry, study, home, finance, health, education, work, family, legal, other. null means the app places the paper automatically from its type.
library.categoryIcons records the icon the person chose for a category:
purpose:identity,admission,approvals-and-receipts,study,work-permission,personal-records,tax,insurance,customorunknown;label: the custom type name forcustom, otherwisenull.
Reminders
A paper has at most one reminder: {"source", "detailId", "date", "leadDays", "enabled"}.
sourceisdetailordate. Adetailreminder names a date detail of the same paper whose key isexpiration-date,valid-until,program-end-date,admit-untilorcoverage-end-date. Adatereminder is on a date the person picked and hasdetailId: null.dateis a real ISO calendar date.leadDaysis7,14or30.
Delivered notifications and the global reminder switch are device state and settings. They are not carried.
Relationships
A relationship joins two papers. Its members are:
id;type: one ofsame-case,different-stage,same-person,same-record,same-organization,supporting-document,renewal,amendment,subsequent-filing,duplicate,alternate-version;paperIds: two different papers, sorted ascending;referringPaperId: the paper that points at the other, for direction, ornull;status:suggested,confirmedorrejected;origin:reading(the app's rules),personoragent;agentId: required exactly for agent relationships;reasons: up to 16 strings of 1–500 characters;decision:nullor{"action": "accept"|"reject", "reason": 0–500}. An accepted relationship is confirmed and a rejected one is rejected.
Duplicates the person kept apart are a duplicate relationship rejected with a reason such as "Keep both Papers". Case history and renewal history are views the apps compute from confirmed relationships. They are not stored separately.
A relationship's id is derived, so every implementation computes the same one, and it is stored in the relationship record for validation and reference during exchange. It is Java's UUID.nameUUIDFromBytes, an MD5-based version 3 UUID, of the UTF-8 text:
showpapers:relationship:1:<paperIds[0]>:<paperIds[1]>:<type>
The reference tool's relationship_id and the fixtures show worked values.
Provenance And Review
generatornames the program that wrote the file. The reference tool always writes itself; the apps writeShowPapers.agents[]names every external AI agent whose proposals the file carries. An agent'sidis any UUID. The reference tool derives it from the agent's name, platform and version (showpapers:agent:1:<name>\n<platform>\n<version>), so proposals from the same agent share one entry.- Agent-origin values:
- details with
origin: "agent"; type.agentSuggestion;- relationships with
origin: "agent"; - removed agent suggestions.
Each names its agent.
- details with
- A file cannot prove who reviewed what. Review marks (
confirmed, decisions) are claims made by the writer. Apps apply the trust rules in Import.
Import
This section is normative for apps that implement the collection format. It is written so that Android and iOS behave identically.
Opening
- Recognize the file by content (ZIP signature plus a root
manifest.jsonwhoseformatisshowpapers), never by name. See accepted names. - Validate the entire archive before showing anything. Any error refuses the whole file with a message that names no content.
- Show a preview: counts of papers, notes, folders, people, relationships and details, and how many values came from each agent and are pending review.
Matching Records To The Library
| Record | Matches an existing library record when |
|---|---|
| Paper | Same ID and the stored original has the same SHA-256. Same ID with different bytes is a conflict: the file's paper is offered as a separate copy with a new ID. |
| Note, folder, person | Same ID. A person or folder with a different ID but the same name (ignoring case) is offered as "use existing", because the apps require unique names. |
| Relationship | Same derived ID, after mapping paper IDs |
| Detail | Same paper and same detail ID. A removed suggestion with that ID keeps it removed. |
A file ID that is already used by a different kind of record in the library gets a new ID. Every reference is rewritten consistently, and relationship IDs are derived again.
Create
Unmatched records are created with the file's IDs (stable IDs survive a round trip), each field exactly as in the file. Omitted paper-record members take the app's defaults:
| Member | Default |
|---|---|
createdAt | Import time |
type | Unread, generic |
| Details, people, reminder | None |
builtInFolder | Automatic |
A paper that arrives with a reading or details is not queued for automatic reading.
Merge
For each matched record, the app compares every member the file describes with the library's value. It never deletes or blanks anything the file does not describe:
- Equal values change nothing.
- A different value becomes a proposed change on the review screen, showing the current and proposed value.
- By default the app keeps the person's own work: a value the person entered, corrected or confirmed in the library is not replaced unless the person picks the file's value.
- A detail, type suggestion or relationship from an agent is added as a pending suggestion. It is never auto-confirmed, and it never replaces a confirmed value.
- Folder membership, pins and people assignments merge as sets. Pins keep the library's order and append new pins in the file's order.
- A file never deletes a library record. Deletions come only from a changes document and each one is reviewed.
Trusting Review Marks
A file is not signed, so review marks are the writer's claim. The apps apply two rules:
- Values with
origin: "agent"always arrive pending (confirmedis ignored) unlessgenerator.nameisShowPapers, meaning the person reviewed them in an app, and the person keeps Keep review marks switched on. - For all other values, Keep review marks defaults to on when
generator.nameisShowPapersand off otherwise. When it is off, every imported detail arrives unconfirmed, and every relationship decision is kept only as a suggestion.
Pending agent values do not count as reviewed. They do not feed reminders, folder rules, end dates or case history until the person confirms them. The app labels each one with its agent's name, for example "Suggested by Example Assistant".
Round-Trip Requirements
A reader that implements the current collection format must meet these four conformance requirements. Application builds should be checked against them before claiming support.
- Valid means importable. Every file the reference validator accepts is accepted by both apps. Every file it refuses is refused by both, with the same category. The collection's per-record limits are the apps' native limits, so nothing is truncated or silently cleaned. For example, a paper title containing
/is refused rather than stripped. - Nothing is lost. After Add to Library into an empty library, exporting the same records again produces an equivalent collection:
- the same IDs, titles, bytes and member values;
- the same order of originals, notes, folders and pins.
Only these may differ:
generator,collection.revisionandparent;- the reading's
sdkAnalysisformatting; - derived views. Among them are relationships with
origin: "reading"that have no decision: the app's rules recompute them on the importing device, so they may appear, change or disappear. Every relationship the person or an agent made, and every decision, round-trips exactly.
- Same result on both platforms. Android and iOS given the same file and the same choices produce the same library records, and export the same file bytes apart from
generator. - Capacity is explicit. If the library cannot hold everything, the app lists what does not fit and lets the person deselect items; it never drops or truncates. The apps hold at most 24 notes, 64 people and 64 folders, and a valid file within these limits always fits an empty library.
validatewarns withnative-note-capacitywhen a file has more notes than the apps hold.
Creating And Editing A Collection
| Goal | How |
|---|---|
| Create from papers | Write a build spec (formatVersion: 4) that lists the original files and any records, then create --spec build.json --output new.showpapers |
| Change anything | edit old.showpapers --output-directory work/, edit the JSON files in work/, then create --spec work/build.json --output new.showpapers. The build spec already carries the next revision and the exact parent archive. |
| Propose specific changes | Write a changes document, then preview old.showpapers --changes changes.json and apply old.showpapers --changes changes.json --output new.showpapers |
| Describe it for an AI app | companion old.showpapers --output old.showpapers.json |
Each collection workflow above validates the complete collection. Commands that produce files write a new file or directory; existing inputs and destinations are never overwritten.
Not Included
These are deliberately absent. The contents statement lists these exclusions:
derived-views-and-caches: thumbnails, search indexes, end dates, case and renewal histories, organization records, the country filter;device-state: device-specific revision stamps and storage metadata;reading-queue: reading jobs and their status;notification-history: delivered reminders;import-history: import ledgers and lineage;conversations-and-ai-history;app-settings;keys-and-credentials;native-vault-envelopes.
Protected Collections
Protected Files wraps the exact bytes of a readable collection and authenticates and validates the inner archive before returning anything. The size caps are unchanged: 103 MiB readable, 104 MiB protected. A protected source saves as a protected copy.
Conformance
The example corpus contains:
- one archive per feature and a combined archive;
- a changes document with the archive it produces;
- a companion describing a collection;
- 30 invalid archives that each break one rule, with the expected error code.
The apps' reader and writer tests must read every valid case with every value intact, and refuse every invalid one with its code. They must also re-export the valid cases to equivalent files.
Download cases from the corpus index. Each entry names its file, SHA-256 and expected result; invalid cases include the expected error code. Verify the downloaded digest, then exercise your own implementation against every case. For example, with the extracted reference tool:
python3 showpapers-format/showpapers_format.py validate everything.showpapers
python3 showpapers-format/showpapers_format.py validate invalid-two-owners.showpapers
The first command succeeds. The second refuses the file with the corpus's expected error code and exit status 2. A reader must retain every admitted value; a writer's output must also pass complete archive validation. Passing JSON Schema checks alone is insufficient.