# Revise Developer Platform
> Build document workflows with Revise CLI, Revise API, and Revise Editor SDK.
Read, edit, and redline Word documents from the command line or TypeScript. MIT licensed.
Edit documents with prompts. Supports .docx files with tracked changes and comments.
Embed a collaborative document editor with agent tools in your web application
Agent skills for Claude Code, Codex, and others, plus Markdown versions of every page.
## All pages
- [Revise CLI](https://revise.io/developer/docs/revise.md): Edit and convert Word documents headlessly
- [Overview](https://revise.io/developer/docs/revise/introduction.md): Read, edit, and redline Word documents from the command line or TypeScript.
- [Changelog](https://revise.io/developer/docs/revise/changelog.md): Release history for revise, the CLI and TypeScript library.
- [Revise CLI](https://revise.io/developer/docs/revise/cli.md)
- [JSON output](https://revise.io/developer/docs/revise/json.md): The --json contract: one envelope for every command.
- [Library](https://revise.io/developer/docs/revise/library.md): ReviseDocument: open, read, edit, review, and export documents from Node.
- [Agents](https://revise.io/developer/docs/revise/agents.md): Give a model Revise's editing tools, or teach a coding agent the CLI.
- [Collaboration](https://revise.io/developer/docs/revise/collaboration.md): Seed, edit, and export live Yjs rooms that browsers are editing.
- [Build with AI](https://revise.io/developer/docs/build-with-ai.md): Agent skills, plugins, and machine-readable docs for coding agents working with Revise.
- [Revise Editor SDK](https://revise.io/developer/docs/editor-sdk.md): A fast, agent-friendly word processor for your app
- [Introduction](https://revise.io/developer/docs/editor-sdk/introduction.md): A word processor component for React applications.
- [Installation](https://revise.io/developer/docs/editor-sdk/installation.md): Install from npm. Free for evaluation, personal projects, and education.
- [Quickstart](https://revise.io/developer/docs/editor-sdk/quickstart.md): Render a document in about ten lines.
- [Licensing](https://revise.io/developer/docs/editor-sdk/license.md): Free for noncommercial use, education, and evaluation. A commercial license for production use by organizations.
- [Concepts](https://revise.io/developer/docs/editor-sdk/concepts.md)
- [Changelog](https://revise.io/developer/docs/editor-sdk/changelog.md): Release history for @reviseio/editor (formerly revise and @reviseio/sdk).
- [Support](https://revise.io/developer/docs/editor-sdk/support.md): How to report a problem, and what to send with it.
- [Guides](https://revise.io/developer/docs/editor-sdk/guides.md)
- [API reference](https://revise.io/developer/docs/editor-sdk/api.md)
- [Version 2.3](https://revise.io/developer/docs/editor-sdk/v2.3.md)
- [Version 2.2](https://revise.io/developer/docs/editor-sdk/v2.2.md)
- [Version 2.1](https://revise.io/developer/docs/editor-sdk/v2.1.md)
- [Version 2.0](https://revise.io/developer/docs/editor-sdk/v2.0.md)
- [Revise API](https://revise.io/developer/docs/revise-api.md): Edit and convert documents with our infrastructure
- [Revise API](https://revise.io/developer/docs/revise-api/introduction.md): Edit and convert documents through a REST API.
- [Authentication](https://revise.io/developer/docs/revise-api/authentication.md): Authenticate REST requests with a Revise API key.
- [REST quickstart](https://revise.io/developer/docs/revise-api/quickstart.md): Upload a document, submit a conversion, and retrieve the PDF.
- [Pricing and usage](https://revise.io/developer/docs/revise-api/pricing.md): Understand charges, account credit, and usage reports.
- [Prompts and document edits](https://revise.io/developer/docs/revise-api/prompts.md): Read, review, and edit documents, with clean and tracked DOCX output.
- [File conversions](https://revise.io/developer/docs/revise-api/conversions.md): Convert document formats or reconstruct editable content from PDFs and images.
- [Models and provider keys](https://revise.io/developer/docs/revise-api/models.md): Choose a model and decide who bills its inference.
- [Files, artifacts, and retention](https://revise.io/developer/docs/revise-api/files.md): Manage uploaded inputs, verified outputs, and content lifetime.
- [Job lifecycle and limits](https://revise.io/developer/docs/revise-api/jobs.md): Poll, cancel, and resume work, and control execution budgets.
- [Errors, idempotency, and retries](https://revise.io/developer/docs/revise-api/errors.md): Recover interrupted work without submitting a duplicate job.
- [Output encryption](https://revise.io/developer/docs/revise-api/encryption.md): Request authenticated JWE ciphertext for document artifacts.
- [Webhooks](https://revise.io/developer/docs/revise-api/webhooks.md): Receive, verify, and acknowledge job status notifications.
- [TypeScript client](https://revise.io/developer/docs/revise-api/typescript.md): Use @reviseio/api for typed REST calls and document workflows.
- [REST reference](https://revise.io/developer/docs/revise-api/reference.md)
Source: https://revise.io/developer/docs
---
# Overview
> Read, edit, and redline Word documents from the command line or TypeScript.
`revise` is a command-line tool and TypeScript library for Word documents.
It reads a `.docx` as HTML with stable block ids, edits it with the same tools
the Revise agent uses, records edits as tracked changes, and writes the file
back. Untouched content is written back byte for byte.
It is built for AI agents and the programs that run them: every command has
`--json` output, edits are undoable, and the editing tools come with schemas a
model can call.
**Install**
```bash
npm i -g revise
```
**Quick install for AI Agents (skill included):**
```bash
npx -y revise@latest skill install
```
Node 20 or newer. No account, API key, or telemetry, and documents never
leave your machine. Two things fetch over the network: PDF export downloads
its fonts from revise.io, and an image you insert by URL is downloaded.
## What it does
- **Read** documents as HTML with block ids, an outline with block ranges, or
plain-text search results.
- **Edit** with tracked changes by default in `.docx`: replace text, insert and
replace blocks, tables, comments, footnotes, styles, headers and footers,
page layout.
- **Review** pending changes by id or by section, see a word-level diff,
accept or reject, and undo.
- **Write** `.docx`, PDF, Markdown, HTML, and text; render pages as images;
compare two versions into a redline; create a document from Markdown or HTML.
- **Collaborate**: seed a sync server's room from a file, and edit or export a
live [Yjs](https://yjs.dev) document that browsers are editing.
## Pick an entry point
Every command, for shells and coding agents.
`ReviseDocument` and friends, for your own Node programs.
The editing tools in Anthropic and OpenAI shapes, and the agent skill.
Seed and edit live rooms next to `@reviseio/editor`.
Source: https://revise.io/developer/docs/revise/introduction
---
# Build with AI
> Agent skills, plugins, and machine-readable docs for coding agents working with Revise.
Revise publishes skills that teach coding agents (Claude Code, Codex, Cursor,
Copilot, and others) to work with Revise documents, and serves every docs page
as Markdown.
## Quickest start
Paste one line into any agent. It installs or updates the right skill and uses
it.
```text
Read https://revise.io/skills/cli.md and follow it.
```
| Line | Skill | For |
| --- | --- | --- |
| `Read https://revise.io/skills/cli.md and follow it.` | `revise` | Reading, editing, redlining, and converting Word documents with the [`revise` CLI and library](https://revise.io/developer/docs/revise/introduction) |
| `Read https://revise.io/skills/editor.md and follow it.` | `revise-editor` | Building document editors with [`@reviseio/editor`](https://revise.io/developer/docs/editor-sdk/introduction) |
| `Read https://revise.io/skills/collaborate.md and follow it.` | `collaborate-in-revise` | Working with people in live revise.io documents over the [Revise MCP server](https://revise.io/articles/mcp-server) |
## Install the skills
The skills live in [github.com/reviseio/skills](https://github.com/reviseio/skills),
as three plugins in the `revise` marketplace:
- **`cli@revise`**: the `revise-cli` skill, for the `revise` CLI and
TypeScript library.
- **`editor@revise`**: the `revise-editor` skill, for building editors with
`@reviseio/editor`.
- **`collaborate@revise`**: the `collaborate-in-revise` skill and the Revise
MCP server (`https://mcp.revise.io/mcp`).
`cli` and `editor` need no account or connection: the CLI, library, and
editor run on your machine and your servers.
### Quickest, for the CLI skill: any agent with a terminal
```bash
npx -y revise@latest skill install
```
It writes the `revise-cli` skill where your agent reads skills (`.claude/skills` from Claude Code, `.agents/skills` for Codex, Cursor, and others) and prints its path. Read that SKILL.md now and follow it: a running session doesn't load new skills, new sessions do.
The plugins below install all three skills for all your projects and update
with the marketplace.
### Claude Code
```bash
claude plugin marketplace add reviseio/skills
claude plugin install cli@revise
claude plugin install editor@revise # building an editor
claude plugin install collaborate@revise # live revise.io documents
```
Update with `claude plugin update cli@revise`, then run `/reload-plugins` or start a
new session.
### Codex and the ChatGPT desktop app
```bash
codex plugin marketplace add reviseio/skills
codex plugin add cli@revise
codex plugin add editor@revise # building an editor
codex plugin add collaborate@revise # live revise.io documents
```
Update with `codex plugin marketplace upgrade revise`, then add the plugins again and start a new
thread.
### Cursor, Copilot, Windsurf, Cline, and others
```bash
npx skills add reviseio/skills --skill revise-cli
npx skills add reviseio/skills --skill revise-editor
npx skills add reviseio/skills --skill collaborate-in-revise
```
Update with `npx skills update`.
### From the CLI
The `revise` CLI contains its own skill, matching the installed version:
```bash
revise skill install # into .agents/skills if that folder exists, else .claude/skills
revise skill install --dir ./skills
revise skill # print it
revise skill --reference library # print a reference file: library, json
```
## Machine-readable docs
- **Markdown version of any page**: add `.md` to its URL, for example
[`/developer/docs/revise/cli.md`](https://revise.io/developer/docs/revise/cli.md).
- [`llms.txt`](https://revise.io/developer/docs/llms.txt): an index of these
docs for language models.
- [`llms-full.txt`](https://revise.io/developer/docs/llms-full.txt): every page
in one document.
- The `revise --json` contract is a JSON Schema shipped in the package:
`revise/cli-output.schema.json` ([JSON output](https://revise.io/developer/docs/revise/json)).
Source: https://revise.io/developer/docs/build-with-ai
---
# Changelog
> Release history for revise, the CLI and TypeScript library.
## 3.1.7
Released with `@reviseio/editor` 3.1.7, the editor's first release in
lockstep with revise: one version and one build for all three packages. A
Y.Doc or .docx moves between the editor and revise unchanged.
### Changed
- **`remove_blocks` takes up to 200 ids** (was 50). Past the limit it removes
nothing and says how to proceed: pass a search's `search_result_id` to
remove every match, or send the ids in batches.
### Fixed
- **Page numbers next to an existing header or footer.** `set_page_layout`
with a page-number position, or a preset that numbers pages (APA puts them
top right), used to report success while a hand-authored header or footer
kept the number from appearing. The number now goes into that header's or
footer's empty zone at the requested side, in the authored text's font. When
the zone already holds text or the header already shows a page number, the
result says so and lists the headers and footers instead of claiming the
layout changed.
- **A `replace` whose `find` and `replace` are identical** fails with an error
that says so, instead of the generic "no changes detected".
## 3.1.6
### Changed
- **The agent skill knows when revise can't run.** In the Claude and ChatGPT
apps, a document attached to the chat lives in a sandbox that can't install
revise. The skill now stops before running anything there and offers the
Revise app, which opens the document as an editable editor in the chat. If
the app is connected, it uses it. Agents with a terminal are unaffected, and
an install that fails on a network error is treated the same way instead of
being worked around.
## 3.1.5
Agents finish document tasks in fewer commands. Measured on the revise
benchmark, Claude Code and Codex finish its tasks for about 40% less with
revise than without it.
### Added
- **`revise batch '[...]'`** runs several tool calls (for example a
table row and the total under it, or a `replace_all` and a `replace`) as one
edit with one save. If any step fails, nothing is saved.
- **`read --section 4,7,9`** reads several sections at once, `--blocks a..b`
reads a range of ids, and `--count N` limits how many blocks are shown.
- **`grep` takes several phrases** and shows about 80 characters around each
match instead of the whole block (at most 40 blocks).
- **`outline` lists headings typed as bold body text** first, each with the
command that turns it into a real heading.
- **PDF exports print the page count and paper size.**
### Changed
- **One `replace` call can edit several blocks:** leave out `id` and each
`find` edits the one block you've read or grepped that contains it.
- **Saves say what they rewrote:** "Rewrote N edited blocks; the rest of the
file is copied byte for byte."
- **`read` output is shorter:** it leaves out formatting that writing the
block back restores anyway (a heading's own style, default table cell
borders and padding, and the like).
- **`outline` shows one id range per section** (`first..last`) and its word
count.
- **`compare` says what differs outside the redline:** header and footer
text and page setup, or that the rest is formatting only.
- **`--dry-run` with `--out`** previews instead of failing.
- **The agent skill is a third shorter** and lists every field of `replace`
and `leave_comment`, so agents don't need to look them up.
### Fixed
- **`read --blocks a,b`** shows only those blocks, not every block between
them.
- **`outline` and `read --section`** see every section, not just the first
100.
- **Rows added with `insert_table_row`** take the alignment, spacing,
indents, and font of the row they're copied from.
## 3.1.4
### Fixed
- **The package README's heading** reads `revise`.
## 3.1.3
### Changed
- **The package README** has a plain heading and tagline. No changes to the
CLI or the library.
## 3.1.2
### Changed
- **`revise skill install` installs where the agent running it reads
skills:** `.claude/skills` from Claude Code or in a project with a
`.claude` folder, otherwise `.agents/skills`, which Codex, Cursor, Copilot,
and others read. It used to fall back to `.claude/skills`, which other
agents don't read.
- **It ends by saying to read the installed SKILL.md now:** a running agent
session doesn't load a newly installed skill; new sessions do. The
quickest install for any agent with a terminal is
`npx -y revise@latest skill install`.
## 3.1.1
### Changed
- **The agent skill is `revise-cli`.** `revise skill install` writes
`.agents/skills/revise-cli/` (or `.claude/skills/revise-cli/`) and replaces
the `revise/` folder earlier versions installed. As plugins it's
`cli@revise`: `claude plugin install cli@revise`,
`codex plugin add cli@revise`, or
`npx skills add reviseio/skills --skill revise-cli`.
## 3.1.0
### Added
- **`--blocks` on `changes`, `accept`, and `reject`.** Select tracked
changes by block: ids (`--blocks a1B2c3,d4E5f6`) or a range in document
order (`--blocks a1B2c3..d4E5f6`). Ids of list items, table cells, and
paragraphs inside them work. In the library: `{ blocks: [...] }`, with
ranges as `{ from, to }`.
- **`--view tracked|accepted|original`** on `read`, `grep`, `search`,
`export`, and `convert`. `read` now shows pending changes inline as
`` and `` with their change ids and authors; `grep` and `search`
find text on both sides of a change and say which side it's on. `export`
and `convert` with `--view original` write the document as it was before
every pending change. `--final` still means `--view accepted`.
- **`replace-all --ignore-case` and `--exact`.**
### Changed
- **`replace-text` is now `replace-all`** (`replaceAll` in the library). It
replaces in page headers and footers and in footnotes and endnotes as well
as the body, and says where. Curly and straight quotes, no-break and plain
spaces, and no-break and plain hyphens match each other unless `--exact`,
and the replacement keeps the document's characters: `"net 30"` →
`"net 45"` keeps its no-break space. Matches it leaves alone, like other
capitalization, are reported as `nearMisses`. `replace-text` and
`replaceText` still work, and JSON output run under `replace-text` still
names it.
- **`--section` is a heading and everything under it.** `--section 8`
includes 8.1 and 8.2. Numbers match whether typed or Word's automatic
numbering. A title matches in full, or by text in only one heading; text in
several headings is an error (`section_ambiguous`), and a section that
doesn't exist is `section_not_found` instead of an empty result.
- **Clearer flag names:** `new --content` (was `--from`), `new --page-size`
(was `--page`), `search --results-page` (was `--page`), and `--blocks` for
block ids on `read` and `replace-all` (was `--ids`). The old names still
work.
- **`changes` lists header, footer, and note changes as such** ("Header",
"Footnote 1"), with their text.
### Fixed
- **Word-numbered headings.** Headings numbered by Word's heading styles, how
most contracts number clauses, are headings again: `outline` lists them
with their numbers ("8.1 Cap on liability") and `--section 8` finds them.
- **Comparing numbered documents.** Two copies of the same numbered document
compare as unchanged, instead of every numbered clause deleted and
reinserted.
- **Comparing against a file revise edited.** A one-word edit compares as a
one-word change, not a whole paragraph replaced: editing a paragraph no
longer adds fonts and sizes to it or its neighbors.
- **Tracked changes in headers and footers** are saved as Word revisions, and
ones made in Word open as pending changes.
- **Markdown export** numbers nested list items per level (8.1 was written
as "9."), and writes numbered headings as headings: `## 8.1 Cap on
liability`.
## 3.0.1
### Fixed
- **Windows-1252 text on Node 20.** Text that isn't UTF-8, like a .txt file
from a Windows editor piped to `convert`, and RTF files read € “ ” ‘ ’ – —
and the other characters Windows-1252 stores in bytes 0x80–0x9F as
invisible control characters. Node 22 and later were not affected.
## 3.0.0
The first release of `revise` as a command-line tool and TypeScript library
for reading, editing, and redlining Word documents.
Versions 2.x of this package were the Revise editor for React. The editor is
now [`@reviseio/editor`](https://www.npmjs.com/package/@reviseio/editor); 2.x
stays installable.
### Added
- **The `revise` CLI.** Read a document as HTML with block ids, search it,
edit it with the same tools the Revise agent uses, and save the file in
place, as tracked changes by default in .docx. Review with `changes`,
`diff`, `accept`, and `reject`; `undo` and `restore` work across edits;
`render` draws pages as images; `export`, `convert`, `compare`, and `new`
write .docx, PDF, Markdown, HTML, and text. `convert` also works in a
pipe: `-` reads stdin (the format is recognized from its bytes) or writes
stdout. `--out new.docx` on an edit saves the result as a new file and
leaves the original as it is. Every command has its own
`--help`, and colored output on a terminal (not in pipes unless
`FORCE_COLOR` is set; never with `--json`).
- **`--json` on every command**: one envelope (`schema`, `ok`, `command`,
`meta`, `data`, `warnings`, `error`) with stable error codes, structured
partial results, and exit codes that say whether anything was saved.
Specified in [JSON output](https://revise.io/developer/docs/revise/json) and shipped as a JSON Schema,
`revise/cli-output.schema.json`.
- **`ReviseDocument`**, the same operations as a library: open a file or
bytes, run typed tools, review changes, export with a fidelity report,
and snapshot and restore state.
- **`agentTools(doc, { provider })`**: the editing tools in the shapes the
Anthropic and OpenAI SDKs expect, with a matching result formatter.
- **Collaboration rooms.** `seedYDoc(room, file)` seeds a sync server's
Y.Doc on every connection (idempotent, repairs two peers that seeded at
once, keeps the original Word file in the room), and
`ReviseDocument.fromYDoc(room)` edits and exports a live room.
- **Word fidelity.** Untouched content in a .docx exports byte for byte;
`xml` commands read and patch the raw package for what the tools don't
cover (styles, numbering, settings).
- **An agent skill**: `revise skill install` writes a SKILL.md that teaches
coding agents the CLI.
### Removed
- **The editor entry points of 2.x.** `revise/editor`, `revise/editor/full`,
and `revise/server` now throw an error pointing to `@reviseio/editor`, and
`revise/style.css` is an empty stylesheet that says the same.
Source: https://revise.io/developer/docs/revise/changelog
---
# Revise CLI
Read, edit, and redline Word documents. Available as CLI and TypeScript library.
```bash
npx revise
```
## For Agents
The `revise` CLI makes AI agents faster and cheaper.
Claude Code and Codex finish the same document tasks about 1.5-2× faster and using 30-70% less usage compared to their default `docx` toolset. See our [benchmarks on GitHub](https://github.com/reviseio/revise/deployments/benchmark).
```text
Read https://revise.io/skills/cli.md and follow it.
```
## Install
```bash
npm i -g revise
```
Global options:
- `--json`: one machine-readable object per command; see [JSON output](https://revise.io/developer/docs/revise/json).
- `--author ` (or `REVISE_AUTHOR`): sign tracked changes.
- `--no-color`: plain text. Output is colored only on a terminal, and never
with `--json`. `NO_COLOR=1`, `TERM=dumb`, and `FORCE_COLOR=0` turn it off;
`FORCE_COLOR=1` turns it on even in a pipe.
- `--help`: help for a command, e.g. `revise render --help`.
Results go to stdout. Warnings and errors go to stderr as
`warning[code]: …` and `error[code]: …`, often with a `hint:` command to run
next.
Exit codes: `0` done (including nothing to do), `1` failed and nothing was
saved, `2` partly done (some of it was saved; the error says what wasn't),
`70` internal error or a save whose outcome is uncertain.
## Reading
| Command | What it does |
| --- | --- |
| `status ` | Size, mode, pending changes, checkpoint, and how faithfully it round-trips to Word |
| `outline ` | Headings with block ranges, as `first..last` block ids, and paragraphs that look like headings but are body text |
| `read [index] [--count n]` | Blocks as HTML with block ids, from `index` on (`n` of them) |
| `read --section s` | A section, subsections included: `"8"`, `"8.1"`, its title, or several: `"4,7,9"` |
| `read --blocks a,b` | Specific blocks, or a range `first..last` |
| `grep "text" ["text" ...] [--regex] [--ignore-case]` | Blocks matching any of the phrases, with ids: a long block shows the passage around each match, and after 40 blocks it stops (`--json` has every block, whole) |
| `search "text" ["text" ...]` | Matching blocks as HTML |
`read`, `grep`, and `search` show pending tracked changes: `read` marks them
`100120 `,
and `grep` writes them `[-100-]{+120+}` and finds text on either side, saying
which matches are in deleted or inserted text. `search` adds the blocks whose
deleted text matches. `--view accepted` reads the text as if every change were
accepted (what the editing tools edit), `--view original` as if every one were
rejected. Tracked HTML pasted back into a tool edits the accepted text: ``
reads as its text and `` is left out.
## Editing
Edits save the file. To keep it as it is, add `--out new.docx` to
`replace-all`, `call`, `accept`, `reject`, or `xml patch`: the result is saved
as the new file (same format; `--force` replaces an existing one), and block
ids read from the original work on it. The copy has its own history, starting
from the original, so `undo` steps back to it.
| Command | What it does |
| --- | --- |
| `replace-all "find" "replace"` | Replace every occurrence: body, headers, footers, footnotes. `--ignore-case`; `--exact` makes quotes and spaces count; `--blocks a,b` limits it |
| `batch '[{"tool":…}, …]'` | Several edits in one save: editing tools, plus `{"tool":"replace_all","find","replace"}`. If one step fails, nothing is saved |
| `replace-text …` | The 3.0 name of `replace-all`; still works |
| `call ''` | Run an editing tool. JSON can also come from stdin (`-`) or a file (`@file`). `--direct` applies it without tracking. |
| ` ''` | The same, shorter: `revise replace f.docx '{...}'`. `revise --help` shows its input schema. |
| `tools [name]` | List the editing tools, or show one's schema |
| `mode [suggest\|edit]` | Track changes, or apply directly |
The tools are the ones the Revise agent uses: `replace`, `insert_block`,
`replace_block`, `remove_blocks`, `style_blocks`, `leave_comment`,
`insert_footnote`, table row, column, and cell tools, `set_header_footer`,
`set_page_layout`, and the read and search tools. `revise tools ` prints
a tool's JSON schema.
```bash
revise call contract.docx replace - <<'JSON'
{"id": "p-12", "replacements": [{"find": "thirty", "replace": "sixty"}]}
JSON
```
## Review
| Command | What it does |
| --- | --- |
| `changes [--section s] [--blocks b] [--details]` | Pending tracked changes: where they are and what text they change |
| `diff [--since n]` | Word-level diff of the pending changes, or since checkpoint `n` |
| `accept ` | Accept changes by id |
| `reject ` | Reject changes by id |
| `accept --all` | Every pending change (`reject` too) |
| `accept --section s` | Changes under one heading, subsections included: `"8"`, `"8.1"`, or its title |
| `accept --blocks b` | Changes in these blocks: `id,id` or a range `first..last` |
| `accept --except-section s` | Every change outside a section |
| `history ` | Checkpoints |
| `undo [steps]` | Undo the last edits |
| `restore ` | Go back to checkpoint `n` |
`--dry-run` on `accept` and `reject` lists what would be settled.
A section is a heading and everything under it until the next heading at its
level or above. Numbers work whether they're typed or Word's automatic
numbering, as `outline` shows them. A title matches when typed in full, or by
any part of it that's in only one heading; text in several headings is an
error, never a guess. `--section`, `--except-section`, and `--blocks` combine.
`read` and `replace-all` take block ids only (`--ids` is the older name).
## Output
| Command | What it does |
| --- | --- |
| `export ` | Write a copy as `.docx`, `.pdf`, `.md`, `.html`, or `.txt`. `--view accepted` accepts pending changes in the copy, `--view original` rejects them; only a `.docx` keeps them tracked. |
| `render [--page n] [--pages a-b]` | Pages as an image. `--view tracked`, `accepted`, or `original`; `--layout` overlays layout boxes. Needs a canvas package: `npm i @napi-rs/canvas` (or `canvas`). |
## Create and convert
| Command | What it does |
| --- | --- |
| `new --content draft.md` | A new document from Markdown or HTML. `--page-size letter` or `a4`, `--orientation`, `--margins 1in`, `--header text`, `--footer "Page {PAGE} of {PAGES}"` |
| `compare [--out redline.docx]` | A redline: the revised document with every difference as a tracked change |
| `convert ` | Convert between formats; `--view accepted` or `original` as for `export`. Either side can be `-` for a pipe (below). |
### Pipes
`-` as the input reads the document from stdin, and `-` as the output writes
it to stdout:
```bash
cat notes.md | revise convert - notes.docx # format recognized from the bytes
curl -s https://example.com/page | revise convert - page.docx
revise convert contract.docx - | grep -i liability # stdout is Markdown by default
revise convert contract.docx - --to html > contract.html
revise convert contract.docx - --to docx | ssh host 'cat > copy.docx'
```
On stdin, revise recognizes .docx and .odt from their zip entries, and RTF
and HTML from how they start (after any leading whitespace or comments). Any
other text reads as Markdown, which plain text already is. Text that opens
with ``, ``, ``, or `` is HTML. Text that
opens with any other tag is HTML too, unless the rest of it writes Markdown
(`#` headings, code fences, links, `*emphasis*`), like a README under its
banner; lists count as Markdown only when no HTML block is closed. Text may
be UTF-8, UTF-16, or Windows-1252, recognized or named. When a guess would be
wrong, `--from` settles it.
`--from md|txt|html|rtf|odt|docx` names the format instead, for example
`--from txt` to keep a line starting with `#` as text. `--to md|html|txt|docx|pdf`
picks stdout's format. A .docx or PDF won't print to a terminal, and `--json`
can't be combined with `-` as the output, because both would need stdout.
## Names from 3.0
These still work, so scripts written for 3.0 keep running:
| 3.0 | Now |
| --- | --- |
| `replace-text` | `replace-all` (output keeps `"command": "replace-text"` when run under the old name) |
| `read --ids`, `replace-text --ids` | `--blocks` |
| `new --from ` | `new --content ` |
| `new --page ` | `new --page-size ` |
| `search --page ` | `search --results-page ` |
## Raw package
For what the tools don't cover: style definitions, numbering, settings,
document properties.
| Command | What it does |
| --- | --- |
| `xml ls ` | The parts in the `.docx` package |
| `xml cat [--match text]` | A part's XML, or the elements matching `text` |
| `xml patch --find '' --replace '' [--all]` | Replace XML that matches once (or every match) |
| `xml patch --file new.xml` | Replace a whole part |
Patches apply directly to the file, and `undo` reverts them.
## Agents
| Command | What it does |
| --- | --- |
| `examples` | Example commands for the most common jobs |
| `skill` | Print the agent skill (SKILL.md) |
| `skill install [--dir d]` | Install it as `revise-cli/` where the agent reads skills: `.claude/skills` from Claude Code or in a project with a `.claude` folder, else `.agents/skills` (Codex, Cursor, Copilot, and others). Replaces a `revise/` folder an earlier version installed |
See [agents](https://revise.io/developer/docs/revise/agents).
Source: https://revise.io/developer/docs/revise/cli
---
# JSON output
> The --json contract: one envelope for every command.
Add `--json` to any command and `revise` prints exactly one JSON object on
stdout, and nothing else on stdout or stderr. Every command uses the same
envelope, so a script or an agent can handle every result the same way.
```bash
revise status contract.docx --json
```
```json
{
"schema": 1,
"ok": true,
"command": "status",
"meta": { "version": "3.0.0", "build": "3881be46a" },
"data": {
"file": "/work/contract.docx",
"format": "docx",
"mode": "suggest",
"blockCount": 68,
"wordCount": 2174,
"trackedChangeCount": 0,
"checkpoint": 0,
"sourceFidelity": {
"fidelity": "exact",
"regeneratedBlockIds": [],
"changedDomains": [],
"convertedReason": null
}
},
"warnings": [],
"error": null
}
```
The contract is also a JSON Schema, shipped in the package:
`revise/cli-output.schema.json`.
## The envelope
| Key | Type | Meaning |
| --- | --- | --- |
| `schema` | `1` | The version of this contract. |
| `ok` | boolean | `true` exactly when `error` is `null`. |
| `command` | string or `null` | The command that ran, by its canonical name: `status`, `xml patch`, `skill install`; `call` for the [tool shorthand](#tool-calls); `help` for `help` and any `--help`; `version` for `version` and `--version`. `null` when the command was not recognized. |
| `meta` | object | `version` (the `revise` package version) and `build` (the commit it was built from). |
| `data` | object or `null` | The command's result. Present on failure too whenever there is something to report, such as what was saved before the failure. `null` when there is nothing. Its presence does not mean success. |
| `warnings` | array | Things worth knowing that did not stop the command. |
| `error` | object or `null` | Why the command failed. |
### Compatibility
All seven keys are always present. Within schema 1, fields may be added;
existing fields keep their names, types, and meanings. Ignore fields you
don't know, and handle error and warning codes you don't know generically.
Breaking changes come with a new `schema` number and a new major version of
`revise`.
Inside `data`, every documented key is present. A value that does not apply
or is unknown is `null`; an empty list is `[]`; a zero count is `0`. Keys are
camelCase, except inside a tool's own `result` (see [Tool calls](#tool-calls)).
Counts end in `Count`. Paths are absolute; package part names (like
`word/document.xml`) are relative to the package.
The output is UTF-8 with one trailing newline. It is indented when stdout is
a terminal and on one line otherwise; both parse the same way. Don't rely on
key order.
### Exit codes
| Code | `ok` | Meaning |
| --- | --- | --- |
| `0` | `true` | Done, including when there was nothing to do. |
| `1` | `false` | Failed, and nothing was saved: a usage error, a missing file, a refused edit. |
| `2` | `false` | Partly done: some of what was asked was saved or written, and some was not. `data` says what happened; `error` says what didn't. A redline that was written but failed its check is also `2`. |
| `70` | `false` | An internal error, or a save whose outcome is uncertain (`save_uncertain`, with `details.file` and `details.saved: null`). Check the file before going on; the next revise command on it finishes or undoes the save. |
A signal, a crash while starting up, or a closed stdout can still prevent a
complete envelope.
### Errors
```json
{
"code": "change_not_found",
"message": "Not pending: prop_17915. 1 other change was accepted and saved.",
"hint": "revise changes contract.docx",
"details": { "notFound": ["prop_17915"], "failed": [] }
}
```
| Key | Meaning |
| --- | --- |
| `code` | Stable and snake_case. Branch on this. |
| `message` | One or two sentences for a person or a model. The wording may change. |
| `hint` | A command to run next, shell-quoted, or `null`. Advice, not an instruction. |
| `details` | Structured facts about the failure, or `null`. |
### Warnings
Same `code`, `message`, and `details` as errors, without `hint`. In human
output (without `--json`), warnings go to stderr as
`warning[code]: message`, and errors (partial results too) as
`error[code]: message` followed by a `hint:` line. A command that fails with
exit `1` prints nothing on stdout.
### Saving
Commands that can change the document report:
| Key | Meaning |
| --- | --- |
| `file` | The document. |
| `saved` | `true` when this command wrote the file and recorded a checkpoint. `false` when it saved nothing. |
| `checkpoint` | The checkpoint the file is at now, saved or not. Undo with `revise undo `. |
Commands that write another file report `output` (its path) and
`outputWritten`. If such a command fails after writing the file (for
example `new` when its page setup fails), it exits `2` with that receipt:
`outputWritten: true`, and `null` for what it could not determine.
`undo` and `restore` move the file back to an earlier checkpoint and discard
the later ones: they don't add a checkpoint.
## Vocabulary
| Term | Meaning |
| --- | --- |
| block | One unit of the document: a paragraph, heading, list item, table, or image. Block ids stay the same across commands until the block is removed. Block indexes start at 0. |
| tracked change | A Word revision waiting to be accepted or rejected. Each has a change id. |
| mode | `suggest` (edits become tracked changes) or `edit` (edits apply directly). |
| checkpoint | A saved state in the file's history. `0` is the file as revise first read it. |
| section | The part of the document under one heading. Not a Word page-layout section. |
## Commands
### Reading
**`status`**: `file`, `format`, `mode`, `blockCount`, `wordCount`,
`trackedChangeCount`, `checkpoint`, and `sourceFidelity`: for a `.docx`, how
an export now would relate to the file it came from (otherwise `null`):
- `fidelity`: `"exact"` (reproduces the original file byte for byte),
`"grafted"` (the original file plus your edits; untouched content is byte
for byte), or `"converted"` (written fresh).
- `regeneratedBlockIds`: blocks an export would rewrite.
- `changedDomains`: other parts it would rewrite (`"styles"`, `"numbering"`, …).
- `convertedReason`: why it is `"converted"`, or `null`.
**`outline`**: `file`, `blockCount`, `wordCount`, `sectionCount`, `sections`
(each: `title`, `level` (1–3), `startIndex`, `endIndex` (inclusive),
`startBlockId`, `endBlockId`, `blockCount`, `wordCount`), and `nextOffset`:
pass it to `--offset` for the next sections, `null` at the end.
**`grep`**: `file`, `pattern`, `regex`, `ignoreCase`, `view`, `matchCount`,
and `blocks` in document order (each: `blockId`, `blockIndex`, `matchCount`,
`text`, `matches`). `text` is the block in the view; in `tracked`, changes are
inline as `[-deleted-]{+inserted+}`. `matches` lists each match (`text`,
`change`: `"deleted"`, `"inserted"`, or `null` for text no change touches,
and `changeId`).
**`read`** and **`search`** are [tool calls](#tool-calls).
### Editing
**`replace-all`**: `file`, `find`, `replace`, `matchedBlockIds` (blocks that
contain `find`), `replacedOccurrences`, `regions` (occurrences per part:
`body`, `header`, `footer`, `footnote`, `endnote`; parts with none left
out), `nearMisses` (text looser matching would have replaced; each: `text`,
`count`, `reason`: `case` or `characters`), `changeIds` (tracked changes
created), `failures` (blocks that could not be edited; each: `blockId`,
`code`, `message`), `saved`, `checkpoint`. Some failures: exit `2`. All
failed: exit `1`. Run as `replace-text`, its 3.0 name, the envelope's
`command` is `replace-text`.
**`mode`**: `file`, `mode`, `changed` (whether this command switched it). The
mode is revise's setting, not part of the file.
**`tools`**: `tools` (each: `name`, `summary`). **`tools `**: `name`,
`description`, `inputSchema`.
### Review
**`changes`**: `file`, `section` and `blocks` (the filters, or `null`),
`trackedChangeCount`, and `changes` in document order (each: `changeId`,
`type`, `author`, `blockId`, `blockIndex`, `section`, `deleted`, `inserted`).
`deleted` and `inserted` are the full text, never shortened.
**`accept`**, **`reject`**: `file`, `action`, `dryRun`, `resolved` (change
ids accepted or rejected), `wouldResolve` (with `--dry-run`: what would be;
otherwise `[]`), `notFound` (requested ids that are not pending), `failed`
(pending changes that could not be resolved; each: `changeId`, `code`,
`message`), `saved`, `checkpoint`. If some ids were not found or failed while
others were resolved: exit `2`. If none were resolved: exit `1`.
**`diff`**: `file`, `since` (the `--since` checkpoint, or `null` for the
pending tracked changes), `changedParagraphCount`, `text`. `text` is for
reading: `[-removed-]` and `{+added+}` inline, which can be ambiguous if the
document itself contains those markers.
**`history`**: `file`, `checkpoint` (current), `checkpoints` (each:
`checkpoint`, `at` (ISO 8601, UTC), `command`, `summary`).
**`undo`**, **`restore`**: `file`, `checkpoint` (now), `from` (before),
`discarded` (checkpoint numbers removed), `saved`.
### Output
**`render`**: `file`, `output` (the image), `outputWritten`, `mimeType`,
`width`, `height`, `pages` (1-based), `totalPages`, `view`, `description`,
`placeholderImageCount`, `failedImageCount`.
**`export`**: `file`, `output`, `outputWritten`, `format`, `byteCount`,
`final` (whether the output shows no tracked changes: only a `.docx` can
hold them, so every other format is `true`), `view` (`tracked`, `accepted`,
or `original`: what the output shows),
`trackedChangeCount` (tracked changes in the written file, as revise reads it
back; Word stores a replacement as a deletion and an insertion, so this can
be higher than the source's count; `0` for other formats; `null` if a `.docx`
could not be read back), `docxExport`, and `summary`. For `.docx` output, `docxExport` says what the export did:
`method` (`"graft"`: untouched content copied from the original file;
`"regenerated"`: written fresh), `exact`, `regeneratedBlockCount`,
`fallbackCode` (why it regenerated, or `null`). `summary` describes the
written `.docx` (read back from it). Both are `null` for other formats.
### Create and convert
**`new`**: `output`, `outputWritten`, `format`, `mode`, `checkpoint`,
`summary`. `checkpoint` and `summary` are `null` when setting up the file
failed after it was written (exit `2`). Page options are checked before
anything is written.
**`convert`**: `input`, `inputFormat`, `output`, `outputWritten`, `format`,
`byteCount`, `final`, `view` (as for `export`). `input` is `null` when the
document came from stdin
(`-`); `inputFormat` is the format read (`docx`, `odt`, `rtf`, `html`, `md`,
or `txt`), from `--from`, from stdin's bytes, or from the input's extension.
Writing to stdout (`-` as the output) can't be combined with `--json`.
**`compare`**: `original`, `revised`, `output`, `outputWritten`, `author`,
`inlineChangeCount`, `insertedBlockCount`, `deletedBlockCount`,
`unmarkedBlockCount` (changed blocks that can't carry a tracked change),
`untrackedRegions` (parts that differ but can't be tracked, taken from the
revised file), and `verification`: `scope` (`"normalized_text"`),
`acceptAllMatchesRevised`, `rejectAllMatchesOriginal`. The check compares
text with whitespace normalized; it does not check formatting or comments. A
redline that fails it is still written: exit `2`, `redline_unverified`.
**`summary`** (in `new` and `export`): `pageCount` (`null` when not
measured), `wordCount`, `pageSize`, `orientation`, `pageInches` (`width`,
`height`), `marginsInches` (`top`, `right`, `bottom`, `left`), `title`,
`headingCounts` (`{"H1": 1, "H2": 4}`), `lists` (each: `itemCount`), `tables`
(each: `rowCount`, `columnCount`), `imageCount`, `headersFooters` (each:
`side`, `slot`, `text`), `trackedChangeCount`.
### Raw package
**`xml ls`**: `file`, `parts` (each: `path`, `byteCount`).
**`xml cat`**: `file`, `part`, `match`, `fragments` (the distinct elements
that contain `match`: several matches in one paragraph show it once),
`truncated` (more than 20 such elements; the first 20 are shown).
**`xml patch`**: `file`, `part`, `replacementCount`, `saved`, `checkpoint`,
`copiedFrom`.
**`copiedFrom`** (`replace-all`, `call`, `accept`, `reject`, `xml patch`):
with `--out`, the original the copy was made from, and `file` is the copy the
edit was saved to; otherwise `null`. When the edit fails outright (exit `1`),
no copy is left behind.
### Everything else
**`examples`**: `examples`, each `{ description, command }`, plus `docs`
(a page to read more) on some.
**`skill`**: `markdown`. **`skill install`** (its own command name):
`output`, `outputWritten`.
**`help`** and **`--help`**: `commands` and `globalOptions`, or `command` for
one command's help (each command: `name`, `usage`, `summary`, `options`).
**`version`**: `version`, `build`.
## Tool calls
`call`, the tool shorthand (`revise replace contract.docx '{…}'`), `read`,
and `search` run an [editing tool](https://revise.io/developer/docs/revise/agents) and report:
| Key | Meaning |
| --- | --- |
| `file`, `saved`, `checkpoint` | As for every command that can change a document. |
| `tool` | The tool that ran. |
| `message` | The tool's summary, for reading; may span lines. `null` if none. |
| `changeIds` | Tracked changes this call created. An edit that extended an existing change creates none, so an empty list does not mean nothing changed: check `saved`. |
| `html` | The blocks the call returned, as HTML with block ids, or `null`. |
| `result` | The tool's own structured result, or `null`. Its keys are the tool's own and include snake_case ids such as `search_result_id`, which later calls take as input. |
| `view` | `read` and `search` only: the view `html` shows (`tracked`, `accepted`, or `original`). |
| `viewMatches` | `search` only: blocks the search tool can't see in the view (each: `query`, `blockId`, `blockIndex`, `text`, `changeIds`). Tracked: matches inside deleted text. Original: every match in the original text, and `html` and `result` are `null` (no `search_result_id`). |
A `search_result_id` stays valid across commands until the document changes.
revise remembers the 20 most recent searches per file; an older id is
refused with `unknown_search_result`.
When a tool refuses a call, the command exits `1` with `error.code`
`tool_error`, the tool's own code in `error.details.toolCode`, and a `data`
receipt showing `saved: false`.
## Error codes
| Code | When |
| --- | --- |
| `usage` | Wrong arguments, a missing argument, or an unknown option. |
| `unknown_command`, `unknown_tool` | No such command or tool. |
| `file_not_found` | A file the command needs does not exist: the document, an input, `--from`, or `@file.json`. |
| `checkpoint_not_found` | No such checkpoint. |
| `unsupported_format` | revise can't read or write that format. |
| `read_only_format` | revise reads the format but can't save it in place. |
| `import_failed`, `read_failed` | The file did not open or could not be read. |
| `would_overwrite` | The output exists; add `--force`. |
| `same_file` | The output is the input. |
| `file_busy` | Another revise command held the file too long. |
| `file_changed` | Another program saved the file while revise worked on it. Nothing was written. |
| `write_failed`, `commit_failed` | The file or its history could not be written. The document was left as it was; a file `new` had already created is reported with `outputWritten: true` (exit `2`). |
| `io_error` | The system refused a read or write (permissions, a full disk). `details`: `errno`, `syscall`. |
| `save_uncertain` | A save failed and could not be rolled back. Exit `70`. The next command on the file finishes or undoes it. |
| `invalid_regex` | `grep --regex` got a pattern that does not compile. |
| `invalid_json` | Tool input is not a JSON object. |
| `invalid_selection` | `accept`/`reject` got a selection that can't be combined, or a block range written last..first. |
| `section_not_found` | `--section` or `--except-section` matched no heading. The message lists the headings. |
| `section_ambiguous` | The `--section` text is in more than one heading. Name one by its number or full title. |
| `block_not_found` | `--blocks` named a block id the document doesn't have. |
| `tool_error` | A tool refused the call; see `details.toolCode`. |
| `unknown_search_result`, `stale_search_result` | The search result id is unknown, or the document changed since that search. |
| `change_not_found` | Some requested change ids are not pending. |
| `change_not_resolved` | Some pending changes could not be accepted or rejected. |
| `replace_failed` | Some blocks could not be edited. |
| `redline_unverified` | The redline was written but failed its check. |
| `page_setup_failed` | `new` wrote the file, but applying the page setup failed. |
| `no_package` | The file is not a `.docx`, so it has no package XML. |
| `no_such_part` | No such part in the package. |
| `malformed_xml` | A patch would produce XML that does not parse. |
| `find_not_found`, `find_ambiguous` | `xml patch --find` matched nothing, or more than once without `--all`. |
| `body_shape_changed` | `xml patch` on `word/document.xml` would add or remove paragraphs; use the tools. |
| `canvas_missing` | Rendering needs the optional `@napi-rs/canvas` package. |
| `render_failed`, `page_out_of_range`, `too_large` | Rendering failed, the page does not exist, or the image would be too big. |
| `internal` | A bug. Run again with `REVISE_DEBUG=1` for a stack trace on stderr. |
## Warning codes
| Code | When |
| --- | --- |
| `file_changed_outside` | The file changed since revise last saw it, and revise re-read it. `details`: `keptBlockCount`, `changedBlockIds`, `newBlockCount`, `removedBlockIds`. Read changed and new blocks again before editing them. |
| `save_recovered` | An earlier command was interrupted after writing the file; revise finished recording it. |
| `changes_accepted` | The output format can't hold tracked changes (anything but `.docx`), so it shows them accepted. `details`: `trackedChangeCount`. |
| `font_fallback` | A PDF used fallback fonts. `details`: `fonts`. |
| `untracked_regions` | `compare`: parts that differ but can't carry tracked changes. `details`: `regions`. |
| `summary_unavailable` | `export` wrote the file but could not read it back to describe it. |
| `export_note`, `engine_warning` | Anything else the export or engine reported. |
Source: https://revise.io/developer/docs/revise/json
---
# Library
> ReviseDocument: open, read, edit, review, and export documents from Node.
```bash
npm i revise
```
```ts
import { writeFile } from "node:fs/promises";
import { ReviseDocument } from "revise";
const doc = await ReviseDocument.open("contract.docx", { author: "Legal bot" });
await doc.replaceAll("Provider", "Northstar"); // tracked changes
await doc.reject({ section: "8" });
const { bytes, fidelity } = await doc.export({ format: "docx" });
await writeFile("contract-out.docx", bytes);
doc.dispose();
```
The `revise` CLI is built on this API. ESM only; Node 20 or newer.
## Opening a document
| | |
| --- | --- |
| `ReviseDocument.open(source, options?)` | A file path, or `{ bytes, filename }`: `.docx`, `.odt`, `.rtf`, `.md`, `.html`, or `.txt`. A Word file keeps its original package, so untouched content exports byte for byte. |
| `ReviseDocument.create({ html?, markdown?, title? }, options?)` | A new document from the HTML the tools read and write, or from Markdown |
| `ReviseDocument.restore(snapshot, options?)` | From `doc.snapshot()`, or a bare Y.js update |
| `ReviseDocument.fromYDoc(ydoc, options?)` | Wrap a Y.Doc you own, such as a [collaboration room](https://revise.io/developer/docs/revise/collaboration). The Y.Doc stays yours. |
Options:
| Option | |
| --- | --- |
| `author` | Shown as the author of tracked changes and comments |
| `mode` | `"suggesting"` records edits as tracked changes (the default for Word files); `"editing"` applies them directly (the default for plain text) |
| `documentId` | A stable id, for tool sessions and collaboration |
| `images` | How images inserted by URL are fetched. By default only public http(s) hosts are reachable. |
A document runs its operations one at a time, in the order you call them, so
concurrent calls are safe. Call `dispose()` when you're done.
## Reading
```ts
const status = await doc.status(); // blocks, words, pendingSuggestions, fidelity
const outline = await doc.outline(); // sections with block ranges and word counts
const summary = await doc.summary(); // page setup, headers and footers, headings, tables
const hits = doc.grep("indemnif", { ignoreCase: true }); // [{ id, blockIndex, count, text, matches }]
```
Pending tracked changes read in a view: `"tracked"` (the default for
`grep`) finds text on either side of a change, writes the block as
`"Fees are $[-100-]{+120+} per hour."`, and labels each match `"deleted"`,
`"inserted"`, or `null`; `"accepted"` and `"original"` read the text as if every
change were accepted or rejected. The read tools return the accepted view;
`doc.viewHtml(html, "tracked")` marks their HTML with ``/`` (each
with `data-change-id` and `data-author`) and `"original"` shows the text before
the changes.
`status().fidelity` says how faithfully the document will export back to its
original Word file.
## Editing
`replaceAll(find, replace, options?)` replaces every occurrence: in the
body, tables, and lists, in page headers and footers, and in footnotes and
endnotes. Curly and straight quotes, no-break and plain spaces, and
no-break and plain hyphens match each other, so `'"Provider"'` finds
`“Provider”`; a straight quote in the replacement then takes the document's
curly style. Options: `ignoreCase`, `exact` (characters must match exactly),
`blocks` (only these block ids). It returns `replaced` (occurrences),
`blocks` (ids of the blocks that contained the text), `regions`
(occurrences per part: `body`, `header`, `footer`, `footnote`, `endnote`),
`changeIds` (tracked changes created), `failures` (each: `blockId`, `code`,
`message`), and `nearMisses`: text it left alone that `ignoreCase` or
dropping `exact` would have matched (each: `text`, `count`, `reason`).
`replaceText` is its 3.0 name and still works. For everything else, run the
editing tools:
```ts
const result = await doc.tools.call("replace", {
id: hits[0].id,
replacements: [{ find: "thirty", replace: "sixty" }],
});
if (!result.ok) console.error(result.error.code, result.error.message);
```
| | |
| --- | --- |
| `doc.tools.definitions()` | Every tool's name, description, and JSON input schema |
| `doc.tools.call(name, input, { directMode? })` | Run a tool with typed input. Literal names infer the input and result types. |
| `doc.tools.run(name, input, { direct? })` | Run a tool by name with untyped input, as a model sends it |
Neither throws for a bad edit: both return `{ ok: true, value }` or
`{ ok: false, error }`. A mutation's `value.suggestionIds` lists the tracked
changes it created. The tools are described in the [tool
reference](https://revise.io/developer/docs/editor-sdk/api/agent-tools); `revise` runs the server subset (no
`get_selection` or `view_image`, no `document_id`).
`await doc.setMode("editing")` switches to direct edits; `"suggesting"`
switches back.
## Review
```ts
const changes = await doc.changes(); // [{ id, type, author, section, deleted, inserted }]
const inSection8 = await doc.changes({ section: "8" });
const preview = await doc.preview({ exceptSection: "Fees" }); // what a selection would settle
const accepted = await doc.accept({ ids: [changes[0].id] });
const rejected = await doc.reject({ all: true });
// { resolved, missing, unresolved }
const diff = await doc.diff(); // text: "~ a [-old-]{+new+} b", changed: number
```
A selection is exactly one of `ids`, `all`, or filters, which combine:
- `section`: a heading and its subsections, by number (`"8"`, `"8.1"`, Word's
automatic numbering included) or title. A title is matched in full, or by
text in only one heading; otherwise it throws `section_ambiguous`.
- `exceptSection`: everything outside one.
- `blocks`: block ids and `{ from, to }` ranges in document order. Ids of
list items, table cells, and blocks inside them work.
A section or block that doesn't exist throws (`section_not_found`,
`block_not_found`) rather than selecting nothing.
## Output
```ts
const docx = await doc.export({ format: "docx" }); // review: keeps tracked changes
const clean = await doc.export({ format: "docx", view: "accepted" }); // or mode: "final"
const before = await doc.export({ format: "docx", view: "original" }); // every change rejected
const pdf = await doc.export({ format: "pdf" });
```
Formats: `docx`, `pdf`, `markdown`, `html`, `txt`. The result:
| Field | |
| --- | --- |
| `bytes` | The file |
| `mode` | `"review"` or `"final"`, as written (only `.docx` holds tracked changes; other formats are always final) |
| `view` | `"tracked"`, `"accepted"`, or `"original"`, as written |
| `pendingTrackedChanges` | Tracked changes still in the output |
| `fidelity` | For `.docx`: `method: "graft"` carried untouched content over from the original byte for byte (`exact` when nothing changed); `"regenerated"` wrote the file fresh, with `fallbackCode` saying why |
| `warnings` | Anything the output can't hold |
`doc.renderPages({ startPage, pageCount, view })` draws pages as one PNG,
laid out the way the editor lays them out. It needs a canvas package:
`npm i @napi-rs/canvas` (or `canvas`).
## State
| | |
| --- | --- |
| `doc.snapshot()` | The whole document, versioned, including the original Word package: for storing and restoring with `revise`. It is not a Y.js update. |
| `ReviseDocument.restore(snapshot)` | Back from a snapshot |
| `doc.version()` | An opaque value that changes whenever the document does |
| `doc.reload(bytes, filename)` | Re-read a file that changed elsewhere. Block ids carry over only where identity is provable; `changedIds` lists kept ids whose content changed. |
| `doc.ydoc` | The live Y.Doc. To hand the document to `@reviseio/editor`, send `Y.encodeStateAsUpdate(doc.ydoc)` and apply it with `Y.applyUpdate` on the editor's side (or share the Y.Doc over your collaboration provider). |
| `doc.resetToolContext()` | Forget what an agent has read (after compacting its transcript) |
## Comparing and converting
```ts
import { compare, convert } from "revise";
const redline = await compare("v1.docx", "v2.docx");
// redline.document: the revised version with every difference tracked
// redline.verified: { acceptAllGivesRevised, rejectAllGivesOriginal }
const { bytes } = await redline.document.export({ format: "docx" });
const markdown = await convert("brief.docx", "markdown"); // Uint8Array
```
## The raw package
For what the tools don't cover, read and patch the `.docx` parts directly:
```ts
import { readFile } from "node:fs/promises";
import { catPart, listParts, patchPackage } from "revise";
const bytes = new Uint8Array(await readFile("contract.docx"));
const parts = await listParts(bytes); // [{ path, bytes }]
const styles = await catPart(bytes, "word/styles.xml", 'w:styleId="Heading2"');
const patched = await patchPackage(
bytes,
{ part: "word/settings.xml", find: "", replace: "" },
(path) => "",
);
// patched.bytes, patched.replacements
```
## Errors
Expected failures (a missing file, an unsupported format, a malformed patch)
throw `ReviseError`, with a stable machine-readable `code`. Tool calls never
throw for a bad edit; they return `{ ok: false, error }`.
Source: https://revise.io/developer/docs/revise/library
---
# Agents
> Give a model Revise's editing tools, or teach a coding agent the CLI.
## Your own agent loop
`agentTools(doc, { provider })` returns the editing tools in the shape a model
API expects, and a `handle` function that runs one tool call and returns the
tool-result message to send back. No model SDK is a dependency: plain objects
in, plain objects out.
```ts
import Anthropic from "@anthropic-ai/sdk";
import { ReviseDocument, agentTools } from "revise";
const anthropic = new Anthropic();
const doc = await ReviseDocument.open("contract.docx", { author: "Contract bot" });
const agent = agentTools(doc, { provider: "anthropic" });
const messages: Anthropic.MessageParam[] = [
{ role: "user", content: "Change the governing law to New York." },
];
for (;;) {
const reply = await anthropic.messages.create({
model: "claude-sonnet-5-5",
max_tokens: 4096,
tools: agent.tools,
messages,
});
messages.push({ role: "assistant", content: reply.content });
const calls = reply.content.filter((block) => block.type === "tool_use");
if (calls.length === 0) break;
messages.push({
role: "user",
content: await Promise.all(calls.map(agent.handle)),
});
}
const { bytes } = await doc.export({ format: "docx" });
doc.dispose();
```
| Provider | `tools` | `handle(call)` takes | and returns |
| --- | --- | --- | --- |
| `"anthropic"` | Messages API tools | a `tool_use` content block | a `tool_result` block |
| `"openai"` | Chat Completions function tools | an entry of `message.tool_calls` | a `role: "tool"` message |
| `"openai-responses"` | Responses API function tools | a `function_call` output item | a `function_call_output` item |
With the OpenAI Responses API, append every output item the model returned
(reasoning and `function_call` items included) to your input, then the
`function_call_output` items `handle` returns, and call again until the model
stops calling tools.
Options:
| Option | |
| --- | --- |
| `include`, `exclude` | Only these tools, or every tool except these |
| `maxResultChars` | Cap on one result's text (default 100,000). Longer results end with a note telling the model how to page. |
`handle` never throws for a bad call: the model gets the error and can
correct itself. Calls run on the document's one tool session, so reads,
searches, and edits share context across turns, and parallel tool calls queue.
The session remembers what the model has read: when you compact or drop the
transcript, call `agent.reset()`. `agent.version()` changes whenever the
document does, so you can tell whether a turn edited it.
Edits are tracked changes unless you open the document with
`mode: "editing"`. Review them with [`doc.changes()`](https://revise.io/developer/docs/revise/library#review),
or let a person review them in [`@reviseio/editor`](https://revise.io/developer/docs/editor-sdk/introduction):
pass the document as Y.js state (`Y.encodeStateAsUpdate(doc.ydoc)`), or
export a .docx with tracked changes and open that.
## Coding agents
Coding agents (Claude Code, Codex, and others) can drive the CLI directly. The
agent skill teaches them how. To install it as a plugin for your agent, or
with one pasted line, see [Build with AI](https://revise.io/developer/docs/build-with-ai). From the CLI:
```bash
revise skill install # .claude/skills/ for Claude Code, else .agents/skills/
revise skill install --dir ./skills
revise skill # print it
```
It covers reading by block id, editing with tools, reviewing, the `xml`
escape hatch, and the exit codes.
Source: https://revise.io/developer/docs/revise/agents
---
# Collaboration
> Seed, edit, and export live Yjs rooms that browsers are editing.
Every Revise document is a [Yjs](https://yjs.dev) document. A sync server
that relays a room's updates can also seed the room from a file, edit it, and
export it, while people edit the same room in
[`@reviseio/editor`](https://revise.io/developer/docs/editor-sdk/guides/collaboration). Use the same version of
both; `revise` 3 also works with rooms that `@reviseio/editor` 2.3 clients
edit.
## Seeding a room
`seedYDoc(ydoc, source)` writes a document into an empty room and returns
whether it wrote anything. A room that already holds a document is left
alone, so call it on every connection. Two peers that both seeded an empty
room before syncing are repaired to one copy.
With [Hocuspocus](https://tiptap.dev/docs/hocuspocus):
```ts
import { Server } from "@hocuspocus/server";
import { seedYDoc } from "revise";
const server = new Server({
port: 1234,
async onLoadDocument({ document, documentName }) {
// Load stored state first if you persist rooms, then:
await seedYDoc(document, `documents/${documentName}.docx`);
},
});
await server.listen();
```
Clients join with `seed: "never"` and no source file, so only the server ever
seeds. A Word file is stored in the room along with its content, so any
session over the room, on the server or in the editor, exports untouched
content byte for byte.
## Editing a live room
```ts
import { ReviseDocument } from "revise";
import type * as Y from "yjs";
async function reviewRoom(room: Y.Doc): Promise {
const doc = await ReviseDocument.fromYDoc(room, { author: "Review bot" });
try {
const [hit] = doc.grep("Delaware");
if (!hit) return [];
const edit = await doc.tools.call("replace", {
id: hit.id,
replacements: [{ find: "Delaware", replace: "New York" }],
});
return edit.ok ? (edit.value.suggestionIds ?? []) : [];
} finally {
doc.dispose(); // the room stays alive
}
}
```
Edits are tracked changes by default, so they appear in every open editor as
suggestions for a person to accept or reject. Peers receive them as ordinary
Yjs updates, and their carets stay in place.
## Exporting a live room
```ts
import { ReviseDocument } from "revise";
import type * as Y from "yjs";
async function downloadRoom(room: Y.Doc): Promise {
const doc = await ReviseDocument.fromYDoc(room);
try {
const { bytes } = await doc.export({ format: "docx" });
return bytes;
} finally {
doc.dispose();
}
}
```
`fromYDoc` never replaces or destroys your Y.Doc: `dispose()` releases only
the tool session, and `reload()` is not available on a room.
## Without a live transport
A request handler can take a client's document as one encoded update, edit
it, and return only the delta, which merges into the live document like any
peer's edit:
```ts
import * as Y from "yjs";
import { ReviseDocument } from "revise";
async function review(update: Uint8Array): Promise {
const doc = await ReviseDocument.restore(update);
try {
const baseline = Y.encodeStateVector(doc.ydoc);
await doc.replaceAll("Delaware", "New York");
return Y.encodeStateAsUpdate(doc.ydoc, baseline);
} finally {
doc.dispose();
}
}
```
Source: https://revise.io/developer/docs/revise/collaboration
---
# Introduction
> A word processor component for React applications.
The [Revise Editor SDK](https://revise.io/sdk) is a browser-only React component
that embeds a word processor in your application. Your application keeps its
own shell, authentication, storage, and users.
You give the SDK a document (`.docx`, `.odt`, `.rtf`, Markdown, plain text, or
HTML) and get a `.docx` back. The component:
- parses the file with Revise's converters, keeping comments and tracked
changes on the DOCX path;
- opens a [Yjs](https://yjs.dev)-backed editing session. Every document is a
CRDT, so real-time collaboration over your own transport needs no extra
setup;
- renders the document on canvas with pagination, with the built-in ribbon or
your own toolbar;
- keeps comments and tracked changes through the round trip;
- exposes Revise's document-local JSON-schema tools, so the model you already
run can read and edit the document;
- exports to `.docx`.
Server-side work is [`revise`](https://revise.io/developer/docs/revise/introduction), the MIT CLI and
TypeScript library built on the same engine and released at the same
version. Your server can join the same collaborative documents: seed rooms
before anyone opens them, edit live documents headlessly, propose tracked
changes for a user to review in the browser, and export to Word. All of this
works on a host-owned Y.Doc, with no React mount and no model. Browser and
server share one tool contract: the same tools, schemas, and
`{ ok, value | error }` results. See
[collaboration](https://revise.io/developer/docs/editor-sdk/guides/collaboration#server-editing) and the
[`revise` library](https://revise.io/developer/docs/revise/library).
## What it does not include
The SDK does not provide accounts, authentication, document storage,
collaboration servers, tenancy, billing, or an application shell. It is a
component, not a platform. Nothing leaves the browser unless you connect an
agent.
For multi-user editing, pass a Yjs provider (Hocuspocus, y-websocket, or your
own). The document then shows remote carets and presence. The SDK never
opens a connection or stores anything. There is no document locking, and
roles are enforced on the client, not the server. See
[collaboration](https://revise.io/developer/docs/editor-sdk/guides/collaboration) and [roles](https://revise.io/developer/docs/editor-sdk/guides/roles).
Install it with `npm i @reviseio/editor`. The SDK is closed-source and free for
evaluation, personal projects, and education. Production use by businesses,
nonprofits, and government requires a [license](https://revise.io/developer/docs/editor-sdk/license).
## Two API surfaces
The SDK has one API for your application and one for your agent.
Component props, callbacks, and the typed controllers on
`ReviseEditorHandle`. Subscriptions, focus management, `File` and `Blob`
objects, custom toolbars and panels. See the [handle
reference](https://revise.io/developer/docs/editor-sdk/api/handle).
JSON-schema tools with serializable inputs and outputs. They operate on
semantic document state, not UI geometry, and run without React state or
browser focus. See [agent tools](https://revise.io/developer/docs/editor-sdk/guides/agent-tools).
Some capabilities exist on both surfaces, such as selection, with different
shapes for each. Interactive state such as review navigation and the comment
panel is host-only. Agents get semantic mutation tools instead.
## Use cases
Products that edit contracts, not only render them.
Apps that replace a round trip through Microsoft Word.
Products where every change must be attributable and reversible.
Products whose workflow ends in "download the .docx".
Start with the [quickstart](https://revise.io/developer/docs/editor-sdk/quickstart). Then read
[architecture](https://revise.io/developer/docs/editor-sdk/concepts/architecture) for what the component owns
and what you own.
Source: https://revise.io/developer/docs/editor-sdk/introduction
---
# Installation
> Install from npm. Free for evaluation, personal projects, and education.
The SDK is the `@reviseio/editor` package on the public npm registry.
Installing it requires no account or token.
```bash
npm i @reviseio/editor
```
npm and pnpm install the peer dependencies (React, `yjs`, `y-protocols`)
automatically. On Yarn, list them: `yarn add @reviseio/editor yjs y-protocols`.
For server-side work, also install [`revise`](https://revise.io/developer/docs/revise/introduction) at the same
version: `npm i revise@3.1.7`.
`@reviseio/editor` is free for evaluation, personal projects, and education. Production
use by businesses, nonprofits, and government requires a
[commercial license](https://revise.io/developer/docs/editor-sdk/license). Building and prototyping are free.
## Entry points
| Import | What it is |
| ---------------------------- | ------------------------------------------------------------------------------------------------------------ |
| `@reviseio/editor` | The React editor. `import ReviseEditor from "@reviseio/editor"`. |
| `@reviseio/editor/full` | The same editor plus Tree-sitter syntax highlighting and the WASM hot paths. Not CSP-safe; opt in if needed. |
| `@reviseio/editor/style.css` | The stylesheet. Import it once. |
| `@reviseio/editor/version` | Build identity (`REVISE_SDK_VERSION`, `REVISE_SDK_BUILD`) alone. No runtime weight. |
## Peer dependencies
React 18 or newer, `yjs`, and `y-protocols` are required peers. The SDK does
not bundle its own copies. Every editor session uses Yjs, with or without a
collaboration provider.
| Package | Needed for |
| -------------------- | ----------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `yjs`, `y-protocols` | All editor sessions; [collaboration](https://revise.io/developer/docs/editor-sdk/guides/collaboration) also passes host-created Yjs objects, so your bundler must resolve exactly one copy |
| `revise` | Optional. Server-side work: conversion, agent tools over a shared Y.Doc, seeding collaboration rooms. Install the same version as the editor. |
The editor never loads `revise`; a browser-only app doesn't need it. Server
code imports `revise` and doesn't need React. `revise` needs Node 20 or newer
and brings its own DOM implementation, so there is no `jsdom` to install.
## Bundle size
The editor is a word processor, with DOCX, layout, and PDF export, and its main
chunk is about 2.7 MB minified (about 800 KB gzipped). Load it where a document
opens rather than in your entry bundle; see [load on
demand](https://revise.io/developer/docs/editor-sdk/quickstart#load-on-demand). Converters, PDF export, math,
and locales load separately, when first used.
Vite warns about any chunk over 500 kB. For the editor that warning is
expected. Raise the limit if you want a clean build:
```ts vite.config.ts
export default defineConfig({
build: { chunkSizeWarningLimit: 3000 },
});
```
## Versioning
The package follows semantic versioning. Pin what you ship:
```json package.json
{
"dependencies": {
"@reviseio/editor": "3.1.7"
}
}
```
Use an exact version, not a range. The package ships a `CHANGELOG.md`. Read
it before changing versions. Breaking changes to the component props or the
handle controllers ship only in major versions.
## Migrate from version 2
The version jumps from 2.3.2 to 3.1.7 because the editor now releases in
lockstep with `revise`: one version, built from one commit. 2.x stays
installable and gets fixes on the 2.x line
(`npm i @reviseio/editor@v2`).
Version 3 removes the editor's `/server` entry point. Server-side work moved to
[`revise`](https://revise.io/developer/docs/revise/library), the MIT library built on the same engine and
released at the same version. Install it (`npm i revise@3.1.7`) and replace
the server imports:
| 2.3 `/server` entry point | `revise` |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `createServerDocumentSession(ydoc)` | `ReviseDocument.fromYDoc(ydoc)`, then `doc.tools` |
| `parseDocument`, `documentToDocx`, `fileToYDoc` | `ReviseDocument.open(file)`, `doc.export({ format: "docx" })`, `doc.ydoc` |
| `exportDocument(room, { mode })`, `ydocToDocx(room)` | `ReviseDocument.fromYDoc(room)`, then `doc.export({ format, mode })` |
| `seedYDocFromFile(room, bytes, filename)` | `seedYDoc(room, { bytes, filename })` |
| `encodeYDoc`, `decodeYDoc` | Y.js itself: `Y.encodeStateAsUpdate(doc.ydoc)`; `ReviseDocument.restore(update)` opens a bare update |
The browser entry points and the component API are unchanged. Documents move
between the two unchanged: a Y.Doc the editor edits is a Y.Doc `revise` opens,
and the reverse. The 2.3 docs, `/server` included, stay available from the
version menu.
Three differences when porting server code:
- `revise` methods are async: `await` them.
- Tools return `{ ok, value }` or `{ ok, error }` instead of throwing, so check
`result.ok` and read `result.value`.
- `doc.export()` returns `{ bytes, fidelity, … }`: write `.bytes`.
`doc.snapshot()` is `revise`'s own storage format (it carries the original Word
file and the document's settings), not a Y.js update. Anything that speaks
Y.js, such as a sync server or the editor, takes `Y.encodeStateAsUpdate(doc.ydoc)`.
## Migrate from `revise`
Through 2.2, the SDK was published as `revise`. From 2.3 it is
`@reviseio/editor`: the same editor, license, and API under a new name. From
3.0, `revise` is a different package: the MIT CLI and server-side library.
Install the editor and update imports:
```bash
npm i @reviseio/editor
```
If you used `revise/server`, upgrade `revise` to the editor's version
(`npm i revise@3.1.7`); otherwise uninstall it.
| Before (`revise` 2.x) | After |
| ---------------------- | ---------------------------- |
| `revise/editor` | `@reviseio/editor` |
| `revise/editor/full` | `@reviseio/editor/full` |
| `revise/server` | `revise` 3.x |
| `revise/style.css` | `@reviseio/editor/style.css` |
| `revise` (version) | `@reviseio/editor/version` |
Server code then follows [Migrate from version
2](#migrate-from-version-2); everything else is unchanged. If your bundler config names the package (for example
Vite's `optimizeDeps.exclude`), rename it there too. The 2.2 and earlier docs
stay available from the version menu.
## Migrate from `@reviseio/sdk`
The private `@reviseio/sdk` package is retired. Replace the dependency with
`@reviseio/editor`, delete the `@reviseio` registry and token lines from
`.npmrc` (`@reviseio/editor` is on the public registry), and update imports:
| Before | After |
| ------------------------- | ---------------------------- |
| `@reviseio/sdk` | `@reviseio/editor` |
| `@reviseio/sdk/full` | `@reviseio/editor/full` |
| `@reviseio/sdk/backend` | `revise` |
| `@reviseio/sdk/style.css` | `@reviseio/editor/style.css` |
The named `ReviseEditor` export still works alongside the default export.
## Report a problem
Include the SDK version and build. Both are exported, so your error reporting
can attach them:
```ts
import { REVISE_SDK_VERSION, REVISE_SDK_BUILD } from "@reviseio/editor/version";
// "3.1.7", "f7dc7173"
```
`npm bugs @reviseio/editor` opens the [support](https://revise.io/developer/docs/editor-sdk/support) route.
## Assets
The SDK loads some static assets on demand instead of bundling them:
- PDF export fonts, including the CJK and world-script faces
- math fonts and stylesheet
- the PDF reader's worker
- Tree-sitter grammars (`@reviseio/editor/full` only)
By default they load from `https://revise.io`. A strict Content Security Policy
must allow that origin in `connect-src`, `font-src`, `style-src`, and
`worker-src`.
To serve them yourself, mirror these paths from `https://revise.io` to your
origin with the same layout:
- `fonts/`
- `katex.min.css`
- `pdfjs/`
- `tree-sitter/` (only for `@reviseio/editor/full`)
Then set the base URL once at startup:
```ts
import { setAssetBaseUrl } from "@reviseio/editor";
setAssetBaseUrl("https://assets.example.com/revise");
```
## Troubleshooting
The package requires React 18 or newer. Installing on React 17 or earlier
fails.
Two copies of Yjs are loaded. Make your bundler resolve a single `yjs`, for
example with a `resolve.dedupe` entry in Vite.
The editor was published as `revise` through 2.2 and is `@reviseio/editor`
from 2.3. `revise` 3.x is the CLI and server-side library. See
[Migrate from `revise`](#migrate-from-revise).
Source: https://revise.io/developer/docs/editor-sdk/installation
---
# Quickstart
> Render a document in about ten lines.
## Install
```bash npm
npm i @reviseio/editor
```
```bash pnpm
pnpm add @reviseio/editor
```
```bash yarn
yarn add @reviseio/editor yjs y-protocols
```
React, ReactDOM, Yjs, and y-protocols are peer dependencies. npm and pnpm
install them automatically; with Yarn, list them yourself. For server-side
work, add [`revise`](https://revise.io/developer/docs/revise/introduction) at the same version (an optional
peer); see [collaboration](https://revise.io/developer/docs/editor-sdk/guides/collaboration#seeding-from-your-own-server).
Requires React 18+ and Node 18+. The editor renders client-side only; in
Next.js, use a `"use client"` component loaded with `next/dynamic` and
`ssr: false`.
## Render a document
`ReviseEditor` is a multi-document workspace. Give each document a stable ID
that you own. The SDK uses it for routing, callbacks, and agent tools. The
source can be a DOCX, ODT, RTF, Markdown, plain text, or HTML file; see
[supported formats](https://revise.io/developer/docs/editor-sdk/guides/formats).
```tsx DocumentPane.tsx
import ReviseEditor, { type ReviseEditorHandle } from "@reviseio/editor";
import "@reviseio/editor/style.css";
import { useRef } from "react";
export default function DocumentPane({ file }: { file: File }) {
const editor = useRef(null);
return (
{
// Editor is mounted. Set up subscriptions here.
editor.current = handle;
}}
onDocumentReady={() => {
// First document is open. Controllers are usable.
}}
onChange={(documentId, document) => {
console.log(documentId, "now has", document.children.length, "blocks");
}}
/>
);
}
```
That's the whole integration. The editor handles parsing, layout, pagination,
input, undo, find, comments, and review.
The editor fills its container. Give the container a height: `100vh`, a
fixed value, or a flex or grid track.
## Load on demand
Import the editor where a document is opened rather than in your entry bundle:
```tsx
import { lazy, Suspense } from "react";
const DocumentPane = lazy(() => import("./DocumentPane"));
export function Workspace({ file }: { file: File }) {
return (
Loading…
}>
);
}
```
Importing `@reviseio/editor/style.css` inside `DocumentPane` keeps the stylesheet on the
same path.
## Export
```ts
await editor.current.whenReady();
const blob = await editor.current.tools.exportDocx();
```
`exportDocx()` returns a `Blob`. Comments and tracked changes are preserved.
`whenReady()` waits for the document to finish opening.
## Create a blank document
```ts
import { createEmptyDocx } from "@reviseio/editor";
await editor.current.documents.open({
id: crypto.randomUUID(),
title: "Untitled",
source: await createEmptyDocx(),
});
```
## A complete app
[reviseio/sdk-demo](https://github.com/reviseio/sdk-demo) is a standalone
React app on the public package, with a Node service built on [`revise`](https://revise.io/developer/docs/revise/library)
([live demo](https://reviseio.github.io/sdk-demo/)).
## Next steps
What the component owns, and what stays yours.
Opening, activating, and closing documents.
Turn off the ribbon and drive the editor from your own toolbar.
Hand the document to the model you already run.
Source: https://revise.io/developer/docs/editor-sdk/quickstart
---
# Licensing
> Free for noncommercial use, education, and evaluation. A commercial license for production use by organizations.
The Revise SDK is proprietary software. It is published on npm, and you can
install it without contacting us.
## Free use
You can use the SDK for free, with every feature included, for:
- **Evaluation:** development, testing, and prototyping, including for a
commercial product, with no time limit.
- **Personal noncommercial apps:** production apps run by an individual that do
not make money or support an organization.
- **Education:** accredited nonprofit and public schools, colleges, and
universities.
There is no open source requirement: your application stays yours.
## Commercial use
Production use by businesses, nonprofits, and government needs a commercial
license **before deployment**. This applies to internal tools and
customer-facing products. That includes apps that are free, ad-supported, or not yet
earning revenue. Commercial licenses can add support SLAs and source code
access.
To get one, use the form on the [pricing page](https://revise.io/sdk#pricing) or
email [sdk@revise.io](mailto:sdk@revise.io).
## Restrictions
- Modifying, decompiling, or reverse engineering the SDK. Configuring it through
its documented options is fine.
- Reselling or redistributing the SDK on its own, including as a reusable
wrapper. Shipping it inside your application is fine.
Each version of the SDK is licensed under the license included with it. The
SDK sends an anonymous usage ping, which you can turn off with
`telemetry={false}`.
## Full license
The text below is the `LICENSE.md` shipped in the `@reviseio/editor` package. It is the
license that applies; the summary above is for convenience.
```markdown LICENSE.md wrap
# Revise SDK License
Version 1.0
Copyright © 2026 Migraine, Inc. (“Revise”). All rights reserved.
By using the Revise SDK and accompanying documentation (“SDK”), you agree to these terms.
## Free use
Revise grants you a nonexclusive, worldwide, royalty-free license to copy, run, and integrate the SDK for:
- **Evaluation:** Development, testing, and prototyping, including for planned commercial applications, without prior permission or a time limit. Evaluation excludes production use.
- **Personal noncommercial apps:** Production apps run by an individual that do not generate revenue and are not intended to monetize, promote a paid offering, or support the operations of any organization.
- **Education:** Production use by accredited nonprofit or public schools, colleges, and universities for teaching, learning, research, and their own internal operations.
## Commercial use and source access
Production use by businesses, nonprofit organizations, and government entities, whether internal or customer-facing, and all other production use outside the free permissions above, requires a separate written commercial license **before deployment**, including paid, subscription, ad-supported, and commercially intended apps that are temporarily free or not yet earning revenue. Obtain a license before converting a free permitted use to commercial use.
Unpublished source code is not included. Source code access requires a separate commercial agreement expressly granting it.
Please contact [sdk@revise.io](mailto:sdk@revise.io) for a commercial agreement.
## Distribution and restrictions
You may bundle, minify, host, and distribute the compiled SDK within permitted apps, including to browsers. Preserve copyright and license notices. Recipients may run the embedded SDK within those apps. Standalone SDK resale, sublicensing, or redistribution—including reusable SDK wrappers—is prohibited without written permission.
Except to the extent applicable law expressly permits despite this restriction, you may not decompile, disassemble, reverse engineer, or modify the SDK, or attempt to derive its source code, algorithms, or internal structure. Configuring the SDK through its documented options is not modification. Revise retains ownership and all ungranted rights, and this license grants no rights to Revise names or logos except to identify the SDK; you retain your apps and documents. Third-party components retain their separate licenses.
The SDK sends an anonymous usage ping as described in its documentation; you may disable it.
## Termination
A material breach terminates your rights and requires you to stop using and distributing the SDK. Rights are reinstated if a first breach is fully cured within 30 days of written notice.
## Warranty and liability
TO THE EXTENT PERMITTED BY LAW, THE SDK IS PROVIDED “AS IS,” WITHOUT WARRANTIES, INCLUDING MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE, OR NONINFRINGEMENT, AND WITHOUT ANY SUPPORT OBLIGATION. REVISE AND ITS LICENSORS ARE NOT LIABLE FOR LOST PROFITS, LOST DATA, OR INDIRECT, INCIDENTAL, SPECIAL, OR CONSEQUENTIAL DAMAGES. THEIR TOTAL LIABILITY UNDER THIS LICENSE WILL NOT EXCEED US $100.
Ownership, restrictions, and warranty/liability provisions survive termination. A separate written agreement controls conflicting terms. Invalid provisions do not invalidate the remainder. Each version of the SDK is licensed under the license included with it. This license is governed by the laws of the State of Delaware, USA, excluding its conflict-of-laws rules.
```
Source: https://revise.io/developer/docs/editor-sdk/license
---
# Architecture
> What the component owns, what you own, and how state is scoped.
## The boundary
**The SDK owns**
- Parsing DOCX, ODT, RTF, Markdown, text, and HTML
- The document model and its Yjs session
- Canvas layout, pagination, and rendering
- Keyboard input, selection, and undo history
- Tracked changes and comment threads
- The agent tool runtime
**You own**
- The file: where it comes from, where it goes
- Document IDs and their meaning
- The collaboration transport, if you want one
- Accounts, permissions, and tenancy
- Persistence and versioning
- Your agent loop and its model
- Any chrome you choose to render yourself
The SDK does not call Revise servers. Its only possible network call is to an
agent backend you configure. With collaboration, the SDK builds the shared
document and reads awareness, and you open the socket. See
[collaboration](https://revise.io/developer/docs/editor-sdk/guides/collaboration). With
[`revise`](https://revise.io/developer/docs/revise/library), the process that relays updates can also create,
read, and edit the document.
## Canvas rendering
Revise is not built on ProseMirror, Slate, or a `contenteditable` DOM. It is a
word processor rendered to ``, with its own layout engine, text
measurement, and hit testing.
Page geometry follows Word, not the web: pagination, per-section page sizes
and margins, running headers and footers, footnote areas that flow with their
references, and text measurement that matches the `.docx` export.
The document is not in the DOM. Every read and write goes through the handle
or the tools. Selection geometry is not public: there are no canvas rectangle
or hit-test APIs.
## Document sessions
`ReviseEditor` keeps every open document mounted. Editor-local state survives
tab switches: caret and selection, scroll position, formatting context, search,
review and comment state, zoom, and in-flight agent runs.
```mermaid
graph TD
A[ReviseEditor] --> B[Document: contract-1]
A --> C[Document: exhibit-a]
B --> D[Yjs session]
B --> E[Selection, scroll, zoom]
B --> F[Review + comments]
B --> G[Agent state]
```
Every document-local controller supports two forms of routing:
```ts
editor.review.next(); // the active document
editor.review.forDocument("exhibit-a").next(); // a specific one
editor.document("exhibit-a").review.next(); // equivalent
```
Collection state comes from `editor.documents.getState()` and
`editor.documents.subscribe()`. Acting on the active document does not require
reading state first.
Tool state is per document, not per call. Reads, edits, and searches persist
across `tools.execute()` calls, so a `search_result_id` from one call can
target matches in the next. Reloading a document's source starts a new tool
session and invalidates those IDs.
## Document fragments
Each document stores content in separate fragments, as Word does:
| Fragment | Holds |
| --- | --- |
| `content` | The document body. Block operations and agent indices see only this. |
| `chrome` | Running headers and footers, per side, slot, and section. |
| `notes` | Footnote and endnote bodies, referenced inline from the body. |
Inserting a footnote does not shift body block indices. "Block 12" is always
body block 12, regardless of how many notes exist.
## Editing modes
Every document is in one of three modes, set per document and changed at
runtime through `view.setDocumentMode()`:
Changes apply directly. The default.
Changes land as tracked suggestions to accept or reject.
Read-only.
Agent tool calls (`tools.call`, `tools.execute`) make tracked changes by
default, whatever mode the editor is in, so a person reviews them. A delegated
`agent.run()` follows the document mode. Both take `directMode: true` to apply
edits directly. See [tracked changes](https://revise.io/developer/docs/editor-sdk/guides/tracked-changes) and
[agent tools](https://revise.io/developer/docs/editor-sdk/guides/agent-tools).
Mode is not a permission. Any code with the handle can change it. To prevent
a participant from editing directly, set a [role](https://revise.io/developer/docs/editor-sdk/guides/roles).
A role limits which modes are available and refuses agent tool calls that
exceed it.
Source: https://revise.io/developer/docs/editor-sdk/concepts/architecture
---
# Changelog
> Release history for @reviseio/editor (formerly revise and @reviseio/sdk).
## 3.1.7
The version jumps from 2.3.2 to 3.1.7 because the editor now releases in
lockstep with `revise` and `@reviseio/engine`: one version, built from one
commit. 2.x stays installable and gets fixes on the 2.x line
(`npm install @reviseio/editor@v2`).
### Removed
- **`@reviseio/editor/server`.** Server-side work moved to
[`revise`](https://www.npmjs.com/package/revise), the MIT CLI and
TypeScript library built from the same engine and released with this
version. It covers everything `/server` did:
| `@reviseio/editor/server` | `revise` |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `createServerDocumentSession(ydoc)` | `ReviseDocument.fromYDoc(ydoc)`, then `doc.tools` |
| `parseDocument`, `documentToDocx`, `fileToYDoc` | `ReviseDocument.open(file)`, `doc.export()`, `doc.ydoc` |
| `exportDocument(room, { mode })`, `ydocToDocx(room)` | `ReviseDocument.fromYDoc(room)`, then `doc.export({ format, mode })` |
| `seedYDocFromFile(room, bytes, filename)` | `seedYDoc(room, { bytes, filename })` |
| `encodeYDoc`, `decodeYDoc` | Y.js itself: `Y.encodeStateAsUpdate(doc.ydoc)`; `ReviseDocument.restore(update)` opens a bare update |
Documents move between the two unchanged: a Y.Doc the editor edits is a
Y.Doc `revise` opens, and the reverse. Two differences when porting:
`revise` methods are async (await them), and tools return
`{ ok, value | error }` instead of throwing; `doc.export()` returns
`{ bytes, … }`. `doc.snapshot()` is `revise`'s own storage format, not a
Y.js update: keep `Y.encodeStateAsUpdate` for anything that speaks Y.js.
### Added
- **`uploadPlaceholder`.** With `onImportFile`, an empty document shows
"Type or paste your text or [Upload] a document" until something is typed
or pasted. Upload opens a file picker for the same formats as Import and
hands the file to `onImportFile`. Off by default, and never shown in a
read-only editor.
- **Markdown shortcuts while typing.** `#` through `######` and a space at
the start of a paragraph make it a heading. Typing the closing delimiter of
`**bold**`, `__bold__`, `*italic*`, `_italic_`, or `` `code` `` formats the
text and removes the markers. Only keyboard typing converts (paste and
agent edits don't), never in suggesting mode or inside code, and one undo
restores the literal text. Like smart quotes, they're on in every editor;
there is no prop to turn them off yet.
- **Failed tool results carry `details`** when the tool reported more than
its message, for example `edit_table_cells`' per-cell `errors` and the
`recovery_read` that fixes them (`ReviseToolFailure.details`, also on a
thrown `ReviseToolError`'s `failure`).
### Changed
- **`revise` is an optional peer dependency** at the same version. Install
it for server-side work; a browser-only host doesn't need it. `jsdom` is
no longer a peer of this package (`revise` brings it).
- **`remove_blocks` accepts up to 200 ids** per call (was 50). A larger
batch fails without removing anything and points to `search_result_id`.
- **`search_document` doesn't page through empty paragraphs.** When every
match is an empty paragraph, asking for a later page fails and points to
acting on all of them at once with `search_result_id`.
- **`insert_table_row` matches its neighbors.** Text in a new row takes the
paragraph spacing, alignment, font, size, and color of the reference row's
cell in the same column wherever the HTML leaves them unset. Bold and
italic are not copied (the reference is often a totals row).
- **`` in tool HTML** is strikethrough. A `` (how
`revise` shows a tracked deletion) is left out, so HTML copied from a
tracked read edits the text as it reads with changes accepted.
- **The delegated agent on long documents.** `agent.run()` and comment
agents keep long requests within each model's context budget: earlier
document snapshots leave the conversation and the latest read window is
sent again. When a document-wide edit leaves thousands of blocks pending,
the document overview lists a sample of them with the total instead of
every one. An agent that stops "incomplete" with budget left and nothing
blocking it is sent back to work, and one that finishes a task it declared
whole-document after reading under 90% of the document is sent back to the
ranges it skipped. A request restarted after a reload keeps credit for the
edits its first attempt made.
### Fixed
- **Word-numbered headings stay headings.** Word often numbers clause
headings ("8.1 Cap on liability") through its heading styles, so they
import as list items, and their heading level was dropped. They keep it
now: `measure_document` and the delegated agent count them as sections,
titled with their number, tool HTML marks them `data-heading`, and
Markdown export writes them as `## 8.1 Cap on liability`. A paragraph whose
.docx sets `numId` 0 is no longer numbered by its style.
- **Markdown export numbers nested lists per level.** An item under "8." is
"1.", not "9.".
- **Tracked changes in headers and footers** import from .docx as pending
changes (they were silently accepted) and export as Word revisions (the old
and new text were both written, side by side).
- **Exporting an edited Word file no longer restyles it.** A file whose
package declares no document defaults exported with 12pt ones, so text that
inherits its size grew from Word's 10pt to 12pt, and runs that inherit
Word's default font were written back as explicit Arial. Both now export as
the original had them.
- **`set_page_layout` page numbers next to an authored header or footer.**
When the requested header or footer already has text, the number goes into
its empty left or right zone (a running head keeps its text and gains the
number), and the result says where the number is or why it could not be
placed. It used to report success with no number shown. The `apa7` preset's
page numbers work the same way; any existing header used to hide them.
## 2.3.2
### Fixed
- **Reordering a list** with `replace_block` reported success without
reordering, and rejecting it after a .docx round trip could duplicate an
item.
- **Hyperlinks across a .docx round trip:** tracked changes that add, remove,
or retarget a link in a list item (including lists in table cells) are
saved as revisions, so accepting or rejecting them after reopening gives
the same result as in the editor. A link removed in direct mode stays
removed when another author has a pending change on it, and a tracked
removal of a linked list item restores the whole link on reject.
- **Spacing and list levels across a .docx round trip:** tracked paragraph
spacing changes, including in list items and table cells, and tracked list
indent-level changes are saved as revisions, so rejecting them after
reopening restores the original. Deciding a reopened spacing change no
longer moves a nested list item to the top level.
- **Footnotes and table cells across a .docx round trip:** tracked paragraph
splits and joins in footnotes and table cells are saved as revisions; a
joined footnote stays joined after accepting, and rejecting no longer
leaves an extra empty paragraph. Accepting a reopened deletion of a list
item in a table cell no longer leaves an empty item.
## 2.3.1
### Fixed
Agent edits (`tools.call`, `tools.execute`, a delegated `agent.run()`, and
`@reviseio/editor/server` sessions) no longer change other people's pending
tracked changes. Before, an edit on or next to content that held someone
else's suggestions could silently accept them (with `directMode`) or re-create
them under the editing agent's name and today's date.
- **Paragraphs and list items** with pending changes: only the edit itself is
applied; existing suggestions keep their ids, authors, and dates. This
covers replacing text, formatting (`style_blocks`, `clear_formatting`),
appending, splitting, joining, inserting next to them, and item spacing and
indents.
- **Tables:** editing one cell changes only that cell. Inserting or removing
rows and columns keeps the other cells' suggestions.
- **New refusals instead of silent loss.** A few structural edits can't yet
keep existing suggestions, so they now fail with a clear error and change
nothing: `pending_table_changes` (row/column changes or merges on a table
that already has a pending row or column change or merged cells) and
`pending_list_changes` (converting a list's type, reordering, or other
list-level changes while the list holds pending changes). Resolve the
pending changes first, or edit the text directly.
- **Rejecting a format change after a .docx round trip** keeps footnote
references, comment anchors, fields, and hyperlinks. A rejected tracked
format change could delete a footnote's text.
- **DOCX export:** imported revision ids stay stable when new suggestions are
added; deletions over another author's insertion round-trip; replaced
paragraphs have no stray space after accept or reject in Word; suggestions
carry their author and date (some were written as "Anonymous"); deleted
list items accept cleanly after reopening; deleted links restore on reject.
- **DOCX import:** tracked insertions and deletions inside hyperlinks are
read; a list item that is wholly inserted or deleted imports as one
revision.
- `insert_footnote` works in paragraphs that have pending changes.
- Removing a table cell's only paragraph empties it instead of leaving a
change that can't be accepted.
## 2.3.0
### Added
- **`toolbarMode="compact"`.** The everyday part of the ribbon, centered,
for narrow fixed-width hosts (about 760px): undo and redo, font, size,
styles, paragraph, links, indents, lists, Find, and Export. The clipboard,
Insert, and Print are left out.
- **`toolbarMode="floating"`.** The full ribbon as a centered card over the
canvas, the way the Revise app shows it.
- **`onImportFile`.** Shows Import before Export in the ribbon (a file picker
for DOCX, ODT, RTF, Markdown, HTML, text, and XML) and accepts files
dropped anywhere on the editor, with a "Drop to import" overlay. Image
drops still insert images. The host receives the file and decides what
importing means.
- **Header and footer images.** Logos in Word headers and footers now show
on the page and in PDF export. They were kept for DOCX round trips but
never drawn. Images are scaled down to fit the margin band and never
overlap body text. `set_header_footer` takes an `image` (`image_id` of an
image already in the document, with `zone`, `alt`, `width`, and `height`)
and a one-based `sectionIndex` to edit one section's own header or footer.
Concurrent edits to different bands' images merge instead of overwriting
each other. DOCX export keeps the logo's alignment. It turns on Word's
first-page or odd/even headers when a logo needs them, and leaves a logo
out of sections whose margin is too small to show it.
- **`@reviseio/editor/server`: `agentName`.**
`createServerDocumentSession({ agentName })` sets the author on the
suggestions and comments the session's tools create, which DOCX export
writes as `w:author`. Defaults to "Revise Agent".
- **The delegated agent can move any block** as one tracked move: images,
code, equations, horizontal rules, page breaks, and tables of contents as
well as paragraphs, lists, and tables. Section breaks are still refused.
### Changed
- **The package is now `@reviseio/editor`.** It was published as `revise`
through 2.2.0. The editor, license, and API are unchanged; only the import
paths move: `revise/editor` → `@reviseio/editor`, `revise/editor/full` →
`@reviseio/editor/full`, `revise/server` → `@reviseio/editor/server`,
`revise/style.css` → `@reviseio/editor/style.css`, and the `revise` root
(build identity) → `@reviseio/editor/version`. The docs moved to
[revise.io/developer/docs](https://revise.io/developer/docs/editor-sdk/introduction).
- **`replace_block` and `remove_blocks` name a range by its last block.**
`end_id` (inclusive, same container as `id`) replaces `count` in the tool
definitions and in `ReviseToolInputMap`. Calls with `count` still run, but
TypeScript callers that pass it need `end_id`. A reversed, unknown, or
cross-container range fails with `invalid_block_range` before anything
changes.
- **`remove_table_rows` replaces `remove_table_row`.** It removes one or more
rows per call, named by stable row ID (`rowIds`, a string or an array).
Rows the agent read stay removable after other rows change, without
reading the table again. `remove_table_row` with `{ tableId, rowIndex }`
is still accepted.
- **`read_table` `mode: "cell_text"` pages list their nested blocks**
(`blocks`: block ID and text offset), and each nested block's text opens
with an inline `[[block_id]]` marker. Text inside an oversized cell can be
read with `read_specific_blocks` and edited. A `replace` aimed at a table,
row, or cell edits the one nested block that contains the text.
- **`set_header_footer` reports what actually changed.** `clear: true`
without `slot` or `sectionIndex` removes that side from every page and
section. A call that changes nothing fails with "No changes detected" and
the current header/footer listing. An unknown `slot` is an error instead
of widening a clear. Clearing the first-page or even-page slot while the
document-wide footer shows there leaves an empty slot that hides it, as in
Word. `search_document` also matches header and footer text.
- **Mutation tools refuse empty arrays** (`content`, `replacements`,
`cells`, `ids`, `rowIds`, style `attrs` or `selectors`) before changing
anything, and name the tool that fits instead. The definitions mark these
`minItems: 1`. Empty table-insert content still inserts empty cells.
- **Edits next to an unresolved paragraph split or merge are refused** with
`pending_paragraph_boundary`: accept or reject the split first. Such edits
used to lose or duplicate text on review.
- **`style_blocks` headings take the document's heading style.** In an
imported Word document, a paragraph made a heading gets the heading
style's spacing and run formatting, and exports like the document's other
headings.
- **Agent-inserted content follows the surrounding font.** Inserted tables
take the font and body size at the insertion point. One `insert_block`
call uses one font for all its pieces. Rows and columns added by
`insert_table_row` and `insert_table_column` keep the table's font. A
heading's size no longer carries into paragraphs inserted after it. Cells
holding only inline markup (`Total `) keep their
formatting. Tables inserted from the editor take the caret's font.
- **DOCX export keeps more of the original file.** Untouched table rows,
cells, and list items are written as the original bytes when a neighbor
changes. Paragraphs with only text edits keep their original paragraph
properties. The original `styles.xml` is kept, with only the missing style
definitions added. Opening a document with page numbers no longer forces
headers, footers, and styles to be regenerated. In
`@reviseio/editor/server`, `exportDocument(room, { mode: "final" })` to
.docx keeps untouched content as the original bytes too.
- **The delegated agent** (`agent.run()` and comment agents) sees the
document's headers and footers. It stays inside a locked user selection
unless asked to go wider. It may re-read blocks it has already read. It
puts content asked to be added into the document rather than only in
chat, and it no longer finishes before results it asked for arrive. With
Anthropic models, requests that follow edits keep the prompt cache.
### Fixed
- **Comment cards sit beside the page**, as in the Revise app, instead of
at the far right edge of a wide editor.
- **The ribbon stays centered on the pages** when the comments margin moves
them aside, as far as its width allows.
- **Add Comment and Toggle Comments** in the Review ribbon work. They were
always disabled.
- **Find shows in the native ribbon.** It was hidden whenever Dictate was,
which the SDK always hides.
- **The ribbon keeps text color and the styling menu down to 352px**, where
they were dropped below 430px.
- **The selection stays visible, dimmed, when focus leaves the editor**, for
example to a chat input in the host page, instead of disappearing.
- **Copy and Cut work in iframes that deny clipboard access** (no
`clipboard-write` permission).
- **Agent avatars on suggestion cards load**, where they used to resolve
against the host page's origin and show a generic avatar.
- **Hovering Accept all or Reject all previews the decided document.** Blocks
the decision removes, such as a moved table's source, are hidden, while
edited code and math blocks stay. Typing ends the preview, so keys never
edit a hidden block.
- **Word redlines with inserted or deleted paragraphs accept and reject
fully.** Consecutive paragraph-mark revisions used to overwrite each
other, so rejecting inserted clauses left them in place and accepting
could leave an empty paragraph. Rejecting a split keeps the paragraph's
current text, formatting, and pending revisions. Settling a
paragraph-formatting change no longer undoes later inline decisions.
Accept all and Reject all finish in one call when one decision uncovers
another.
- **Tracked table changes export to Word as revisions.** A suggested row
insertion, alone or combined with cell edits, is a Word row revision with
attributed inline cell changes instead of an untracked table. A
repeating header row survives a tracked deletion of the first row.
- **Moves of images, code, equations, rules, page breaks, and tables of
contents export once** to DOCX, not as two copies. Accepting or rejecting
a moved table with code or math in its cells no longer leaves a damaged
copy. Copied content no longer carries suggestion records.
- **A DOCX export never brings back a removed paragraph** (for example, a
rejected insertion) by returning the original file unchanged.
- **Highlight colors map by hue.** Office tints such as lavender no longer
show as gray, and mid or dark neutrals no longer show as green or yellow.
`style_blocks` can restore a block's original highlight.
- **PDF export draws SVG images** instead of blank space, and fetches an
image used on every page once.
- **HTML input with ``** no longer fails, and loose text
before a block keeps its place.
- **Turning page numbers off keeps an empty first-page footer**, so the
footer stays hidden on page 1.
- **Footnote edits.** Text in a just-inserted footnote is edited in place
instead of moving into the body. A find matches only visible footnote
text, not pending deletions. A batch that includes a footnote edit fails
or succeeds as a whole. `leave_comment` accepts a footnote that was read.
- **`replace` batches apply as planned.** Chained steps
(`Alpha→Beta`, `Beta→Gamma`), finds over a `search_result_id`, and
id-less `occurrence: "all"` finds are checked against each block's text
as earlier steps leave it. A step no longer lands on the wrong block or
sentence. Errors say when a long find drifts from the block, when the
block already contains the replacement, and that none of a failed call's
replacements applied.
- **`read_specific_blocks` reads a list item inside a table cell.**
- **NEL, LS, and PS characters read as spaces**, so a find copied from a
read matches.
## 2.2.0
### Added
- **ODT and RTF input.** `ReviseDocumentFormat` and `ReviseServerFormat` gain
`"odt"` and `"rtf"`, detected from `.odt` / `.rtf` filenames and from the
`application/vnd.oasis.opendocument.text`, `application/rtf`, and
`text/rtf` MIME types. Both import fonts, sizes, colors, highlighting,
character and paragraph formatting, headings, lists with their numbering,
tables with merged cells and column widths, images, hyperlinks, footnotes
and endnotes, headers and footers, comments, tracked changes, and page
size and margins. RTF from Word, WordPad, TextEdit, and LibreOffice is
supported, in any Windows code page or East Asian encoding. Each converter
loads only when a host first opens that format. `revise/server`'s
`parseDocument`, `fileToYDoc`, and `seedYDocFromFile` accept both. Host
code with an exhaustive `switch` over either type needs cases for the two
new values.
- **Delete whole table rows, columns, or tables from a cell selection.**
Backspace or Delete over fully selected rows or columns removes them, and
over every cell removes the table; a partial selection still clears only
the cell contents. Removing several rows is one undo step, and a table that
was the only block leaves an empty paragraph. Shift+click in another cell
of the same table now extends a cell selection, as dragging does.
- **Move connectors.** When a tracked move is focused, a line in the left
margin links where the text moved from to where it moved to, across pages.
It is not drawn in print preview.
- **Pleading-paper line numbers.** Documents whose page layout carries a
pleading grid show numbered rows (for example 1 to 28) at fixed positions
on every page, independent of the text, on screen and in PDF export. DOCX
export writes them as Word line numbers restarting on each page.
### Changed
- **Margin line numbers behave like Word's.** Each number sits on its line's
baseline, table lines are not counted on screen (PDF already skipped
them), and a section that turns numbering off shows none instead of
inheriting the document setting. Numbers are set in the font that
dominates each page's text, on screen and in PDF, where they were always
Helvetica before.
- **The delegated agent can move blocks.** `agent.run()` and comment agents
can move paragraphs, lists, and tables as one tracked move (applied
directly in editing mode); blocks with anchored comments are not moved.
`move_blocks` is not part of the embedded tool list for `tools.*` hosts.
The delegated agent also recovers more reliably: it repairs a run that
edited and then reported no change, checks table fill work before it
finishes, and keeps the request and latest tool results when it recovers
from a context limit.
- **Suggestion cards show a date once a change is over a week old.** Old
changes read "Sep 4" or "Oct 11, 2013" instead of "4739d ago".
- **The footnote card is a compact row.** Clicking a footnote reference shows
its label with **Go to note** and **Remove**; the note is edited in the
note itself. In narrow layouts the card is centered under its reference
mark.
### Fixed
- **Links.** Bare email addresses link and autolink as `mailto:`;
`mailto:` and `tel:` URLs are kept as typed; an `@` before the path is not
read as `https://` user info. Cmd+K uses the selected text as the link
target only when it is a full URL or an email address, and the link popup
finds a link Cmd+K just made on a backward selection.
- **Link, footnote, and inline-math popups line up at every zoom level.**
- **Math is no longer clipped when it renders before the KaTeX fonts load.**
Such results show provisionally and re-render once the fonts arrive, and
failed font or stylesheet fetches are not cached.
- **DOCX tracked moves of whole paragraphs, lists, and tables.** On import,
Word's paragraph-mark `w:moveFrom` / `w:moveTo` make the whole block part
of the move, so accepting it removes the source paragraph instead of
leaving an empty one, and a list split across a move becomes two lists.
Export writes paragraph marks and table rows with the move, and accepting
or rejecting resolves both halves together.
- **Accepting the deletion of every row of a table removes the table**, as
in Word, instead of leaving a table with no rows.
- **DOCX header and footer line breaks.** `w:br` in headers and footers is
imported, and export writes `w:br` instead of a raw newline that Word
showed as a space.
- **Header and footer layout.** In multi-column sections, headers and
footers span the full text width instead of one column; a zone is as wide
as its widest line; a bordered zone, such as a footer rule, spans the whole
band; and empty zones give up their space.
- **`replace_block` and `append_to_paragraph` on a non-text target**, such as
a table, fail with `target_type_mismatch` instead of doing nothing.
- **DOCX, ODT, and RTF open where `Blob.arrayBuffer` is missing** (Safari
before 14, jsdom). Files are read with a `FileReader` fallback.
### Security
- **ODT and RTF import is hardened against hostile files.** Links pass a
scheme allowlist (http, https, mailto, tel, ftp, and relative), so
`javascript:` and similar are dropped. Self-referencing ODT deletions and
RTF control words such as `\constructor` can no longer overflow the stack
or swallow text, and imported tables are capped at 1,000 columns and
100,000 cells.
## 2.1.0
### Added
- **`source` on document inputs.** `{ id, source: file }` names the field for
what it takes: DOCX, Markdown, plain text, or HTML. `docx` still works as the
same field; passing both with different values throws.
- **`context_notes` is optional in typed host calls.** `tools.execute()` and
`tools.call()` (browser and `revise/server`) fill in `"N/A"` for the read and
search tools, so application code no longer passes a model's working memory.
`executeDynamic()` keeps the schema requirement for model-provided calls.
- **`leave_comment` reports a retargeted anchor.** When the anchor text is not
in the named block and the comment goes to the only read block that has it,
`data.requestedBlockId` names the block you asked for and the message says so.
- **`read_table` and `edit_table_cells` agent tools.** Read a large table in
bounded windows (a summary, up to 50 rows, or one oversized cell in pages),
then set the plain text of up to 50 cells per call by row ID and column.
Cell text is written literally. Only cells a `read_table` call returned in
full can be edited, and that read stays valid across later `execute()` calls
on the same document while the cell and table geometry remain unchanged.
An invalid target rejects the whole batch with no changes. In suggesting mode each cell is its own suggestion. Read-only
surfaces and viewers get `read_table` only. Both tools are also in the
`revise/server` session. The SDK's `read_table` does not take the native
agent's task-declaration arguments (`task_kind`, `scope_*`,
`source_attachment_id`, `deliberate_blanks`). They are left out of the
published schema, and a call that supplies one fails with
`unsupported_argument` without reading anything. Empty placeholders
(`null`, `""`, `[]`) are ignored.
### Changed
- **The delegated agent plans long tasks.** `agent.run()`, `revise_run_agent`,
and comment agents keep a step-by-step plan for multi-step work, update it
in the same turns as their edits, and save it with the conversation, so
fewer turns go to bookkeeping. The embedded tool list is unchanged: plan
tools stay inside the delegated agent.
- **Faster on long documents.** The scrollbar track and its markers are drawn
once and reused while scrolling, Find reuses each block's normalized text
between keystrokes, and tracked-change records are indexed in one pass
instead of one document walk per suggestion.
### Fixed
- **DOCX paragraph spacing follows the document's rules.** `w:contextualSpacing` no longer
zeroes a paragraph's declared space before and after on import. Spacing is
resolved from neighboring paragraphs on screen, in table cells and
containers, and in PDF export: the gap between two paragraphs is the larger
of their spacings, not the sum, and contextual spacing drops it only between
paragraphs of the same style. PDF keep-with-next reserves the whole gap, and
exported DOCX keeps the declared spacing. Documents whose paragraphs both
declare spacing lay out tighter than in 2.0.x.
- **`style_blocks` with `list-style-type: none` turns list items into plain
paragraphs**, keeping their spacing and line spacing and applying the call's
other paragraph attributes in the same step.
- **Unsupported DOCX content can be suggested for removal.** In suggesting
mode, removing a block the editor preserves but cannot render (an opaque
DOCX payload) now becomes a tracked deletion.
- **Comment previews keep their layout.** In a comment chip's hover preview,
the avatar holds its size and a long author name is truncated.
- **An editor with no agent configured sends nothing.** `agent.run()`, the
`revise_run_agent` tool, and comment agents used to stream the document,
unauthenticated, to Revise's hosted agent and then report a generic
connection error. Without a `turnStream`, `baseUrl`, or `token`, they now
fail at once with `agent_not_configured` (`AgentNotConfiguredError` from
`agent.run()`), and `getToolDefinitions()` leaves the delegation tool out.
- **`documents.open()` activates the new document every time.** Opening a
document straight after another open, or after `documents.activate()`, left
the earlier one active. `documents.list()` status was also briefly stale
right after a document became ready.
- **`tools.getDefinitions()` lists only the native tools.** It included
`revise_run_agent`, which the docs say it leaves out.
- **Changing `agent.turnStream` no longer reloads the document.** A new
`turnStream` or `disableMetrics` value re-created the document runtime,
re-parsing the source and dropping unsaved edits, so an inline generator in
JSX reloaded the document on every render. Agent settings now apply to the
next run in place.
- **Reads are not refused for the life of the editor.** After a window had
been read once, every later `read_blocks_from_index` of it failed with
`no_new_read_context`, so a host agent's second conversation could not read
the document. The guard now resets with each call, like the other per-turn
limits.
- **Tool errors speak to SDK callers.** Messages no longer carry the internal
`Failed operation: : :` prefix or point at ``,
which only Revise's own agent sees; they point at read results instead.
- **The open-error screen shows a plain message.** A corrupt file showed the
parser's internals (a zip library's message and link) to the person who
picked it. `onError` still receives the full detail. Loading text no longer
says "DOCX" for other formats, and a `File` source's title drops `.md`,
`.txt`, and `.html` extensions as well as `.docx`.
- **PDF export requests a failing font once.** When a font download failed
(a CSP or firewall blocking revise.io, say), export requested the same URL a
second time. The warning now says a fallback font was used and names
`setAssetBaseUrl()` for mirroring the fonts.
- **Readable errors from the package in Node.** Bundled code is wrapped at
short lines, so an uncaught error (such as the missing-`jsdom` hint) is no
longer printed under a 100 KB line of minified source.
- **Quieter host builds.** Vite no longer warns that `"buffer"` was
externalized, or prints the inlined WebAssembly's base64 as a file that
"doesn't exist at build time".
## 2.0.1
### Added
- **`tools.export({ format, mode })`.** Export as DOCX, PDF, HTML, Markdown,
or plain text from code, not just the ribbon's Export menu. `mode: "final"`
accepts every pending change and drops comments first for a clean copy;
the default `"review"` keeps tracked changes and comments in a .docx. The
editor-level `tools.export(options, documentId?)` routes to any open
document.
- **`exportDocument(source, options)` in `revise/server`.** The same export
for a room or a parsed document on the server: DOCX, HTML, Markdown, and
plain text, in review or final mode.
- **`onImageUpload(file, documentId)`.** Store images users insert, paste, or
drop in your own storage and return the URL the document should use.
### Fixed
- **Inserting, pasting, or dropping an image works.** It used to post to
Revise's app API, which the SDK cannot reach, so the image never appeared.
Without `onImageUpload`, images are now embedded in the document as data
URLs and export into the .docx. `toolbar.insertImage()` resolves `false`
when an upload fails, and the failure reaches `onError`.
- **Docs:** linked moves are described correctly in Support, the reserved
collaboration keys point to `RESERVED_DOCUMENT_KEYS`, and the delegated
agent page says how to get hosted-agent access.
## 2.0.0
The SDK is now public: `npm i revise`. No account, token, or contact
with us is needed to install it. 2.0.0 also ships everything from the
2.0.0-alpha releases below.
### Changed
- **BREAKING: the package is now `revise`.** The private `@reviseio/sdk`
package is retired. Update the dependency and imports:
`@reviseio/sdk` → `revise/editor`, `@reviseio/sdk/full` →
`revise/editor/full`, `@reviseio/sdk/backend` → `revise/server`, and
`@reviseio/sdk/style.css` → `revise/style.css`. Remove the `@reviseio`
scope and token lines from `.npmrc`.
- **New license: free for evaluation, personal projects, and education.**
Production use by businesses, nonprofits, and government requires a paid
license. See `LICENSE` and [the license page](https://revise.io/developer/docs/editor-sdk/license).
The previous 60-day evaluation terms no longer apply.
- **The editor fills its container at any size.** The 640×560px minimum is
gone, so the editor fits phone-width columns and short panels without
overflowing them. A container with no height gets a 560px fallback and a
one-time console warning instead of a collapsed editor.
- **Hosted agent defaults to production.** Without `agent.baseUrl`, the
delegated Revise agent now targets `https://agent.revise.io` instead of a
development address.
### Added
- **A 4.5 MB package, with assets loaded on demand.** PDF export fonts were
inlined as base64 and made up three quarters of the package; they, the math
fonts and stylesheet, the pdf.js worker, and Tree-sitter grammars now load
from `https://revise.io` when first needed. **`setAssetBaseUrl(url)`**
points the SDK at a self-hosted mirror.
- **`import ReviseEditor from "revise/editor"`.** The editor entry now has a
default export. The named `ReviseEditor` export still works.
- **`revise` root entry** with `REVISE_SDK_VERSION`, `REVISE_SDK_BUILD`, and
the shared types, and no runtime weight.
- **`THIRD-PARTY-NOTICES.md`** lists every bundled open-source package with
its license.
- **Anonymous usage ping, and the `telemetry` prop to turn it off.** Once
per page load, the editor sends Revise its version; the browser adds the
site's origin. Development hosts are counted per day and version without
their origin. No cookies, user data, or document content.
`telemetry={false}` disables it.
### Fixed
- **Math renders with its real fonts in host apps.** Equations looked for
their fonts and stylesheet at the host's own `/fonts` and `/katex.min.css`
and fell back to system fonts; the help link in the editor menu also
pointed at the host's `/help`. All now resolve to Revise's assets.
- **The SDK no longer reaches for development servers.** Revise application
endpoints (document history, preferences, uploads, failure reports) and the
sync-server addresses no longer resolve to `localhost` in an embedding
host. Document history in particular fetched from `localhost:3001` in
browsers without a `process` global.
- **No debug globals on the host's `window`.** `__getEditor`,
`__getDocLayout`, and `__aiLogs` are no longer installed.
- **DOCX math export uses Revise's own MathML → OMML converter.** Equations
export to native Word math (fractions, radicals, scripts, sums and
integrals with limits, delimiters, matrices, accents, bars, braces,
functions, boxed and text runs) with no third-party copyleft code in the
package. Re-importing them is more faithful: `\left\{ … \right.`,
`\underline`, `\overbrace`, `\boxed`, `\text{…}`, `\mathbb`, binomials,
and named functions (`\sin`, `\log`, `\lim_{…}`) now come back as the
same LaTeX instead of approximations, and symbols no longer run into the
following letter (`\alpha x`, not `\alphax`).
## 2.0.0-alpha.7
### Fixed
- **DOCX round trips keep every modeled mark and block.** A new
export → import matrix (every formatting mark in every text context,
every paragraph, list, table, cell, image, code, math, note, header,
footer, and page-layout attribute, plus a second-pass fixed-point
check) found and pinned a set of losses: lists inside table cells
were dropped on export; hanging indents exported as zero and
`w:hanging` was ignored on import; headings came back as Word's blue
16pt built-ins (export now writes the editor's heading sizes with
inherited color); unscoped header/footer blocks were not exported;
hyperlinks discarded an explicit text color; links inside footnotes
were lost on import; header and footer runs used a reduced codec in
both directions and now share the body run codec, links and inline
code included; inline code and code blocks flattened to Courier text
and now travel as `CodeInline` / `CodeBlock_` styles and
return as the original nodes; todo items exported as literal
`[x]` text and now use checkbox glyphs that re-import as todo lists
with their checked state; code-block line breaks doubled on
re-import; and a single run in another font restamped the whole
document (the inferred document font is now the character-weighted
majority).
- **Table borders export and re-import faithfully.** Export no longer
throws on non-hex colors such as `TRANSPARENT`, `rgba()`, or CSS
names — unresolvable values are omitted or written as `auto`. The
uniform `borderWidth`/`borderColor` pair is honored (width 0 means
no borders), width-0 spec edges export as `none` instead of a
hairline, and an explicit `w:sz="0"` imports as hidden rather than a
1px line. Table Settings border changes now rewrite the structured
table spec and clear per-cell overrides, so hiding borders on an
imported table takes effect.
- **Byte-preserving DOCX export degrades instead of failing.** A carrier
that fails to load falls back to the regenerated package like every
other graft failure; the graft option reports its outcome (exact,
grafted, regenerated, and the fallback code) through an isolated
callback that can never break an export. Untouched imports of rich
documents now export byte-identical: note-body font inheritance,
header/footer zone ids, and legacy chrome no longer drift the
fidelity signatures; watermark and settings edits graft instead of
restoring carrier values; body grafting is invalidated when wrapper
or style sidecars change; single-quoted and non-numeric relationship
ids parse; and a changed header image is replaced even when its
media filename collides.
- **Formatting suggestions preserve footnotes.** Accepting a
`style_blocks` suggestion keeps footnote references and bodies.
- **Large documents no longer stall on batch edits.** Styling 205 blocks
in a 10,000-block document, applying 100 replacements, and large
multi-block insertions run without re-decoding the document per
action; suggestion projection latency is bounded; batch paragraph
formatting suggestions behave identically through preview,
acceptance, and rejection.
- **Semantic tool errors and inputs.** Errors distinguish Markdown find
syntax from stored plain text; metric CSS paragraph indentation
(`cm`, `mm`) is accepted; number-dot prose can be parsed as inline
text instead of a list when requested.
- **Comments render AI citations.** Structured citations persist on
replies and render in the panel; unsupported provider citation
markers are hidden, including on legacy comments.
- **Agent image rehosting.** Wikimedia thumbnail URLs are recovered by
rehosting the original image, and rate-limited rehosts surface the
backend reason and status.
- **Revision timeline never collapses to zero.** A stale or missing
server update counter no longer disables history or pins every frame
to the first update; loaded frames are authoritative.
- **Pasted images write once.** A pasted image no longer inserts a
temporary base64 placeholder that is replaced later; the final hosted
URL is written at the captured paste position when the upload
completes.
### Changed
- **Comment margin geometry is direction-aware.** Chip, floating-stack,
and insert-chip positions are computed along an outward axis with a
single conversion to physical coordinates. The embedded document
surface itself is still laid out left-to-right: the margin chrome
stays on the page's right regardless of the host page's direction.
Mirroring the embed for right-to-left hosts is not part of this
release.
## 2.0.0-alpha.6
### Fixed
- **A healed seed race now recovers full fidelity — and never loses a
comment.** Three repair refinements: the winner is the seed whose
metadata survived the merge (each seed stamps a ballot in the same
transaction), so the kept content matches the surviving import
capture; a shared deterministic id keeps the WINNER's element, not an
arbitrary one; and map records under a shared key (deterministic
comment ids) are never deleted — the merge already deduplicated them,
and alpha.5's repair removed the only surviving record. A raced,
repaired room now reports "exact" and exports byte-identically.
## 2.0.0-alpha.5
### Fixed
- **Seed-race repair kept a duplicate footnote.** Deterministic ids that
every seed generates identically (docx footnotes) were protected from
repair as a collision safeguard, leaving one copy per seed. Repair now
keeps exactly one occurrence of a shared id. Supersedes 2.0.0-alpha.4,
which was deprecated minutes after publish.
## 2.0.0-alpha.4
### Fixed
- **Double-seed connect race no longer duplicates the document.** Two
peers that each found an empty room and seeded the same file before
syncing used to merge into a perfectly-synced, perfectly-duplicated
document. Every seed now records a claim in the same transaction as
its content, and any peer that sees two claims deterministically
removes the losing seed's content — idempotent, convergent, and
content added after a seed is never touched. `seedYDocFromFile` is
now genuinely safe to call on every connection: a later call also
heals an already-raced room, and every live editor session repairs
one the moment the second claim syncs in.
- **Editing no longer rewrites document properties.** A structural edit
(or a rejected tracked change) permanently dropped the imported core
properties from the live model, so every later export regenerated
`docProps/core.xml` with an "Un-named" creator, the title removed,
and the creation date reset to export time. Core properties now
survive every model rebuild, and when core.xml IS legitimately
regenerated it is a faithful merge: `cp:revision` round-trips and
fields the original never had are not invented.
- **`docProps/app.xml` passes through untouched.** It is never
regenerated (preserving Application/Company/TotalTime) and never
added to a package that did not have one.
### Added
- **`repairSeedRace(ydoc)`** on the backend entry (with
`SeedRaceRepairResult`): detect and repair a double-seed race on
demand. Editor sessions and `seedYDocFromFile` run it for you.
### Changed
- **New reserved top-level key in shared documents: `"seed-claims"`**
(see `RESERVED_DOCUMENT_KEYS`) — where seed claims live.
## 2.0.0-alpha.3
### Fixed
- **Backspace before a chip deleted the chip.** A preserved-object chip
has two caret stops — before and after — and a delete now respects
which side the caret is on, like a caret between two characters:
backspace at the before-stop targets the block BEFORE the chip (an
atomic neighbor is deleted whole, a text neighbor loses its last
character), and forward delete at the after-stop targets the block
after. Deletes pointing AT the chip still remove it.
- **Up/down arrows at a chip slid the caret to the chip's other side**
instead of changing lines. Vertical movement now leaves the chip's
line and lands on the neighboring block's stop nearest the caret's
column — arrowing through a stack of chips keeps the caret on the
same side all the way down.
## 2.0.0-alpha.2
### Fixed
- **Exporting .docx could crash in production builds of the host app**
("Cannot destructure property 'default' … as it is undefined" from
the converter's zip loader). The SDK's build emitted duplicate
interop chunks for its zip dependency behind dynamic imports, and
some consumer bundlers resolved one family to `undefined` when
producing production builds — development builds were unaffected,
which is why the crash only appeared after deployment. The dependency
is now a static import inside the (still lazily loaded) converter
modules: the load profile is unchanged and the chunk graph no longer
depends on the host bundler's interop handling.
- **Backspace in an empty paragraph directly after a chip deleted the
chip** instead of the paragraph. Preserved payloads are content the
source file owns, not editor furniture: the empty paragraph dies, the
chip survives. (Page and section breaks keep their old
consume-on-backspace behavior.)
- **Chips gained real before/after caret stops.** The caret can now be
placed on either side of a chip — arrow keys walk across it, and
clicking the left or right half of the chip places the caret on the
matching side.
## 2.0.0-alpha.1
Prerelease for evaluation. The 2.0 line's headline is DOCX source
fidelity: a document imported from .docx can now leave the editor as the
file it came from, not a reconstruction of it.
### Added
- **DOCX source-fidelity mode (default on).** Importing a .docx keeps the
original package inside the document (the "carrier", shared documents
included, capped at 25MB). On export, content you did not touch is
emitted as the exact bytes that arrived — byte-for-byte, unannotated —
and only changed content is regenerated. A verification stage inspects
every composed package and falls back to full regeneration rather than
ship anything it cannot verify.
- **`handle.getSourceFidelity()`** (and `ydocSourceFidelity(ydoc)` /
`documentToDocx(doc, { originalDocx })` on the backend entry) reports
the honest relationship of the next export to the original file:
`"exact"` (byte-identical), `"grafted"` (original bytes plus your
edits), or `"converted"` (no usable original; the report says why).
- **Unmodeled Word formatting survives editing.** Run, paragraph, table,
row, cell, and section properties the editor does not model (vendor
effects such as `w14:glow`, revision-save ids, row heights, table
look flags, page borders, note-numbering properties…) are carried
through the model and re-emitted — so an EDITED paragraph keeps them
too, with or without the carrier.
- **Preserved objects render as labeled chips.** OLE embeds (Excel,
Visio), SmartArt without a raster fallback, and unknown OOXML
constructs import as atomic blocks labeled with their source tag
(e.g. "w:object · Excel.Sheet.12"), render as chips on canvas and in
PDF export, and re-export verbatim. In agent HTML they appear as
` `: an agent can keep, move, or omit one
(omission deletes), but can never inject raw OOXML of its own.
- **Editable-region permissions preserved.** `w:permStart`/`w:permEnd`
pairs (and future paired ranges) survive import, editing, and export
with balance guaranteed by construction: typing inside extends the
region, deleting its text retires it, and unbalanced pairs in source
files are repaired at import.
### Changed
- **BREAKING: two new reserved top-level keys in shared documents** —
`"docx-carrier"` and `"oxml-ranges"` (see
`RESERVED_DOCUMENT_KEYS`). A host-built Y.Doc already using these
names is rejected by `assertUsableSharedDocument`.
- **BREAKING: docx-sourced exports are byte-preserving by default.**
1.x regenerated every export from the model; 2.0 reproduces original
bytes for untouched content. Hosts that depended on exports being
normalized into the generated dialect should convert explicitly.
- `ReviseDocument` block children may now include the `"docx-raw"`
node type (opaque; convert rather than inspect, as ever).
- Shared documents built from a .docx are larger by roughly the size of
the source file (the carrier). It is stored once, chunked, and synced
like any other document state.
## 1.3.1
### Fixed
- **Delegated page inspection now reaches the embedded editor canvas.** The
SDK connects the delegated agent's `render_document_pages` tool to the
mounted document renderer, so appearance and layout requests return the
actual pages instead of reporting that canvas inspection is unavailable.
- **Agent-tool documentation now matches the generated SDK catalog.** The
public docs use the canonical `remove_blocks` name, distinguish the
deterministic document-local tool subset from delegated-agent-only tools,
and explain where visual page inspection is available.
## 1.3.0
### Added
- **The built-in editor UI now localizes itself in English, Spanish, and
German.** Ribbon controls, menus, dialogs, comments, Review mode, and agent
chrome follow the browser language or the persisted Revise UI-language
preference, while English remains the fallback if a locale chunk cannot be
loaded.
- **The delegated browser agent can inspect the document's rendered pages.**
It can request bounded appearance contact sheets or layout diagnostics from
the real canvas renderer, including page geometry, tables, images, math,
headers, footers, and safe placeholders for unavailable visual assets.
### Changed
- **Tab editing and layout now follow Word-style paragraph behavior.** Tab and
Shift+Tab at the start of a block keep indenting or outdenting, while either
key inside paragraph text inserts a tab. Long tabbed lines wrap at word
boundaries, restart their ruler on each visual line, and preserve custom,
aligned, leader, and right-to-left tab stops.
### Fixed
- **DOCX named styles remain stable through editing and repeated round trips.**
Imported paragraph styles and document defaults now drive newly created
headings and agent-created styled blocks, survive Yjs persistence, suppress
redundant direct formatting on export, and converge without accumulating
unused generated Word styles. Explicit formatting resets, borders, spacing,
fonts, and styles inside table cells remain intact.
- **Agent document measurements reconcile pending Review changes.** Primary
totals consistently measure the final, as-if-accepted document and include
the original review projection when pending suggestions make the visible
word or character count differ.
## 1.2.0
### Added
- **International text support now extends across the editing and conversion
pipeline.** Unicode-aware line breaking, search, list numbering, font
fallback, and IME composition preserve multilingual text more reliably, and
text import now detects UTF-8 and UTF-16 files. HTML, DOCX, RTF, ODT, and
plain-text conversions keep the expanded script coverage intact.
- **Orange and purple join the SDK highlight palette.** The editor UI,
`find_highlights`, `style_blocks`, agent HTML, and DOCX round trips now
share the same seven canonical highlight colors.
- **Agent styling covers richer document structures.** `style_blocks` adds
span, descendant-wildcard, first-row, and named-paragraph-style selectors,
with durable mappings for image sizing and alignment, paragraph borders,
nested tables and lists, per-edge cell borders, cell fills, padding, and
table-cell text styles.
### Fixed
- **DOCX import follows Word's effective paragraph and table styling.**
Unstyled paragraphs inherit the document's default style; exact and
at-least line rules use the resolved font size; paragraph before/after and
contextual spacing replace the editor's fallback gap; missing table borders
remain borderless; and non-uniform cell margins survive import. The imported
spacing persists through Yjs, paragraph splits and merges, HTML, DOCX, and
PDF output, keeping compact Word layouts from growing onto extra pages.
- **Unavailable Word fonts use the document's own substitution hints.** DOCX
import reads `fontTable.xml` alternate names and PANOSE/family/pitch
metadata, persists them with the document, and chooses metric-compatible
fallbacks before resorting to a generic family.
- **Multilingual PDF export embeds and shapes the scripts it needs.** Chinese,
Japanese, Korean, Indic, Southeast Asian, Tibetan, Ethiopic, Armenian,
Georgian, Syriac, Thaana, symbols, emoji, and rare Han use bundled lazy
fallback faces, while a hidden logical-text layer preserves search and text
extraction.
- **Agent edits preserve complex formatting and pending review state.** Styled
tables, nested content, list-item formatting, explicit text overrides,
comments, and edits layered over pending insertions now survive preview,
acceptance, and rejection without duplicating or flattening structure.
- **Borderless layout tables stay visually borderless while editing.** The
canvas now reveals only the specific invisible vertical edge under a resize
pointer instead of ghosting the table's entire grid on hover or focus.
- **Imported pale-yellow highlights remain highlights.** HTML clipboard import
no longer remaps Word's pale yellow to the editor's green palette entry.
## 1.1.0
### Added
- **First-class right-to-left editing.** The built-in toolbar can set paragraph
and list direction to automatic, LTR, or RTL. Mixed Hebrew, Arabic, and
left-to-right text now uses bidi-aware line layout, visual arrow-key
movement, stable caret and selection geometry, direction-aware alignment and
indents, mirrored list markers, and RTL tab stops. Direction survives Yjs,
clipboard and agent edits, HTML, and DOCX round trips; PDF export preserves
mixed-direction reading order and positioning.
- **Multi-column sections.** Page layout now supports one, two, or three
newspaper-style columns with a configurable gap. Continuous section breaks
balance their final columns, while page and next-page sections keep ordinary
pagination. The canvas editor and PDF export share the flow rules, DOCX and
HTML import/export preserve the settings, and `set_page_layout` exposes
`columnCount`, `columnGap`, and section-scoped updates to browser and
server agent sessions.
- **Native document-structure tools.** `insert_block` can create a live table
of contents with ` `. The canonical
`remove_blocks` tool can delete a counted range, exact IDs, a saved search
result, or an explicitly authorized document tail with `through_end`.
### Changed
- **Agent tool sessions avoid unproductive read cycles.** Sequential reads now
reject windows containing only blocks already returned in the same request
with `no_new_read_context`; `read_specific_blocks` remains available for
intentional revisits. `remove_blocks` is now the model-facing name, while
the previous `remove_block` name remains an executable compatibility alias.
### Fixed
- **Strict-provider nulls behave like omitted optional tool fields.** Browser
and server tool execution normalize provider-materialized `null` values
from the generated schema before dispatch, without stripping required or
deliberately nullable values. Optional layout and mutation inputs therefore
no longer take invalid branches merely because a provider filled them in.
- **Agent-generated Unicode escapes become the intended text.** Literal
`\uXXXX` sequences in agent HTML and replacement paths are decoded without
disturbing escaped backslashes or code spans, preventing visible escape text
in edited documents.
- **Review decisions preserve the reader's place.** Accepting or rejecting a
tracked change no longer forces the embedded editor to scroll back to the
active suggestion.
- **SDK PDF export keeps Hebrew and Arabic text self-contained.** The package
now bundles the Noto fallback faces used by RTL export, so embedding hosts do
not need to mirror Revise's public font directory to avoid dropped glyphs.
## 1.0.1
### Fixed
- **Malformed model input can no longer crash a host's agent loop.** A bare
string where `search_document` expects a `queries` array — the most
common model slip — threw a raw `TypeError` out of `execute()` /
`executeDynamic()` instead of returning a structured failure. Bare
strings are now coerced to one-element arrays (also for
`read_specific_blocks`' `block_ids`), anything uncoercible fails
structurally, and both surfaces gained a safety net that converts an
unexpected handler throw into an `internal_error` result. The documented
contract — expected failures as results, only environment errors reject —
now holds for arbitrary input.
- **Invalid enum values no longer mutate the wrong target silently.** An
insert tool called with `position: "above"` placed content _after_ the
reference block while reporting success, and `set_header_footer` with a
misspelled or missing `side` edited the header. Both now return a
structured failure naming the valid values.
## 1.0.0
One package, one tool contract. The browser and server surfaces now speak
the same canonical envelope, so host result-handling code is shared verbatim
between web and Node. That convergence is breaking on the browser side —
every change is listed below.
### Breaking
- **`tools.execute()` returns the canonical discriminated envelope.** The
flat `{ toolCallId, name, success, error?, agentFeedback?, output?,
documentContent? }` result is gone. Both surfaces now return
`{ ok: true, value: { callId, tool, message, data, context, suggestionIds } }`
or `{ ok: false, error: { callId, tool, code, message } }`
(`ReviseToolResult`). Field mapping: `toolCallId → value.callId`,
`name → value.tool`, `agentFeedback → value.message`,
`output → value.data`, `documentContent → value.context.html`;
`view_image`'s attachment lives on `value.image`.
`ReviseEditorToolExecutionResult` no longer exists.
- **`tools.execute()` is typed and no longer routes `document_id`.**
`execute()`/`call()` take the generated per-tool input types and
cover the shared 24-tool contract; untrusted model calls — including the
browser-only `get_selection`, `view_image`, and
`revise_run_agent`, and model-facing `document_id` routing — go
through the new `executeDynamic()`, exactly as on the server.
- **Unknown tools and role denials are results, not throws.** `execute()`
returns `{ ok: false }` with `code: "unknown_tool"` or
`"role_not_permitted"` instead of throwing; the new `tools.call()`
throws a typed `ReviseToolError` for hosts that prefer exceptions.
`ReviseRoleError` is still thrown by `agent.run()` and UI
controllers.
- **ID-keyed suggestion decisions return per-ID outcomes.**
`review.acceptSuggestions`/`rejectSuggestions`,
`review.acceptCommentSuggestions`/`rejectCommentSuggestions`, and
`tools.acceptAllSuggestions`/`rejectAllSuggestions`, and the server
session's `acceptSuggestions`/`rejectSuggestions` return
`{ resolved, missing, unresolved }` (`ReviseSuggestionDecision`)
instead of a bare count. Stale IDs land in `missing`; a role that may
not resolve reports everything `unresolved`. `review.acceptAll()` /
`rejectAll()` keep their boolean UI-gesture contract.
- **Server sessions default to suggesting mode.** `createServerDocumentSession`
without `mode` now proposes tracked changes instead of applying edits
directly — the same default posture as the browser surface, and the safe
one. Pass `mode: "editing"` (or per-call `directMode: true`) for
direct application.
### Added
- **Browser mutation results report created suggestion IDs.** Successful
mutations carry `suggestionIds` — the tracked records that call created
(empty for direct edits, `null` for read/search/measure tools) — with a
concurrent human suggestion never attributed to the tool call. Feed them
straight to `review.acceptSuggestions()`.
- **The tool contract types ship from both entries.** `ReviseToolResult`,
`ReviseToolResponse`, `ReviseToolFailure`, `ReviseToolError`,
`ReviseSuggestionDecision`, the generated `ReviseToolInputMap`, and
friends are exported by `@reviseio/sdk` and `@reviseio/sdk/backend`
alike.
- **Server mutation results report the tracked records they created.**
Successful mutation calls from `createServerDocumentSession` now carry
`suggestionIds` — the tracked suggestion records that call created (empty
for direct edits, `null` for read/search/measure tools) — so a host can
persist per-edit IDs with its review workflow instead of diffing the
document-global pending set.
- **`listSuggestions()`** returns every pending suggestion as a reviewable
record with authorship metadata (`authorType`, `agentName`,
`agentModel`, `source`, `label`, `createdAt`), so hosts can decide on
their own agent's suggestions and leave collaborators' pending work alone.
- **`acceptAllSuggestions()` / `rejectAllSuggestions()`** make the
whole-document decision an explicit, greppable call instead of the
`acceptSuggestions(getPendingSuggestionIds())` idiom.
### Fixed
- **Server tool types now resolve for `moduleResolution: "NodeNext"`
consumers.** The packaged declarations re-exported the server tool contract
through extensionless relative specifiers, which NodeNext cannot resolve —
every tool input/output type silently degraded to `any`, and importing a
contract type by name (e.g. `ServerDocumentSession`) failed to compile.
The declarations now use explicit `.js` specifiers, which every supported
resolution mode maps to the sibling `.d.ts` files.
- **Importing a document with code blocks no longer risks crashing a Node
host.** The canvas syntax highlighter tried to fetch its tree-sitter wasm
under Node (the server DOM shims install a jsdom `window`, defeating its
browser check), failing as an unhandled promise rejection. Highlighting now
recognizes the server runtime explicitly and stays off, and a failed
highlighter init in the browser no longer poisons later attempts.
- **Table and list mutations no longer spam `Invalid access` warnings.**
Building table rows, cells, and list items called Yjs `push()` on
elements not yet integrated into a document, which logs a warning per
child. Construction now inserts children at explicit indices — dozens of
stderr lines per table insert in server logs, gone.
- **Editing mode applies server edits directly again.** A server session in
`"editing"` mode quietly recorded every mutation as a
pending tracked suggestion instead of applying it. Editing-mode calls now
run in the runtime's direct mode and settle within the call, matching the
documented behavior; an explicit per-call `directMode` still overrides in
either direction.
## 0.9.0
### Added
- **`review.listChanges()` now reports linked moves as moves.** A move pair
surfaces as ONE `ReviseTrackedChange` with `kind: "move"`,
`moveSourceBlockIds`/`moveDestinationBlockIds` locating each half, and
`deletedText`/`insertedText` carrying the text as it left and as it
arrived. Previously both halves were folded into a single change labeled
`"delete"`, leaving a host review panel no way to distinguish a move from
a replacement. Accept/reject semantics are unchanged: resolving the change
settles both locations atomically.
## 0.8.0
### Added
- **Cut and paste now creates native linked moves in Suggesting mode.** Cutting
text from supported paragraphs and pasting that same internal clipboard
payload elsewhere in the document produces one atomic “Moved from”/“Moved
here” pair. Accepting or rejecting either half resolves both locations, undo
cannot strand an orphaned half, and DOCX export/reimport preserves native
linked move markup. Copy/paste, editing mode, reused or mismatched clipboard
payloads, and unsupported structural selections continue to use ordinary
insertion/deletion behavior.
## 0.7.0
### Added
- **Server-side semantic tools.** `await createServerDocumentSession(ydoc,
{ documentId, mode })` in `@reviseio/sdk/backend` binds the canonical
document-local agent tools — read, search, measure, mutate, layout,
footnotes, tables, comments — to a host-owned Y.Doc under plain Node.
`mode: "suggesting"` produces Word-compatible tracked changes with
accept/reject; concurrent calls are serialized; literal tool calls infer
their schema inputs and structured outputs. See the README's "Server semantic
tools" section.
- **Linked Word moves now round-trip as atomic move suggestions.** DOCX import
pairs native `w:moveFrom` and `w:moveTo` ranges across runs, paragraphs,
and table cells. Accepting either half keeps the text at its
destination; rejecting either half restores its original location; export
re-emits native paired move markup. Suggestion cards distinguish “Moved from”
and “Moved here”, and focusing either half highlights its partner even across
paragraphs.
### Fixed
- **Node-side DOCX imports preserve tracked table-row revisions without a
browser `FileReader`.** The backend DOM shims and table-revision
postprocessor now work from `Blob.arrayBuffer()`, retaining row insertion
and deletion metadata in server integrations.
- **Word comment anchors preserve their identity and semantic range.** Safe
native numeric IDs no longer shift, cross-paragraph and table-cell ranges
emit one anchor triple instead of duplicates, and collapsed, overlapping,
threaded, and resolved comments survive repeated round trips.
- **Visible drawing text survives DOCX import when shape geometry is
flattened.** Inline and anchored DrawingML, grouped shapes, legacy VML,
headers, and standards-valid `mc:AlternateContent` Choice/Fallback content
become editable paragraphs in reading order without duplicate fallback text.
- **DOCX archives no longer inflate and large imports avoid repeated work.**
Generated packages use DEFLATE compression, parse the main document XML once,
and skip move indexing when a file has no moves, preventing the reported
multi-fold output growth and superlinear no-move scan.
## 0.6.0
### Added
- **Horizontal rules and ornamental dividers are now supported throughout the
editor.** Insert thin, thick, double, dashed, dotted, star, or diamond
dividers from the toolbar or context menu, or create them with text triggers
such as `---`, `===`, `-**-`, and `-<>-`. DOCX, HTML, Markdown, agent HTML,
PDF, and plain-text output preserve the divider where the format permits;
DOCX, HTML, Markdown, RTF, and ODT imports recognize their native divider or
paragraph-border representations.
### Changed
- **Em-dash autocorrection now waits for a word boundary.** Typing `--` remains
literal until Space or Enter, allowing `---` to be used for horizontal-rule
insertion and preserving word-initial values such as `--flag`.
### Fixed
- **Words no longer wrap in the middle at formatting or tracked-change run
boundaries.** A word split across differently styled text nodes now wraps as
one unit and keeps a stable layout when suggestions are accepted.
- **Suggestion editing produces stable runs and caret placement.** Continued
typing or backspacing merges into the existing suggestion instead of creating
one run per keystroke, replacement normalization no longer leaves the caret
inside hidden deleted text, and Enter after a pending deletion creates the
expected paragraph or list item without moving the deleted content.
- **Structural editing keeps the expected document shape and keyboard focus.**
Backspace exits or splits empty list items correctly, compatible lists rejoin
when their separating empty paragraph is removed, Enter at the start of a
leading heading or code block creates a body paragraph above it, and toolbar
commands no longer steal keyboard focus from the editor.
## 0.5.1
### Fixed
- **PDF table-of-contents page numbers stay accurate when a heading moves to
the next page.** Export now records a heading's destination after paragraph
pagination, so a keep-with-next or end-of-page preflight cannot leave the
TOC pointing at the page the heading would have occupied before it moved.
## 0.5.0
### Added
- **Comments now work on list items, including nested items.** Selecting list
text, expanding a word from the caret, and commenting on a whole item all
create item-local anchors. Returning the caret to that text activates the
thread, list-item threads survive agent-HTML round trips, and the
`leave_comment` agent tool accepts list-item IDs.
### Changed
- **PDF export now follows the editor's layout and print semantics much more
closely.** It shares wrapping and pagination rules for tab stops and
leaders, hyphenation, keep-with-next, keep-lines, widows and orphans, code
blocks, nested tables, footnotes, page and section breaks, live TOC page
numbers, line numbers, and watermarks. Editor-only placeholders and review
chrome are not printed, and Word hidden text remains hidden in ordinary PDF
output. The SDK now carries its metric-compatible PDF fonts itself, so an
embedding host does not need to mirror Revise's public font directory.
- **Agent tool sessions reject redundant full-document read loops.** After a
session has read the complete document, another sequential `read_document`
returns `no_new_read_context`; agents can still revisit known content with
`read_specific_blocks`.
### Fixed
- **DOCX formatting exceptions survive a complete edit and export cycle.**
Explicit run-property clears over named styles, zero paragraph indents,
paragraph border and shading clears, decimal font sizes, and mixed small-cap
and all-cap overrides now remain distinct from inherited formatting and do
not leak into adjacent text.
- **Complex Word structures no longer lose modeled content on re-export.**
Internal bookmark links stay internal; exact table widths, columns,
alignment, cell padding, and vertical alignment survive; nested tables keep
the paragraphs around them; display equations are not flattened when a file
is immediately re-exported; and multi-block footnotes and endnotes can retain
their lists and tables.
- **Accepting or rejecting a compound agent edit is atomic.** Mixed text,
formatting, and structural suggestions are resolved together, so accepting
a rewrite no longer retains deleted fragments and rejecting it restores the
original content and formatting.
- **Comment cards stay where users put them.** A previously active card no
longer drifts with the viewport, and selecting text inside a card no longer
jumps focus back to the document or clears the selection. Inline code in
comment bodies is also styled as code.
- **Package managers can no longer omit the Yjs runtime.** Every editor session
is Yjs-backed even without a collaboration provider, so `yjs` and
`y-protocols` are now required peers instead of optional peers. This makes
package managers install or validate them instead of letting bundlers
substitute empty optional-peer modules and fail the consumer build.
## 0.4.5
### Fixed
- **Starting a list no longer drops the font.** Typing a list trigger
(`- `, `1. `, `[] `) in a paragraph set in a non-default font produced
a list item that fell back to the default font: clearing the trigger text
left an empty item with nowhere to carry its formatting. The formatting at
the trigger's trailing edge — the font family included — is now preserved
on the empty item and applies to the next character typed.
## 0.4.4
### Fixed
- **Suggestion cards now hang from the suggested text.** The floating
accept/reject card anchored to the caret; it now follows the suggested
fragment (or your selection) and sits centered below its line, the same
placement the revise.io app uses, falling back to the caret only when the
fragment cannot be measured. Wide cards are clamped to the page edges
instead of a fixed margin.
## 0.4.3
Identical in content to 0.4.2; republished.
## 0.4.2
### Fixed
- **Word-level diffs misplaced edits next to repeated words.** When the text
adjacent to an edit repeated a word from the edit itself (replacing
"[Berkshire County / appropriate Massachusetts county]," with "Suffolk
County, Massachusetts," just before "Massachusetts will"), the unchanged
suffix could be drawn as deleted and retyped. Unchanged repeated words now
stay anchored as unchanged, in tracked changes and the diff view alike.
- **Writing a comment no longer collapses the comments margin.** Finishing a
draft that had itself opened the margin always collapsed it — even when
submitting had just created a thread, so the pages jerked sideways in both
directions and hid the card the user just wrote. The margin now stays open
on the new thread; only an abandoned draft with no other open thread
collapses it.
- **Comment card placement around drafts and tracked changes.** A comment
draft started while hovering the card stack could pin its card — and drag
the viewport — to the top of page 1; it now anchors where the draft was
made. A caret inside a resolved thread's highlight, or inside an imported
thread's replies, activated no card at all; it now activates the right
open thread. And when a commented paragraph also contains tracked changes,
the card describes the change under the cursor instead of jumping to the
top of the paragraph.
- **The selection card no longer chases the pointer mid-drag.** While the
mouse button is down nothing pops up under the pointer; the card appears
on release, anchored below the selection the user meant.
## 0.4.1
### Fixed
- **Opening the export menu crashed.** An icon component in the export menu
referenced React without importing it, which the packaged build has no
global to fall back on.
## 0.4.0
### Changed
- **The default entry no longer ships Tree-sitter syntax highlighting or the
embedded WASM payload.** Code blocks still render as code, and every WASM
hot path has a TypeScript implementation. This keeps the normal
integration smaller and compatible with strict Content Security Policies.
Optional features now load as async chunks inside the package.
### Added
- **`@reviseio/sdk/full`** — an opt-in entry point that keeps token-colored
code blocks and the WASM hot paths. The API and stylesheet are identical
between the two entries.
## 0.3.4
### Fixed
- **The SDK no longer attempts any telemetry.** Earlier builds emitted
editor health metrics toward a Revise backend that does not exist in your
deployment, which could only fail — loudly, as console CORS errors — and
represented network calls you never asked for. Telemetry is now something
only the revise.io application itself can switch on; embedded editors
send nothing anywhere: no metric POSTs, no analytics vendor code, no
beacons.
### Added
- **`onAnalyticsEvent`** — the editor's own instrumentation, delivered to
the host instead:
` ...} />`.
Forward events to your own analytics service; a throwing handler never
breaks the editor. Event names and property shapes are internal and
version-unstable.
## 0.3.3
Four suggesting-mode fixes, two of them content corruption. If your users
edit in `suggesting` mode — the mode this SDK exists for — take this
release.
### Fixed
- **Pasting in suggesting mode corrupted the document.** The paste path
predated suggesting mode: it duplicated the rest of the paragraph and
inserted the pasted text without tracked-change marks, so the paste was
invisible to review and survived reject. Pasting is now recorded exactly
as if the pasted characters had been typed — an insertion suggestion, plus
a deletion suggestion for any replaced selection.
- **Formatting across a pending change destroyed it.** Applying bold (or any
inline format) over a range touching a pending insertion or deletion
overwrote the change's identity; resolving the format then silently turned
a pending insertion into accepted text, or resurrected deleted text.
Formatting now leaves deletion spans alone, folds into insertion spans (as
Word nests run properties inside `w:ins`), and records a reviewable
format change only on unmarked text.
- **Accepting a deletion that crossed a paragraph boundary left the
paragraphs unmerged.** The boundary is now part of the suggestion, as the
pilcrow is in Word: accepting merges the paragraphs, in any resolution
order, for any number of paragraphs in the selection.
- **Copying a selection inside your own pending insertion copied nothing.**
It now copies the selected text. Clipboard payloads are also sanitized so
tracked-change marks never travel with copied content.
### Console
- Opening a document no longer logs to the console (previously one Yjs
warning per block plus two internal debug lines). `console.warn` and
`console.error` remain for genuinely actionable problems.
## 0.3.2
### Fixed
- `editor.whenReady()` was missing from the published type declarations. It
worked at runtime, but TypeScript rejected the call the documentation tells
you to make.
## 0.3.1
### Fixed
- Using the editor handle from `onReady` no longer throws. `onReady` fires
before any document exists, and "subscribe to everything on ready" is the
first thing a host writes — a subscription placed then now waits and
attaches when a document arrives. `selection.observe()` was affected too.
Methods that act on a document still fail loudly, and the message now names
the fix.
### Added
- `editor.whenReady()` — resolves once a document is open and its
controllers are usable.
- `REVISE_SDK_VERSION` and `REVISE_SDK_BUILD` exports, and a `bugs` entry
in the manifest: a support ticket can name the exact build it came from.
## 0.3.0
### Collaboration
Multi-user editing over a Yjs transport you own. The editor builds the
document; you attach the connection:
```ts
collaboration: {
connect: (doc, awareness) =>
new HocuspocusProvider({ url, name, document: doc, awareness }),
synced,
}
```
Remote carets, a presence roster (`editor.collaboration`), seed-or-join
semantics, and validation of any document you supply. See the collaboration
guide.
### Roles
`role="editor" | "suggester" | "viewer"`, editor-wide or per document, and
enforced on every surface: typing, the built-in chrome, the handle, and agent
tool calls. A suggester cannot leave suggesting mode, apply anything directly,
or accept and reject.
### Tracked changes you can list
`review.listChanges()` and `review.getChange(id)` return each pending change
with its kind, author, timestamp, affected blocks, and text — enough to build
your own review panel. Imported Word redlines keep their original reviewers.
### Server-side primitives
New subpath export `@reviseio/sdk/backend` (Node only): `parseDocument`,
`fileToYDoc`, `seedYDocFromFile`, `ydocToDocx`, `encodeYDoc`/`decodeYDoc`.
Your server can create a collaborative room from a .docx before any browser
connects, and export a live room without one. Requires `jsdom`.
### Breaking
- **`yjs` and `y-protocols` are now peer dependencies** and are no longer
bundled. Install them yourself, and make sure your bundler resolves ONE copy
— Yjs identifies its own types with `instanceof`, so a second copy breaks a
shared document. Only collaboration needs them; they are optional peers.
- **`ReviseDocumentInput.docx` is now optional**, since a document joining a
collaborative room needs no source file. Inputs must supply `docx`,
`collaboration`, or both.
### Fixed
- `search_result_id`, which the agent tool schemas advertise for bulk edits,
was unusable: tool state did not survive between `tools.execute()` calls.
- `exportDocx()` omitted TOC page numbers, which only the live layout knows.
- Exports lost imported named styles and reset core property dates in any
non-browser environment.
- `collaboration.synced` was read once at mount, so a host reporting sync
later was ignored and the document never opened.
- The margin insert-comment chip was missing from the embedded editor.
- Presence colors: an explicit `currentUser.color` was ignored whenever the
user had an id.
## 0.2.0
Multi-document sessions, host-owned chrome, skinning props, per-embed
theming, the selection controller, and comment-thread agents.
## 0.1.0
First release: the editor, DOCX in and out, tracked changes, comments, and
the agent tool surface.
Source: https://revise.io/developer/docs/editor-sdk/changelog
---
# Support
> How to report a problem, and what to send with it.
Email [sdk@revise.io](mailto:sdk@revise.io). `npm bugs @reviseio/editor` opens the
same route.
Response times, escalation paths, and service commitments are set in your
license agreement. Email us your requirements to include them.
## What to include
Include the SDK version and build:
```ts
import { REVISE_SDK_VERSION, REVISE_SDK_BUILD } from "@reviseio/editor/version";
console.log(REVISE_SDK_VERSION, REVISE_SDK_BUILD); // e.g. "2.0.1" "0eadd8b4"
```
Add them to your error reporting so every report includes them. Also include:
- **Steps**, in order. For example: "Opened a .docx with tracked changes,
accepted the second one, exported."
- **The document**, if you can share it. Fidelity bugs are usually specific to
a file. A redacted copy that still reproduces the bug is enough.
- **Browser and version**, and whether it reproduces in another.
- **Anything in the console**, including warnings before the error.
## Common problems
Two copies of Yjs are loaded. Yjs identifies its types with `instanceof`,
so a shared document rejects nodes built by the other copy. `npm ls yjs`
must show exactly one. See [collaboration](https://revise.io/developer/docs/editor-sdk/guides/collaboration#one-copy-of-yjs).
The handle was used before a document existed. `onReady` fires before the
first document opens. Subscriptions work there. Calls that act on a
document need `onDocumentReady` or `await editor.whenReady()`.
No `currentUser` was passed. Authorship is recorded when an edit is made,
not at export, so existing edits cannot be reassigned. Set `currentUser`
before anyone types.
Linked moves round-trip as moves. A complete `moveFrom`/`moveTo` pair
imports as one move suggestion and exports as the same paired markup. A
move whose halves are orphaned or mismatched in the source file falls back
to an ordinary deletion and insertion. See [tracked
changes](https://revise.io/developer/docs/editor-sdk/guides/tracked-changes) for what survives the round trip.
The SDK was published as `revise` through 2.2. From 2.3 it is
`@reviseio/editor`. See
[migrate from `revise`](https://revise.io/developer/docs/editor-sdk/installation#migrate-from-revise).
## Security
Report suspected vulnerabilities to [sdk@revise.io](mailto:sdk@revise.io) with
"security" in the subject. Do not report them publicly first.
Source: https://revise.io/developer/docs/editor-sdk/support
---
# Documents
> Opening, switching, and closing documents in a multi-document workspace.
## Open a document
A document is a `File`, `Blob`, or `ArrayBuffer` in any of the [supported
formats](https://revise.io/developer/docs/editor-sdk/guides/formats). Pass documents in `initialDocuments`, or
open them at runtime:
```ts
const doc = await editor.documents.open(
{ id: "exhibit-a", title: "Exhibit A", source: file },
{ activate: true },
);
```
`open()` resolves to a `ReviseDocumentHandle` when the document is parsed and
ready. A new document becomes active by default. Pass `{ activate: false }` to
open it in the background.
You assign document IDs. Each must be stable and unique within one editor
instance. Callbacks, tool routing, and `forDocument()` use them. Do not reuse
an ID for different content.
### Per-document options
Any editor-wide option can be overridden per document:
```ts
await editor.documents.open({
id: "exhibit-a",
source: file,
format: "markdown", // optional; inferred from the filename or MIME type
title: "Exhibit A",
documentMode: "suggesting",
readOnly: false,
zoom: "fit-width",
defaultCommentsOpen: false,
agent: { model: "claude-opus-5-5" },
});
```
## Switch and close
```ts
editor.documents.activate("exhibit-a");
editor.documents.close("exhibit-a");
editor.documents.getActiveId(); // "contract-1"
editor.documents.list(); // [{ id, title, status, active, error }]
```
Closing a document does not save it. Export it first if you need the file.
## Observe the document list
```ts
useEffect(
() =>
editor.documents.subscribe((state) => {
setTabs(state.documents);
setActiveId(state.activeDocumentId);
}),
[editor],
);
```
Each entry has a `status` of `"opening"`, `"ready"`, or `"error"`, and an
`error` string when parsing failed. Use them to render tabs with loading and
error states.
## Controlled or uncontrolled
By default, the component tracks the active document. To control it from your
own state, pass `activeDocumentId` and handle `onActiveDocumentChange`:
```tsx
```
`defaultActiveDocumentId` sets the initial active document without controlling it.
## Export
```ts
const blob = await editor.tools.exportDocx(); // active document
const other = await editor.tools.exportDocx("exhibit-a"); // a specific one
```
## Read content
Read the document tree for your own logic, such as word counts, validation, or
autosave:
```ts
const doc = editor.getDocument(); // active
const doc2 = editor.getDocument("exhibit-a");
```
`onChange(documentId, document)` fires on every edit from the user, your
toolbar, or an agent.
`onChange` fires on every keystroke. Debounce before saving. Use
`measure_document` or a cheap check instead of walking the whole tree on
every call.
Source: https://revise.io/developer/docs/editor-sdk/guides/documents
---
# Input formats
> Import DOCX, ODT, RTF, Markdown, plain text, and HTML. Export DOCX, PDF, HTML, Markdown, and text.
The SDK accepts six input formats. All of them open in the same editor with
the same document model. They differ in how much of the source they preserve.
| Format | Extensions | Notes |
| --- | --- | --- |
| DOCX | `.docx` | Full fidelity, and the only format whose untouched content exports back as the original bytes |
| ODT | `.odt` | LibreOffice, OpenOffice, and Google Docs exports: formatting, lists, tables, images, footnotes, headers and footers, comments, tracked changes |
| RTF | `.rtf` | Word, WordPad, TextEdit, and LibreOffice: formatting, lists, tables, images, footnotes, headers and footers, comments, tracked changes |
| Markdown | `.md`, `.markdown` | Headings, lists, tables, code, emphasis, links |
| Plain text | `.txt` | Paragraphs split on blank lines |
| HTML | `.html`, `.htm` | Headings, lists, tables, links, inline formatting |
```tsx
```
Earlier versions named this field `docx`. That name still works; pass one
or the other.
## Format detection
The SDK checks, in order:
1. An explicit `format` on the document input.
2. The filename extension, when the source is a `File`.
3. The MIME type, when the source is a `Blob` that carries one.
4. DOCX, as the fallback.
## Set the format
A `Blob` or `ArrayBuffer` built in memory has no filename, and its MIME type
may be missing or wrong. Pass `format`:
```tsx
```
```ts
await editor.documents.open({
id: "imported",
source: await response.blob(),
format: "html",
});
```
A source with no `format`, filename, or MIME type is parsed as DOCX. Any
other content then fails to parse. Pass `format` when you build the source
yourself.
The MIME types recognized for the binary formats are
`application/vnd.openxmlformats-officedocument.wordprocessingml.document`
(DOCX), `application/vnd.oasis.opendocument.text` (ODT), and
`application/rtf`, `text/rtf`, `application/x-rtf`, or `text/richtext` (RTF).
## Format details
Preserves styles, fonts, tables, images, sections, page setup, headers
and footers, footnotes and endnotes, comments, and tracked changes. All of
them are written back on export.
Formatting is resolved through the document's styles and applied directly
to the text: fonts, sizes, colors, highlighting, bold, italic, underline,
strikethrough, superscript and subscript, small caps, and letter spacing;
paragraph alignment, indents, spacing, line height, borders, shading, and
page breaks. Lists keep their numbering, including start values and
numbering that continues across interruptions. Tables keep merged cells,
column widths, and cell shading. Images, footnotes and endnotes, running
headers and footers, comments, tracked insertions and deletions, and page
size and margins all come in.
Floating pictures and text boxes are placed inline where they are
anchored; charts and embedded objects come in as their preview image when
the file carries one. Named styles are not kept as styles, only their
effect.
Reads the RTF that Word ("Save as RTF"), WordPad, TextEdit, and
LibreOffice write: fonts, sizes, colors, highlighting, and the full range
of character formatting; paragraph alignment, indents, spacing, line
spacing, borders, and shading; headings from Word's heading styles; lists
from Word's list table and from WordPad's older bullet markup; tables with
merged cells and column widths; PNG, JPEG, and Windows metafile pictures;
hyperlinks; footnotes and endnotes; headers and footers; comments; tracked
changes; and page size and margins. Text in any Windows code page or East
Asian encoding decodes correctly.
Tables nested inside table cells are flattened into the outer cell, and
multi-column sections import as a single column.
Headings, lists, and tables become the matching document blocks. Markdown
has no page setup or revision marks, so the document opens with default
page setup and no tracked changes.
Each block of text separated by a blank line becomes a paragraph.
Structural markup maps to document blocks. Scripts, styles, and layout CSS
are ignored.
## Titles
Without a `title`, the SDK uses the filename without its extension. `Master Agreement.docx` becomes "Master Agreement".
## Export
`tools.export()` produces a file in any of five formats:
```ts
const docx = await editor.tools.export(); // .docx, tracked changes intact
const clean = await editor.tools.export({ format: "docx", mode: "final" });
const pdf = await editor.tools.export({ format: "pdf" });
const html = await editor.tools.export({ format: "html" });
const markdown = await editor.tools.export({ format: "markdown" });
```
| Format | Notes |
| --- | --- |
| `docx` (default) | Pending tracked changes and comments export as native Word revisions and comments. Untouched content from an imported .docx leaves as the original bytes. |
| `pdf` | Paginated like the editor, fonts embedded. Browser only. |
| `html` | A standalone page with styles. |
| `markdown` | Headings, lists, tables, links, code, and math. |
| `txt` | Plain text. |
Two modes decide what happens to pending changes:
- `"review"` (default): a .docx keeps pending tracked changes and comments;
the other formats show the text with changes accepted.
- `"final"`: every pending change is accepted and every comment dropped
first — a clean copy to send out. In the editor, a final .docx is
regenerated rather than grafted onto the original file, so none of its
revisions or comments come back. On the server, a final .docx keeps the
original file's bytes for content the accepted changes didn't touch.
On the server, [`revise`](https://revise.io/developer/docs/revise/library)'s `doc.export({ format, mode })`
takes the same options, adds `pdf`, and reports what a .docx export kept from
the original file.
A document imported from ODT, RTF, Markdown, or HTML exports as a valid
`.docx`. ODT and RTF are import-only.
Source: https://revise.io/developer/docs/editor-sdk/guides/formats
---
# Chrome and toolbars
> Use the built-in ribbon or build your own toolbar.
All UI the SDK draws around the document is optional. Every control's action
is also available from the handle.
## Optional chrome
| Chrome | Prop | Default | Drive it yourself with |
| --- | --- | --- | --- |
| Tab strip | `showTabs` | off | `documents` |
| Title bar | `showTitleBar` | on | `view` |
| Ribbon | `toolbarMode` | `"native"` | `toolbar` |
| Editing/Suggesting toggle | `showDocumentModeToggle` | off | `view.setDocumentMode()` |
| Comments panel | None | None | `view.setCommentsOpen()` |
The comments panel has no built-in button. Comment chips render next to the
page, and your UI opens or closes the panel.
## Ribbon modes
| `toolbarMode` | What it renders |
| --- | --- |
| `"native"` | The full ribbon. |
| `"compact"` | The everyday-formatting subset, centered, for narrow fixed-width hosts (about 760px): undo and redo, font, size, styles, paragraph, links, indents, lists, Find, and Export. |
| `"floating"` | The full ribbon as a centered card over the canvas, as in the Revise app. |
| `"none"` | No toolbar. Build your own (below). |
## Custom toolbar
Set `toolbarMode="none"`, subscribe to toolbar state, and render your own
controls:
```tsx
const [state, setState] = useState(null);
const editor = useRef(null);
{
editor.current = handle;
handle.toolbar.subscribe(setState);
}}
/>;
editor.current?.toolbar.toggleBold()}
>
Bold
;
```
`ReviseToolbarState` includes:
`ready`, `readOnly`, `documentMode`, `hasSelection`, `canUndo`/`canRedo`, the
`formatting` marks at the caret, `heading`, `alignment`, `lineSpacing`,
`fontSizePt`, the current `blockType`, and a `review` summary.
Subscribe once and call the returned unsubscribe function on unmount. State
is published on every selection change.
### Toolbar commands
`undo`, `redo`, `copy`, `copyAs("plaintext" | "markdown" | "html")`, `cut`,
`paste`, `pasteTextOnly`, `pasteFormattingOnly`
`toggleBold`, `toggleItalic`, `toggleUnderline`, `toggleStrikethrough`,
`toggleDoubleStrikethrough`, `toggleSmallCaps`, `toggleAllCaps`,
`toggleCode`, `toggleLatex`, `toggleSuperscript`, `toggleSubscript`,
`clearFormatting`, `setFontFamily`, `setFontSize`, `setLetterSpacing`,
`setTextColor`, `setHighlightColor`
`setHeading`, `setAlignment`, `setLineSpacing`, `toggleList`, `createLink`,
`increaseIndent`, `decreaseIndent`
`insertTable`, `insertImage`, `insertContainer`, `insertCodeBlock`,
`insertMathBlock`, `insertDiagram`, `insertFootnote`, `insertPageNumber`,
`insertPageBreak`, `insertSectionBreak`
`openReview`, `closeReview`, `reviewPrevious`, `reviewNext`,
`acceptCurrent`, `rejectCurrent`, `acceptAll`, `rejectAll`,
`setShowRemovals`, `setSuggestionViewMode`
A command returns `false` when it cannot apply, for example in a read-only
document, with no selection, or with nothing to undo.
## View controller
The `view` controller sets the title, document mode, and panels:
```ts
const { title, documentMode, commentsOpen, reviewOpen, reviewTargetCount } =
editor.view.getState();
editor.view.setTitle("Master Services Agreement");
editor.view.setDocumentMode("suggesting");
editor.view.setReviewOpen(true);
editor.view.setCommentsOpen(true);
```
An Editing/Suggesting toggle:
```tsx
const suggesting = view?.documentMode === "suggesting";
editor.current?.view.setDocumentMode(suggesting ? "editing" : "suggesting")
}
>
{suggesting ? "Suggesting" : "Editing"}
;
```
## Built-in tabs
Turn on the built-in document tabs with `showTabs`:
```tsx
openBlankDocument()}
initialDocuments={docs}
/>
```
The **+** button renders only when you pass `onNewTab`.
## Focus
The editor takes keyboard focus when a document becomes active. To focus it
only on click, for example in a preview or split view, set `autoFocus={false}`:
```tsx
```
Source: https://revise.io/developer/docs/editor-sdk/guides/chrome
---
# Appearance
> Theme, colors, fonts, and zoom.
## Theme
The `theme` prop sets the editor theme independently of the host page. Several
editors on one page can use different themes, and a dark editor can sit in a
light application.
```tsx
```
Without `theme`, the editor follows the host page.
The theme applies to the editor and its canvas only. It does not change the
surrounding page.
## Colors
Three surfaces have their own colors. Set each with a prop or with a CSS
custom property on the editor root.
| Prop | CSS variable | Surface |
| --- | --- | --- |
| `canvasBackground` | `--revise-canvas-background` | The area behind the page |
| `pageBackground` | `--bg-page` | The page |
| `pageBorderColor` | `--page-border` | The outline around each page |
```tsx
```
All three accept any CSS value, including gradients for the canvas:
```tsx
```
`pageBackground` also applies inside the canvas. Image placeholders,
container fills, and table cell backgrounds use your color instead of
white.
## Fonts
Add your application's fonts to the font picker. The DOCX family name and the
browser font stack are set separately:
```tsx
```
`family` is stored in the document and written to `.docx`. `cssFamily` is used
only to render the font in the browser.
The SDK does not load font files. Load them with `@font-face` or a
stylesheet. A family that is not loaded renders in a fallback font and still
exports correctly.
## Zoom
Zoom accepts any positive scale or `"fit-width"`:
```tsx
```
```ts
editor.zoom.setZoom(1.25);
editor.zoom.fitWidth();
const { zoom, scale } = editor.zoom.getState();
```
`zoom` is the requested value. `scale` is the actual canvas scale. For
`"fit-width"`, `scale` is computed from the widest page and updates when the
container or the comments stack changes. An expanded comments stack reduces
the available width. A numeric zoom stays fixed, and the editor scrolls
horizontally.
Subscribe to zoom changes:
```ts
useEffect(() => editor.zoom.subscribe(setZoomState), [editor]);
```
## Performance
Word-level diffing, agent text anchoring, and document statistics have
Rust/WASM implementations. They are off by default:
```tsx
```
The binary is included in the package. There is nothing to host or configure.
If WebAssembly is unavailable, the editor uses the TypeScript implementations.
Both produce the same output.
## Container size
The editor fills its container. Give it a sized parent:
```tsx
```
`className` and `style` are passed to the editor root.
Source: https://revise.io/developer/docs/editor-sdk/guides/appearance
---
# Tracked changes
> Suggestions your users can accept or reject, from people and agents alike.
## Modes
A document in `suggesting` mode records every change as a tracked suggestion
instead of applying it. This covers typing, toolbar commands, and agent tool
calls. An agent can draft in this mode, and the user reviews each change.
```ts
editor.view.setDocumentMode("suggesting");
```
Set the starting mode per document, or editor-wide:
```tsx
```
`defaultDocumentMode` sets the starting mode only. Changing the prop later
does not affect a document that is already open. Use `view.setDocumentMode()`
for that.
The user can switch modes. To lock a participant into suggesting, use a
[role](https://revise.io/developer/docs/editor-sdk/guides/roles) instead. `role="suggester"` pins the mode,
refuses direct edits, and hides accept and reject everywhere.
## Reviewing
The `review` controller is the host API for tracked changes. It is not an
agent tool: it exposes interactive state and moves the caret and viewport.
```ts
const state = editor.review.getState();
// { open, targetCount, openSuggestionIds, currentSuggestionIds, ... }
editor.review.open();
editor.review.next();
editor.review.acceptCurrent();
editor.review.rejectCurrent();
editor.review.acceptAll();
```
Subscribe to render your own review UI:
```ts
useEffect(() => editor.review.subscribe(setReviewState), [editor]);
```
### Targeted operations
To act on specific suggestions, such as everything one agent run produced,
pass their IDs:
```ts
const ids = editor.review.getOpenSuggestionIds();
editor.review.navigateToSuggestion(ids[0]);
editor.review.previewSuggestions(ids, "accept"); // preview only, no changes
editor.review.acceptSuggestions(ids); // { resolved, missing, unresolved }
```
`previewCurrent()` and `previewAll()` do the same for the current stop and the
whole document. Pass `null` to clear a preview.
### Display
```ts
editor.review.setShowRemovals(false);
editor.review.setSuggestionViewMode("final"); // "all-markup" | "final" | "original"
```
`"final"` shows the document with every suggestion accepted. `"original"`
shows it with none accepted. Neither changes the document, so either works as
a read-only "clean copy" toggle.
## Direct mode
To apply an agent edit without tracking it, such as a formatting sweep or a
find-and-replace the user asked for, pass `directMode` per call:
```ts
await editor.tools.execute(
"style_blocks",
{ selectors: "*", attrs: [{ name: "fontFamily", value: "Georgia" }] },
{ directMode: true },
);
```
In a delegated agent run, every tool call in the loop inherits the mode. One
flag covers the whole task.
Direct mode skips tracked changes. Use it only for changes the user already
approved in your UI.
## Attribution
Each suggestion records its author, and agent suggestions are marked as such.
`review.getState().visibleAgentSuggestionIds` lists only the agent's
suggestions, and `nextAgentSuggestion()` steps through only those. Use them to
review the agent's changes without the user's own edits.
Changes made through `tools.execute()` and agent runs are agent changes,
signed **"Revise Agent"**. To sign them as your product's
assistant, name it:
```tsx
```
People's own edits use `currentUser`.
## Export
`exportDocx()` writes tracked changes as Word revision marks. A reviewer who
opens the file in Word sees the same suggestions, and accepting them there
gives the same result as accepting them in Revise.
Suggestions use the identity you pass as `currentUser`. Without it, human
suggestions export with the reviewer name **"Anonymous"** and agent
suggestions as **"Revise Agent"**. The name is recorded at edit time, not at
export, so pass `currentUser` before anyone edits.
## Word round trip
DOCX import and export preserve revision marks. From a Word file, through the
editor, and back:
| Word markup | Round trip |
| --- | --- |
| `w:ins` / `w:del`, including nested insert-inside-delete | Preserved, as layered suggestions |
| Run formatting changes (`w:rPrChange`) | Preserved, with the prior properties, so rejecting reverts |
| Paragraph property changes (`w:pPrChange`) | Preserved, with the prior properties |
| Paragraph-mark (pilcrow) revisions | Preserved, as pending splits and merges |
| Tracked deletion of a section break | Preserved, both directions |
| Table rows, cells, and table properties | Preserved, as one resolvable change per table |
| Author, date, and Word revision ids | Preserved, both directions |
| Comments, threading, resolved state, anchors inside tracked runs | Preserved |
Tracked row insertions and deletions (`w:trPr` → `w:ins` / `w:del`), cell
revisions (`w:cellIns`, `w:cellDel`, `w:cellMerge`), and table property changes
(`w:tblPrChange`) import as resolvable suggestions. They export as the same
markup, with authors intact.
Linked moves (`w:moveFrom` / `w:moveTo` pairs) import as one move suggestion.
Accepting either half keeps the text at its destination. Rejecting either half
restores the original location. Export writes the paired move markup back. An
orphaned or mismatched half imports as an ordinary insertion or deletion, so
unrelated content is never resolved together.
## Custom review panel
`listChanges()` returns every pending change with the metadata a panel needs.
Use it to render the list yourself instead of using the built-in ribbon:
```ts
for (const change of editor.review.listChanges()) {
// { id, kind, author, authorType, createdAt, blockIds,
// insertedText, deletedText, description, agentModel,
// moveSourceBlockIds, moveDestinationBlockIds }
}
editor.review.getChange(id); // one row, or null once it is resolved
```
| Field | Value |
| --- | --- |
| `kind` | `"insert"`, `"delete"`, or `"format"` (as in Word), or `"move"` for a linked move pair |
| `author` | A person's name, `"Revise Agent"`, an external agent's name, or `"Anonymous"` when no identity was supplied. Imported Word redlines keep their original reviewer. |
| `blockIds` | The blocks the change touches. `navigateToSuggestion()` scrolls to these. |
| `moveSourceBlockIds`, `moveDestinationBlockIds` | Where moved text left and where it landed. `deletedText` and `insertedText` hold the text at each end. |
A replacement made in one operation, such as the agent's `replace` tool, is
one change with both `deletedText` and `insertedText` set. Its `kind` is the
one it was recorded with, usually `"delete"`, so check for both texts to show it
as a replacement. One accept or reject settles both halves. A move is also one
change for both halves, and one accept or reject settles both locations.
Wire a row to the controller with the ID:
```tsx
const verb =
change.kind === "move"
? "moved"
: change.kind === "format"
? "reformatted"
: change.deletedText && change.insertedText
? "replaced"
: change.kind === "delete"
? "removed"
: "added";
editor.review.navigateToSuggestion(change.id)}>
{change.author} {verb}{" "}
{change.deletedText ?? change.insertedText}
editor.review.acceptSuggestions([change.id])}>
Accept
```
`listChanges()` is not a subscription. It computes the list from the
document on each call, in one pass. Call it when you render, for example
after `review.subscribe()` reports that the counts changed.
Source: https://revise.io/developer/docs/editor-sdk/guides/tracked-changes
---
# Roles and permissions
> What a participant is allowed to do, enforced everywhere.
A **mode** is something the user switches between: editing or suggesting. A
**role** limits what the user can do. It cannot be changed from inside the
editor.
```tsx
```
| Role | Edits | Direct edits | Accept / reject | Comments |
| --- | --- | --- | --- | --- |
| `editor` (default) | Yes | Yes | Yes | Yes |
| `suggester` | As tracked suggestions only | No | **No** | Yes |
| `viewer` | No | No | No | No |
## Why a suggester cannot accept
Accepting a tracked change is an edit. If a suggester could accept their own
suggestions, every change could skip review by someone else.
A suggester can still navigate review, preview accept and reject without
changing the document, and comment.
## Enforcement
The role applies to every surface:
- **Typing and the toolbar.** The document stays in a mode the role permits.
`setDocumentMode()` clamps the requested mode: asking for `"editing"` as a
suggester sets `"suggesting"`. The `onDocumentModeChange` callback reports
the resulting mode.
- **The built-in chrome.** The mode toggle is hidden when the role has one
mode. The ribbon shows review navigation without accept and reject. The
inline accept and reject tooltip on each suggestion is hidden.
- **The handle.** `review.acceptCurrent()`, `acceptAll()`,
`acceptSuggestions()`, and the reject counterparts return `false` or a
decision with every ID `unresolved`. Comment mutations do nothing for a
viewer.
- **Agent tools.** `tools.execute()` returns
`{ ok: false, error: { code: "role_not_permitted" } }` for a mutation a
viewer cannot make, and for any `directMode` call from a suggester. A
delegated `agent.run()` is checked before it starts and throws
`ReviseRoleError` instead.
```ts
import { ReviseRoleError } from "@reviseio/editor";
const result = await editor.tools.execute("replace", input, { directMode: true });
if (!result.ok && result.error.code === "role_not_permitted") {
// A suggester can't apply changes directly; without directMode, the same
// call adds the edit as a suggestion.
}
try {
await editor.agent.run(task, { directMode: true });
} catch (error) {
if (error instanceof ReviseRoleError) {
// error.role === "suggester", error.action === "apply changes directly"
}
}
```
`directMode` applies a change without a tracked change, so a suggester cannot
use it. Without `directMode`, the same tool call adds the edit as a
suggestion.
## Read the role
```ts
editor.getRole(); // active document
editor.getRole("exhibit-a"); // a specific one
```
A role is a UI and API boundary, not a security control. It limits what this
browser can do through the SDK. It does not authenticate anyone. In a shared
room, each participant sets their own role. To make a role authoritative,
your server must assign it and your transport must reject writes that
violate it. Revise does not run a server here.
Source: https://revise.io/developer/docs/editor-sdk/guides/roles
---
# Comments
> Threads anchored to text, from your UI or your agent.
Comments are threads anchored to ranges of text. They render as chips beside
the page and are preserved in DOCX import and export. Your code can read and
write them.
## Read threads
```ts
const { commentThreads, activeCommentId, commentsOpen } =
editor.review.getState();
```
Each thread has a root comment, replies, an anchor, and related suggestions:
```ts
interface ReviseCommentThread {
root: CommentRecord;
replies: CommentRecord[];
anchor: { blockId: string; start: number; end: number } | null;
relatedSuggestionIds: string[];
}
```
## Add comments
Comment bodies are Markdown. Each method returns the new comment's ID, or
`null` when the target no longer exists.
```ts
// At the user's current selection
editor.review.addCommentAtSelection("Is this cap mutual?");
// At a known range
editor.review.addCommentToRanges(
[{ blockId: "b12", start: 0, end: 24 }],
"Confirm the notice period.",
);
// On a whole block, or attached to a suggestion
editor.review.addCommentToBlock("b12", "Needs legal review.");
editor.review.addCommentForSuggestion(suggestionId, "Why this wording?");
// Replies and lifecycle
editor.review.replyToComment(commentId, "Agreed. Updated.");
editor.review.setCommentResolved(commentId, true);
editor.review.deleteComment(commentId);
```
Each method takes optional mentions as an additional argument.
## Panel and selection
```ts
editor.view.setCommentsOpen(true);
editor.review.selectComment(commentId); // scrolls to and highlights the thread
editor.review.selectComment(null); // clear
```
The SDK has no comments button. Chips render inline, and your UI opens the
panel. See [chrome](https://revise.io/developer/docs/editor-sdk/guides/chrome).
Selecting text in an editable document shows an insert-comment chip in the
page margin. Clicking it starts a draft on the selection, like
`addCommentAtSelection`. The chip is hidden during a draft, in read-only
documents, and on selections that already have a thread.
## Comments and suggestions together
A comment can be linked to suggestions. A reviewer can then accept or reject
those suggestions from the comment:
```ts
const ids = editor.review.getRelatedSuggestionIds(commentId);
editor.review.acceptCommentSuggestions(commentId); // returns the count
editor.review.rejectCommentSuggestions(commentId);
```
## Agent comments
The `leave_comment` tool adds comments from an agent:
```ts
// Start a thread on one block with one anchor form
await editor.tools.execute("leave_comment", {
id: "b12",
anchor: { text: "capped at the fees paid in the preceding twelve months" },
comment: "Consider making this cap mutual.",
});
```
Other anchor forms are `{ start_text, end_text }` for a long range, and
`{ whole_block: true }` for structural or empty content.
To reply, call the same tool without an anchor:
```ts
await editor.tools.execute("leave_comment", {
reply_to_comment_id: threadId,
comment: "Agreed. The mutual version is market standard.",
});
```
`leave_comment` anchors only to text the agent has read. Call a read tool
such as `read_blocks_from_index` first, or the anchor does not resolve.
If the anchor text is not in the block you named, but exactly one block you
have read contains it, the comment goes there instead. The result reports both:
`data.blockId` is where it landed and `data.requestedBlockId` is the block you
asked for. Block IDs change when a file is imported again, so this keeps a
stale ID from failing.
Read tools return existing threads as `` elements in their
HTML. A new thread that overlaps an unresolved thread is rejected, and the
agent should reply to that thread instead. To add an overlapping thread
anyway, list the overlapping IDs in `acknowledge_existing_thread_ids`.
Tool comments are signed "Revise Agent" unless you set
[`agent.name`](https://revise.io/developer/docs/editor-sdk/guides/tracked-changes#attribution).
## Comment agents
The `comments` controller runs an agent scoped to one thread. Use it when each
thread is a separate conversation with the model:
```ts
await editor.comments.run(threadId, "Draft a redline that addresses this.");
editor.comments.steer(threadId, "Keep it to one sentence.");
editor.comments.cancel(threadId);
useEffect(() => editor.comments.subscribe(setThreadRuns), [editor]);
```
`getState(threadId)` returns the thread's run state. `subscribe()` publishes a
map of every active run.
Source: https://revise.io/developer/docs/editor-sdk/guides/comments
---
# Collaboration
> Real-time multi-user editing over your own transport, with your server as a participant.
Every document is a [Yjs](https://yjs.dev) CRDT from the start. A single-user
editor is a room with one participant. This gives you:
- **Real-time editing over your own transport.** Carets, presence, tracked
changes, and comments are collaborative by default. Your authentication and
infrastructure carry the traffic.
- **Word round trip after concurrent edits.** Edits merge as CRDT operations,
and the result exports to `.docx` with native tracked changes and comments.
- **A server in the same room.** [`revise`](https://revise.io/developer/docs/revise/library) runs the same
converters and document tools in Node. Your backend can create documents
before anyone opens them, edit live rooms without a browser, and propose
tracked changes for users to review.
The SDK never opens a socket. You own the transport, its authentication, and
its lifecycle. Pass the provider to make the document collaborative:
```tsx
import { HocuspocusProvider } from "@hocuspocus/provider";
const provider = new HocuspocusProvider({
url: "wss://collab.example.com",
name: "contract-1",
});
;
```
The editor edits the provider's document in place. The provider's awareness
carries carets and presence. The provider object marks which edits came from
other people.
## One copy of Yjs
Yjs identifies its own types with `instanceof`, so your application and the
SDK must resolve to **the same** Yjs module. Yjs is a peer dependency. Install
it yourself:
```bash
npm install yjs y-protocols
```
If the bundler gives each side a different copy, seeding a shared document
fails with `Unexpected content type in insert operation`. In Vite, keep the
SDK and Yjs on the same side of the dependency optimizer:
```ts vite.config.ts
export default defineConfig({
resolve: { dedupe: ["yjs", "y-protocols"] },
optimizeDeps: { exclude: ["@reviseio/editor", "yjs", "y-protocols"] },
});
```
Webpack and Next.js resolve a single copy when your lockfile has only one
`yjs`. Run `npm ls yjs` to check.
## Connect callback
The editor reserves eight top-level keys in the Yjs document (`content`,
`chrome`, `notes`, `comments`, `annotations`, `metadata`, `sdt-wrappers`,
`paragraphs`) and needs specific document settings. The recommended form
gives you a document and an awareness instance to connect:
```ts
collaboration: {
connect: (doc, awareness) =>
new HocuspocusProvider({ url, name: "contract-1", document: doc, awareness }),
synced,
}
```
Return the provider. The editor uses it to recognize edits from peers. Your
application still owns the connection, its authentication, and its lifetime.
## Supplying a Y.Doc
Some transports create the document themselves. Both of these forms work:
```ts
collaboration: { provider } // reads document/doc/getYDoc() + awareness
collaboration: { ydoc, awareness, remoteOrigin }
```
The editor checks a supplied document before writing to it:
| Document state | Result |
| --- | --- |
| Empty | Seeded |
| Holds a Revise document | Joined |
| Reserved keys hold other data | Refused |
A refused document reports the conflicting key:
```
Revise collaboration: this Y.Doc already uses "metadata", which the editor
reserves for its own document.
```
This happens if your application keeps one Yjs document per room for all of
its data and already uses a name such as `comments` or `metadata`. Give the
editor its own document, or keep your data in a separate one.
Use the explicit form for an unrecognized transport, a custom sync layer, or a
setup where the document and the connection are separate objects:
```ts
collaboration: {
ydoc,
awareness, // omit or pass null for no presence
remoteOrigin: myTransport, // see below
synced: isSynced,
seed: "if-empty",
}
```
The explicit form **requires** `remoteOrigin`: the transaction origin your
transport passes to `Y.applyUpdate`. Without it, the editor treats a peer's
edit as local. Cursor restoration breaks, and Ctrl+Z can undo someone else's
typing. A transport that applies updates with a `null` origin must be
wrapped to tag its own transactions. Passing `provider` handles this, since
most providers pass themselves as the origin.
## Seeding
The first participant writes the content. Everyone else joins it. `seed`
controls this:
| Value | Behavior |
| --- | --- |
| `"if-empty"` (default) | If the shared document is empty, the `source` document is written into it. Otherwise the source is ignored and the editor joins the existing content. |
| `"never"` | Always join. `source` is not needed, and a document input with no source is valid. |
```ts
{ id: "contract-1", collaboration: { provider, synced, seed: "never" } }
```
Seeding happens after the first sync. Seeding before receiving the room's
state would duplicate the document.
## Sync state
Set `synced` to true once the provider has received the room's initial state.
Until then, the editor keeps the document closed instead of showing an empty,
editable page:
```tsx
const [synced, setSynced] = useState(false);
useEffect(() => {
provider.on("synced", () => setSynced(true));
}, [provider]);
```
Later disconnections do **not** close the document. Yjs merges offline edits
when the transport reconnects. A dropped connection affects presence only.
If your transport has no sync signal, omit `synced`. The editor opens
immediately, and a brand-new room can be seeded twice.
## Roster
`collaboration.getState()` returns awareness as a list of peers, with the same
colors the carets use:
```ts
const { enabled, synced, peers } = editor.collaboration.getState();
// peers: [{ clientId, isLocal, id, name, email, image, color }]
useEffect(() => editor.collaboration.subscribe(setPresence), [editor]);
```
It fires on join, leave, caret movement, and sync state changes. A participant
with no `currentUser` does not appear.
## Presence and cursors
Remote carets render when awareness is available, with a label and color per
participant. Peers see what you pass as `currentUser`:
```tsx
```
`name` labels the caret. `image` is drawn on its flag and can be any URL the
browser can load, including a `data:` URI.
`color` is optional. Without it, the editor derives a stable color from the
user `id`, so each person has the same color for everyone. Set it to use your
own palette or to match an avatar color.
A participant with no `currentUser` has no caret for others to see. Pass one
for anyone who should be visible.
`currentUser` also sets the author of tracked changes. Without it,
suggestions export to Word with the reviewer name "Anonymous".
## Server editing
With [`revise`](https://revise.io/developer/docs/revise/library), your server is a peer in the room, not only a
relay. It runs the same converters and mutation engine as the editor, at the
same version. Changes your server makes to the `Y.Doc` reach every open browser
as ordinary updates, and browser changes reach your server the same way. This
section covers common workflows; the [library reference](https://revise.io/developer/docs/revise/library)
covers the full API.
```bash
npm i revise@3.1.7
```
### Seeding from your own server
Your server can create the room from a file it already has:
```ts
import * as Y from "yjs";
import { ReviseDocument, seedYDoc } from "revise";
// When a room is opened. Safe on every connection: a room that already
// holds a document is left alone, and two peers that seeded at once are
// repaired to one copy.
await seedYDoc(ydoc, { bytes: docxBytes, filename: "agreement.docx" });
// Store the encoded state however you like:
await store.put(roomId, Y.encodeStateAsUpdate(ydoc));
// Export the live room without a browser. A room seeded from a .docx keeps
// the original file, so untouched content exports byte for byte.
const doc = await ReviseDocument.fromYDoc(ydoc);
const { bytes } = await doc.export({ format: "docx" });
doc.dispose(); // your Y.Doc stays alive
```
Clients then pass `seed: "never"` and no source file, and join the existing
document. This avoids a race where two people open a new document at the same
time and both import it. For a sync server built this way, see the
[`revise` collaboration guide](https://revise.io/developer/docs/revise/collaboration).
### Editing a live room
To change a room on the server, open it with `ReviseDocument.fromYDoc`. The
document has the same tools as the browser: read, search, measure, mutate, and
comment. Edits go through the same Yjs-backed mutation engine, so peers
receive ordinary updates and remote carets stay in place:
```ts
import { ReviseDocument } from "revise";
const doc = await ReviseDocument.fromYDoc(ydoc, {
documentId: "agreement-1",
author: "Review bot",
});
try {
// Documents default to suggesting, so this appears as a tracked change
// in every open tab.
const edit = await doc.tools.call("replace", {
search_result_id: searchResultId,
replacements: [{ find: "Delaware", replace: "New York" }],
});
if (edit.ok) await notifyReviewers(edit.value.suggestionIds ?? []);
} finally {
doc.dispose(); // your Y.Doc stays alive
}
```
A document opened this way is in **suggesting** mode. A service proposes
tracked changes unless it opts into direct edits with `mode: "editing"` or a
per-call `{ directMode: true }`. Nothing an unattended backend job proposes is
applied until a person accepts it.
### Review across server and browser
Server and browser share the same tool contract: `doc.tools.call()` on the
server and `editor.tools.execute()` in the browser return the same
`ReviseToolResult`, and mutations report the same `suggestionIds`. The server
can propose changes and a person can resolve them in the browser:
```ts
// Server: propose, and pass the IDs to your workflow.
const edit = await doc.tools.call("replace", { ... });
if (edit.ok) await queueForReview(edit.value.suggestionIds ?? []);
// Browser: later, resolve that batch from your review UI.
const decision = editor.review.acceptSuggestions(await reviewBatch());
// { resolved, missing, unresolved }. Stale IDs are listed in `missing`.
```
It also works in reverse. A person suggests changes in the browser, and a
scheduled job reads `await doc.changes()`. Each entry includes its author, so
the job can settle its own agent's changes with `doc.accept({ ids })` or
`doc.reject({ ids })` and leave human ones alone. See [tracked
changes](https://revise.io/developer/docs/editor-sdk/guides/tracked-changes) for the review UI.
### Working without a live transport
A server can participate without a socket. For an API endpoint or a queue
worker, the client posts its document as one encoded update. The server edits
a copy and returns only the delta. The delta merges into the live document
like any peer's edit, even if the user kept typing in the meantime:
```ts
// Client
const response = await api.review({ update: Y.encodeStateAsUpdate(ydoc) });
Y.applyUpdate(ydoc, response.delta, "my-backend");
// Server
import * as Y from "yjs";
import { ReviseDocument } from "revise";
const doc = await ReviseDocument.restore(update);
const baseline = Y.encodeStateVector(doc.ydoc);
// ...run tools, propose suggestions...
const delta = Y.encodeStateAsUpdate(doc.ydoc, baseline);
doc.dispose();
```
The server keeps no state and needs no transport. The response carries the
created `suggestionIds` for the same review flow as above.
## What the SDK does not do
- **It does not connect.** No socket, retries, authentication, or reconnection
backoff. Your provider handles all of it.
- **It does not persist.** Your server decides where the document is stored
between sessions.
- **It does not destroy your `Y.Doc`.** A document you pass in outlives the
component. Unmounting only clears this client's caret.
- **It does not enforce who may edit.** [Roles](https://revise.io/developer/docs/editor-sdk/guides/roles)
limit what each embed can do, but each client sets its own role, and there
is no document locking. To enforce a role, check it on the server at the
transport. With [`revise`](https://revise.io/developer/docs/revise/library), that server process can read
and edit the document it guards.
Source: https://revise.io/developer/docs/editor-sdk/guides/collaboration
---
# Selection
> Read the selection, and keep it while your own UI takes focus.
The selection is exposed as blocks and offsets, marks, comment IDs, and
suggestion IDs. It never includes canvas geometry, so your UI does not depend on
how Revise lays out text.
## Reading
```ts
const snapshot = editor.selection.getSnapshot();
snapshot.empty; // caret rather than a range
snapshot.text; // the selected text
snapshot.target; // portable ranges for comments and formatting
snapshot.selectionTarget; // explicit start/end for insert and replace
snapshot.activeMarks; // ["bold", "italic"]
snapshot.activeCommentIds;
snapshot.activeChangeIds;
```
Subscribe to follow it:
```ts
useEffect(() => editor.selection.observe(setSelection), [editor]);
```
`observe()` publishes the snapshot. `subscribe()` publishes `{ snapshot }`.
Both return an unsubscribe function. Both are memoized, so an identical
selection does not re-render your UI.
## Capture and restore
When the user clicks into an input in your own UI, such as a comment popover,
the editor loses focus and the selection is lost. Capture it first and restore
it afterward.
```ts
const capture = editor.selection.capture();
// The user types in your popover. The editor loses focus.
const result = editor.selection.restore(capture);
if (!result.success) {
// The captured blocks were moved or deleted.
}
```
`capture()` returns a frozen snapshot scoped to the current document. It stays
valid after focus moves away. It returns `null` when there is no selection or
caret.
`restore()` puts the selection back and refocuses the editor. It returns a
typed failure when:
- the editor is unavailable
- the document is read-only
- the capture belongs to a different document
- the captured blocks no longer exist (for example, after an agent edit)
Capture when your UI opens and restore when it submits. A capture is scoped to
one document and does not restore into another.
## Clearing
```ts
editor.selection.clear();
```
## For agents
The `get_selection` tool returns the same snapshot to an agent: selected text,
caret or range positions, portable target segments, active formatting, comment
IDs, and suggestion IDs. It includes no geometry.
Your own agent does not receive the selection automatically. Give it
`get_selection` when it needs to act on the text the user selected.
Source: https://revise.io/developer/docs/editor-sdk/guides/selection
---
# Tools for your agent
> Hand the document to whatever model you already run.
The SDK exposes the document tools from Revise's own agent. Each tool has a
JSON schema, runs without React state or browser focus, and works on document
structure rather than screen positions.
## Tool loop
```ts
const definitions = editor.tools.getDefinitions();
const response = await yourModel.createMessage({
messages,
tools: definitions.map((tool) => ({
name: tool.name,
description: tool.description,
input_schema: tool.inputSchema,
})),
});
for (const call of response.toolCalls) {
const result = await editor.tools.executeDynamic(call.name, call.input);
messages.push({
role: "tool",
tool_call_id: call.id,
content: result.ok
? [
result.value.message ?? JSON.stringify(result.value.data),
// Read and search results: the document itself, with block IDs.
result.value.context?.html,
]
.filter(Boolean)
.join("\n\n")
: `${result.error.code}: ${result.error.message}`,
});
}
```
Send the schemas to the model and return the results. For reads and searches,
`message` is only a summary. The text the model edits is in `context.html`, so
send both. The definitions come from the same source as Revise's production
agent, with the same descriptions and constraints.
## Results
This API and [`revise`](https://revise.io/developer/docs/revise/library)'s `doc.tools` return the same result
type, so the same handling code works in the browser and in Node:
```ts
type ReviseToolResult =
| { ok: true; value: ReviseToolResponse }
| { ok: false; error: ReviseToolFailure }; // { callId, tool, code, message }
interface ReviseToolResponse {
callId: string;
tool: Name;
message: string | null; // summary for the model
data: ReviseToolData; // typed metadata, per tool
context: { format: "revise-html"; html: string } | null;
suggestionIds: string[] | null; // tracked changes this call created
}
```
A rejected edit returns `ok: false`. It does not throw. Check `ok` and send
`error.message` back to the model. The message explains why the call failed
in terms the model can act on. `tools.call()` is the throwing variant for
application code and raises a typed `ReviseToolError`.
Read and search tools return `context.html`. This HTML keeps block IDs, inline
formatting, tables, and notes. Mutation tools target those block IDs. Mutations
in suggesting mode list the tracked changes they created in `suggestionIds`.
Pass them to `review.acceptSuggestions()`.
## Working with blocks
The model reads part of the document, then edits by block ID:
```ts
const read = await editor.tools.call("read_blocks_from_index", {
index: 0,
context_notes: "Looking for the liability clause",
});
await editor.tools.call("replace", {
id: "b12",
replacements: [
{
find: "capped at the fees paid",
replace: "capped at two years of fees paid",
occurrence: "unique",
},
],
});
```
`context_notes` is how a model carries what it learned from one read into the
next, and the schemas require it. When your own code calls a tool through
`execute()` or `call()`, you can leave it out; it defaults to `"N/A"`. Calls
through `executeDynamic()` are passed on exactly as the model sent them.
`replace` works inside **one** block. Edits in two blocks take two calls. A
single call with finds in more than one block fails without applying any of
them.
## Edit every match
Searching the active document returns a `search_result_id` for the full match
set, across all pages. Pass it to `replace`, `replace_block`, `style_blocks`,
or `remove_blocks` instead of an `id`. The edit applies to each matched block
separately, in one call:
```ts
const search = await editor.tools.call("search_document", {
queries: ["Acme Corp."],
page: 0,
context_notes: "Renaming the counterparty throughout",
});
await editor.tools.call("replace", {
search_result_id: search.data.search_result_id,
replacements: [
{ find: "Acme Corp.", replace: "Acme Holdings Ltd.", occurrence: "all" },
],
});
```
A search result ID works only in the editor session that created it. If the
matched blocks change, the call is rejected and nothing is applied. Run
`search_document` again and use the new ID.
## Routing to a document
Every tool schema has an optional `document_id`. Omit it to target the active
document, or pass the ID of any ready document in the same editor:
```ts
await editor.tools.call("set_title", { title: "Exhibit A" }, {
documentId: "exhibit-a",
});
// or bind a controller once
const exhibit = editor.tools.forDocument("exhibit-a");
await exhibit.execute("measure_document", {});
```
## Suggestions or direct edits
Tool mutations are tracked changes by default, whatever mode the editor is in
for typing. Pass `directMode` to apply them directly:
```ts
await editor.tools.call("style_blocks", input, { directMode: true });
```
See [tracked changes](https://revise.io/developer/docs/editor-sdk/guides/tracked-changes) for when to use it.
## What is not a tool
Review navigation, comment panel state, focus, viewport, zoom, and canvas
rendering belong to the host. They are not tools in this catalog. The agent
makes document edits, and your UI presents them. A delegated Revise agent can
inspect the rendered canvas with its internal `render_document_pages` tool. See
[architecture](https://revise.io/developer/docs/editor-sdk/concepts/architecture) and [delegation](https://revise.io/developer/docs/editor-sdk/guides/delegated-agent).
The full catalog is in the [tool reference](https://revise.io/developer/docs/editor-sdk/api/agent-tools).
## Server-side editing
[`revise`](https://revise.io/developer/docs/revise/library) runs the same tool definitions and executor in
Node, on a document you open from a file or a `Y.Doc` you own. No model or
React mount is involved. Install it at the same version as the editor
(`npm i revise@3.1.7`).
A `ReviseDocument`:
- is one document, so its schemas have no `document_id`
- keeps search result IDs across calls
- runs every call in arrival order
- must be disposed. Disposing a document opened with `fromYDoc` leaves your
`Y.Doc` intact.
Literal tool names infer their exact input and output types. `doc.tools.call()`
never throws for a bad edit: it returns `{ ok: true, value }` or
`{ ok: false, error }`. Use `doc.tools.run()` for tool names and JSON from a
model.
Switch modes with `await doc.setMode("editing")` and
`await doc.setMode("suggesting")`, or pass `{ directMode: true }` to one call.
```ts
import { readFile, writeFile } from "node:fs/promises";
import { ReviseDocument } from "revise";
const doc = await ReviseDocument.open("agreement.docx", {
documentId: "agreement-1",
mode: "suggesting",
author: "Contract bot",
});
const read = await doc.tools.call("read_blocks_from_index", {
index: 0,
context_notes: "Locating the liability clause",
});
if (read.ok) console.log(read.value.context?.html); // semantic HTML with block IDs
const search = await doc.tools.call("search_document", {
queries: ["fees paid"],
page: 0,
context_notes: "Liability cap",
});
const searchResultId = search.ok ? search.value.data.search_result_id : null;
if (!searchResultId) throw new Error("The search returned no editable matches");
// Exact-text replacement needs no HTML. Apply this one directly.
await doc.tools.call(
"replace",
{
search_result_id: searchResultId,
replacements: [{ find: "fees paid", replace: "fees paid or payable" }],
},
{ directMode: true },
);
// Suggesting mode produces tracked changes.
const governingLaw = await doc.tools.call("search_document", {
queries: ["Delaware"],
page: 0,
context_notes: "Suggesting a governing-law change",
});
const governingLawId = governingLaw.ok
? governingLaw.value.data.search_result_id
: null;
if (!governingLawId) throw new Error("The governing-law text was not found");
const edit = await doc.tools.call("replace", {
search_result_id: governingLawId,
replacements: [{ find: "Delaware", replace: "New York" }],
});
// Resolve by ID. Mutation results list the tracked changes they created.
// Store these IDs with your review task.
const created = edit.ok ? (edit.value.suggestionIds ?? []) : [];
const decision = await doc.accept({ ids: created });
// decision.resolved: settled now. decision.missing: stale or unknown IDs.
// Or list everything pending. Each entry has its author, so you can act on
// your agent's changes and leave collaborators' changes alone:
const mine = (await doc.changes()).filter(
(change) => change.author === "Contract bot",
);
// await doc.reject({ ids: mine.map((change) => change.id) });
// These also settle collaborators' pending changes. Use them only when the
// host owns the document:
// await doc.accept({ all: true }); // or doc.reject({ all: true })
await writeFile("agreement.revise", await doc.snapshot());
const restored = await ReviseDocument.restore(
await readFile("agreement.revise"),
);
const { bytes } = await restored.export({ format: "docx" });
await writeFile("agreement-amended.docx", bytes);
restored.dispose();
doc.dispose();
console.log(decision.resolved.length, mine.length);
```
In a model loop, return expected failures to the model as text:
```ts
const result = await doc.tools.run(call.name, call.input);
const toolMessage = result.ok
? (result.value.message ?? JSON.stringify(result.value.data))
: `${result.error.code}: ${result.error.message}`;
```
`revise`'s [`agentTools`](https://revise.io/developer/docs/revise/agents) wraps this loop in the shapes the
Anthropic and OpenAI SDKs expect.
### Revise semantic HTML
Read and search tools return `response.context.html` in **Revise semantic
HTML**: document markup with stable block IDs for paragraphs, formatted runs,
tables, comments, and notes where supported. Typed metadata is in
`response.data`. A short summary for the model is in `response.message`.
Structural insertion and whole-block replacement accept the same HTML. Use it
instead of building `DocRoot` or OOXML. It is not general browser HTML and CSS,
and it does not represent Word layout that Revise does not support. The
Yjs-backed Revise model is the source of truth, and DOCX is the import and
export format. Running a tool does not reparse unchanged content through HTML.
Do not mutate `DocRoot` directly. Some conversion calls still return that type
for compatibility. The supported server mutation API is `revise`'s
`doc.tools`.
Source: https://revise.io/developer/docs/editor-sdk/guides/agent-tools
---
# Delegate to the Revise agent
> Run a whole task with the Revise multi-turn agent.
`revise_run_agent` sends a task to the Revise multi-turn agent, the same agent
that runs in the Revise product. It plans, reads, and edits until the task is
done, then returns the result.
## Configure a backend
Tell the editor where turns go. Fetch any token before rendering, for example
in a loader or a parent component:
```tsx
export function Workspace({ token, docs }: { token: string; docs: ReviseDocumentInput[] }) {
return (
);
}
```
The editor needs one of three transports: `turnStream` for your own model (see
[custom transport](#custom-transport)), `baseUrl` for your own agent backend,
or `token` for Revise's hosted agent. With none of them, `agent.run()` rejects
with an `AgentNotConfiguredError` (`code: "agent_not_configured"`) and nothing
leaves the page. The `revise_run_agent` tool returns the same code, and
`getToolDefinitions()` leaves it out.
Per-document configuration overrides the editor-wide default:
```ts
await editor.documents.open({
id: "exhibit-a",
source: file,
agent: { model: "claude-sonnet-5-5" },
});
```
The delegated agent is the only SDK feature that sends document content off
the page. Each run streams the document to the agent backend at `baseUrl`,
which defaults to Revise's hosted agent at `https://agent.revise.io`. To use
the hosted agent in your product, email [sdk@revise.io](mailto:sdk@revise.io)
for access. Everything else in the SDK runs in the browser or on your own
server.
## Run a task
```ts
const result = await editor.agent.run(
"Make the liability cap mutual and raise it to two years of fees.",
);
result.actionCount; // number of edits
result.messages; // the conversation
result.state; // "complete" | "error"
```
It is also available as a tool, so a supervising model can call it:
```ts
await editor.tools.execute("revise_run_agent", { task });
```
## Stream, steer, and cancel
```ts
useEffect(
() =>
editor.agent.subscribe((event) => {
setStatus(event.status); // progress text
setActiveTool(event.activeTool);
setMessages(event.messages);
}),
[editor],
);
editor.agent.steer("Keep the defined terms as they are.");
editor.agent.cancel();
```
`steer()` adds guidance to a run in progress. It does not queue a new turn.
The same events are available as a component callback:
```tsx
setStatus(documentId, event.status)}
/>
```
## Custom transport
To send turns through your own gateway, logging, or model, pass a transport
instead of a URL:
```tsx
```
The editor keeps its tool runtime, tracked changes, and review behavior. Only
the model call changes. A new `turnStream` takes effect on the next run, so an
inline generator like this one is fine: the open document is never reloaded.
## Run accounting
When a run finishes, the editor tells the agent backend at `baseUrl`, with a
request to `/api/v1/chat/metrics/complete`, so the backend can settle the
run's cost. It goes only to the backend you configured. `turnStream` skips it,
since there is no backend to tell, and `disableMetrics: true` turns it off for
a backend that does not need it. It is separate from the editor's
[`telemetry`](https://revise.io/developer/docs/editor-sdk/api/revise-editor) ping.
## Visual page inspection
The delegated agent can call an internal `render_document_pages` tool to check
layout visually. It renders page contact sheets or layout diagnostics from the
mounted editor canvas. The agent sees pagination, tables, images, math,
headers, and footers as the user sees them.
`render_document_pages` is not in `editor.tools.getDefinitions()`. That catalog
is shared with [`revise`](https://revise.io/developer/docs/revise/library) on the server, and page rendering
needs a mounted browser canvas. Run the task with `editor.agent.run()` or `revise_run_agent`, and the
SDK routes the render call.
## Suggestions by default
Delegated runs follow the document mode. In `suggesting`, every agent edit is
a tracked change for a person to accept or reject. Pass `directMode` in the run
options to apply edits directly. Every tool call in that run inherits it.
```ts
await editor.agent.run(task, { directMode: true });
```
Source: https://revise.io/developer/docs/editor-sdk/guides/delegated-agent
---
#
> Every prop on the editor component.
```tsx
import ReviseEditor from "@reviseio/editor";
import "@reviseio/editor/style.css";
```
## Documents
Documents to open on mount. Each needs a host-owned `id` and a `source`:
DOCX, ODT, RTF, Markdown, plain text, or HTML (see [input
formats](https://revise.io/developer/docs/editor-sdk/guides/formats)). `docx` is accepted as the older name
for the same field. A document joining an
established [collaborative](https://revise.io/developer/docs/editor-sdk/guides/collaboration) room needs no
source.
Controlled active document. Pair with `onActiveDocumentChange`.
Initial active document for uncontrolled use. Defaults to the first document.
## Behavior
Identity attached to comments and suggestions the user authors.
What this participant may do in every open document. Override per document
with `ReviseDocumentInput.role`. Unlike `defaultDocumentMode`, this is a
limit, not a starting point: a `suggester` cannot switch to editing mode, and
agent tool calls that would edit directly are refused.
See [roles](https://revise.io/developer/docs/editor-sdk/guides/roles).
Behavior settings. `settings.trackedChanges.markupStyle: "word"` switches
the canvas to classic Word-style redlines (all revisions in red, insertions
underlined, deletions struck through, no background tint);
`settings.trackedChanges.showRemovalsInReview: true` starts review with
deleted text visible inline. See [tracked changes](https://revise.io/developer/docs/editor-sdk/guides/tracked-changes).
Backend, credentials, model, or a custom transport for the delegated agent.
See [delegating to the Revise agent](https://revise.io/developer/docs/editor-sdk/guides/delegated-agent).
The mode each document opens in. This is not a controlled prop. Use
`view.setDocumentMode()` to change an open document.
Initial zoom.
Whether the comments panel starts open.
Take keyboard focus when a document becomes active. Set `false` when the
editor sits inside a larger page and should wait for a click.
Opt in to the Rust/WASM accelerations for word diff, agent text matching, and
document statistics. The TypeScript implementations remain the fallback, so
this changes speed only, not behavior.
Fonts added to the native picker. Your app loads the font files.
Store an image the user inserts, pastes, or drops, and resolve to the URL the
document should reference — your CDN or object storage. Without it, images
are embedded in the document as data URLs, which export into the .docx like
any other image but make the document larger. A rejection shows the user a
notice and reaches `onError`. Files over 10 MB are refused before upload.
Shows Import before Export in the ribbon, with a file picker for DOCX, ODT,
RTF, Markdown, HTML, text, and XML, and accepts a document file dropped
anywhere on the editor, with a "Drop to import" overlay. Image drops still
insert images. The editor only collects the file: what importing means,
such as opening it as a new document, is up to you.
## Chrome
`"native"` renders the full Revise ribbon. `"compact"` renders the
everyday-formatting subset, centered, for narrow fixed-width hosts (about
760px). `"floating"` renders the full ribbon as a centered card over the
canvas, as in the Revise app. `"none"` renders no toolbar.
The bar above the ribbon: editable title, optional mode toggle, Review.
Built-in tab strip, one tab per open document.
Who handles Cmd/Ctrl+F. `"editor"` opens the built-in find-and-replace
panel. `"browser"` leaves the shortcut to the browser's native find. Use
`"browser"` when the editor is embedded in a longer page.
Show the Editing/Suggesting toggle in the title bar.
Adds a "+" button to the tab strip. Without it, no button renders.
## Appearance
Scoped to this editor. Defaults to the host page's theme.
The area behind the page. Any CSS background.
The paper itself. Any CSS color.
The outline around each page. Any CSS color.
## Callbacks
Fires once the editor is mounted. Store the handle here.
Fires once per document, after it has parsed and is ready to read or edit.
Fires on any content change, from the user, your UI, or an agent. Debounce
before saving.
Fires when the set of open documents changes.
The editor's internal instrumentation (tool invocations, exports, review
interactions), delivered to your own analytics. These events leave the page
only through this callback. Names and property shapes are
internal and may change between versions.
Once per page load, the SDK sends Revise one anonymous ping with its version.
The browser adds your site's origin. From `localhost` and other
development hosts, only a daily count per version is kept, never the origin.
The ping contains no cookies, user data, or document content. Set `false` to turn it off. See
[license](https://revise.io/developer/docs/editor-sdk/license). The ping is the SDK's only report to Revise.
It still downloads assets from `https://revise.io` when first needed (math
fonts, PDF export fonts, the PDF import worker); `setAssetBaseUrl()` points
it at your own mirror. An [agent](https://revise.io/developer/docs/editor-sdk/guides/delegated-agent), if you
configure one, talks only to the backend you chose.
Parse failures and runtime errors. A document that fails to open also shows
`status: "error"` in the collection state.
## Helpers
Returns a blank DOCX. Use it for "New document" actions.
```ts
import { createEmptyDocx } from "@reviseio/editor";
```
Source: https://revise.io/developer/docs/editor-sdk/api/revise-editor
---
# ReviseEditorHandle
> The controllers your application drives the editor with.
`onReady` passes you a `ReviseEditorHandle`. Store it in a ref. Every
controller on it is stable for the life of the component.
```ts
const editor = useRef(null);
(editor.current = handle)} />;
```
## Routing
Document-local controllers act on the active document. Each one can be bound to
a specific document instead:
```ts
editor.review.next();
editor.review.forDocument("exhibit-a").next();
editor.document("exhibit-a").review.next(); // equivalent
```
Scoped controllers: `tools`, `toolbar`, `review`, `view`, `zoom`, `selection`,
`agent`, `comments`.
## documents
Opens and parses a document. `{ activate: false }` loads it in the background.
As `get`, but throws instead of returning `null`.
## tools
Native document-local tool schemas, without the delegation tool. Hand these
to your own model. `editor.getToolDefinitions()` returns the same tools with
an `execute` function each, plus `revise_run_agent` when an
[agent is configured](https://revise.io/developer/docs/editor-sdk/guides/delegated-agent).
Typed inputs from the shared tool contract. Expected failures return
`{ ok: false }`. Options: `{ documentId?, directMode? }`. `context_notes`,
which a model uses to carry context between reads, is optional here and
defaults to `"N/A"`.
For untrusted model-provided calls, browser-only tools (`get_selection`,
`view_image`, `revise_run_agent`), and model-facing `document_id` routing.
Returns the successful response, or throws `ReviseToolError`.
Export as `"docx"` (default), `"pdf"`, `"html"`, `"markdown"`, or `"txt"`.
With `mode: "review"` (default) a .docx keeps pending tracked changes and
comments, and other formats show the text with changes accepted.
`mode: "final"` accepts every pending change and drops comments first — a
clean copy. See [export](https://revise.io/developer/docs/editor-sdk/guides/formats#export).
## toolbar
Live selection formatting, block type, undo/redo availability, and review
counts.
`setActiveTab` switches the ribbon's row. The SDK has no tab strip for the
ribbon, so connect your own controls to
`toolbar.setActiveTab("layout" | "insert" | "review" | "edit")`. Page and
section breaks are on the Layout row. The SDK has no Tools row. Get word and
character counts from [`editor.getStatistics()`](#convenience).
Commands return `false` when they cannot apply.
| Area | Methods |
| --- | --- |
| History and clipboard | `undo`, `redo`, `copy`, `copyAs`, `cut`, `paste`, `pasteTextOnly`, `pasteFormattingOnly` |
| Inline | `toggleInline`, `toggleBold`, `toggleItalic`, `toggleUnderline`, `toggleStrikethrough`, `toggleDoubleStrikethrough`, `toggleSmallCaps`, `toggleAllCaps`, `toggleCode`, `toggleLatex`, `toggleSuperscript`, `toggleSubscript`, `clearFormatting` |
| Type | `setFontFamily`, `setFontSize`, `setLetterSpacing`, `setTextColor`, `setHighlightColor` |
| Paragraph | `setHeading`, `setAlignment`, `setLineSpacing`, `toggleList`, `createLink`, `increaseIndent`, `decreaseIndent` |
| Insert | `insertTable`, `insertImage`, `insertContainer`, `insertCodeBlock`, `insertMathBlock`, `insertDiagram`, `insertFootnote`, `insertPageNumber`, `insertPageBreak`, `insertSectionBreak` |
| Review | `openReview`, `closeReview`, `reviewPrevious`, `reviewNext`, `acceptCurrent`, `rejectCurrent`, `acceptAll`, `rejectAll`, `setShowRemovals`, `setSuggestionViewMode` |
## view
Title, document mode, comments panel, and review panel.
`{ ready, title, documentMode, readOnly, commentsOpen, commentCount, reviewOpen, reviewTargetCount }`
## review
Open state, target counts, suggestion IDs, active comment, display mode, and
every comment thread.
| Area | Methods |
| --- | --- |
| Navigation | `open`, `close`, `previous`, `next`, `nextAgentSuggestion`, `navigateToSuggestion`, `navigateWithinSuggestions` |
| Decisions | `acceptCurrent`, `rejectCurrent`, `acceptAll`, `rejectAll`, `acceptSuggestions`, `rejectSuggestions` |
| Preview | `previewCurrent`, `previewAll`, `previewSuggestions` |
| Display | `setShowRemovals`, `setSuggestionViewMode`, `getOpenSuggestionIds` |
| Comments | `openComments`, `closeComments`, `selectComment`, `addCommentAtSelection`, `addCommentToRanges`, `addCommentToBlock`, `addCommentForSuggestion`, `replyToComment`, `setCommentResolved`, `deleteComment`, `getRelatedSuggestionIds`, `acceptCommentSuggestions`, `rejectCommentSuggestions` |
Every pending change with `kind`, `author`, `authorType`, `createdAt`,
`blockIds`, `insertedText`/`deletedText`, and `description`. It is not part
of the subscribed state. Each call reads the document once. See [building your
own review panel](https://revise.io/developer/docs/editor-sdk/guides/tracked-changes#custom-review-panel).
One change, or `null` after it is resolved.
## selection
Saves the selection so it survives focus moving into your UI. Returns `null`
when nothing is selected.
Returns a typed failure when the document has changed since the capture.
## zoom
## agent
Send guidance to a run in progress.
## comments
Agent runs scoped to a single comment thread.
Publishes a map of every run in progress.
## collaboration
Presence and sync state for a shared document. See
[collaboration](https://revise.io/developer/docs/editor-sdk/guides/collaboration).
`{ enabled, synced, peers }`. `peers` lists everyone in the room, including
the local participant (`isLocal: true`), each with the `color` their caret is
drawn in. Empty when no awareness was supplied.
Fires on join, leave, caret movement, and sync-state changes.
## Convenience
Shorthands that act on the active document unless given an ID.
`onReady` fires **before** any document exists. Subscriptions can be added
immediately and attach when a document opens. Calls that act on a document
throw until one is open. Wait for `onDocumentReady`, or
`await editor.whenReady()`.
```ts
await editor.whenReady(); // resolves when a document is open
editor.getRole(documentId?); // "editor" | "suggester" | "viewer"
editor.getDocument(documentId?);
editor.getStatistics(documentId?); // { wordCount, characterCount }
editor.getDocumentMode(documentId?);
editor.setDocumentMode(mode, documentId?);
editor.getToolDefinitions(documentId?);
editor.focus(documentId?);
editor.openReview(documentId?);
editor.closeReview(documentId?);
editor.openComments(documentId?);
editor.closeComments(documentId?);
```
`getStatistics()` counts are computed without a layout pass, so they are safe
to call per keystroke behind a debounce. Counts follow the markup view, as in
Word: pending insertions count and pending deletions do not. In the
`"original"` suggestion view mode, counts use the text as if all suggestions
were rejected. `editor.document(id).getStatistics()` takes no argument.
Source: https://revise.io/developer/docs/editor-sdk/api/handle
---
# Server-side work
> Convert, edit, and export documents in Node with revise.
`@reviseio/editor` is the browser editor. From version 3 it has no Node entry
point: server-side work is [`revise`](https://revise.io/developer/docs/revise/introduction), the MIT CLI and
TypeScript library built on the same engine and released at the same version.
It is an optional peer dependency of the editor; a browser-only app doesn't
need it.
```bash
npm i revise@3.1.7
```
Documents move between the two unchanged: a `Y.Doc` the editor edits is a
`Y.Doc` `revise` opens, and the reverse. `revise` runs the same converters and
the same document tools, with the same schemas and `{ ok, value | error }`
results, so a server can seed a room, edit it while people have it open, and
export it to Word:
```ts
import { ReviseDocument } from "revise";
import type * as Y from "yjs";
async function reviewRoom(room: Y.Doc): Promise {
const doc = await ReviseDocument.fromYDoc(room, { author: "Review bot" });
try {
const [hit] = doc.grep("Delaware");
if (hit) {
// A tracked change in every open editor.
await doc.tools.call("replace", {
id: hit.id,
replacements: [{ find: "Delaware", replace: "New York" }],
});
}
const { bytes } = await doc.export({ format: "docx" });
return bytes;
} finally {
doc.dispose(); // the room stays alive
}
}
```
Open, read, edit, review, and export documents.
Seed, edit, and export live rooms.
A server and browsers in the same room.
The agent tools without a browser.
## Migrate from version 2
Version 3 removes the editor's `/server` entry point. Install `revise`
at the editor's version and replace the imports:
| 2.3 `/server` entry point | `revise` |
| ---------------------------------------------------- | ---------------------------------------------------------------------------------------------------- |
| `createServerDocumentSession(ydoc)` | `ReviseDocument.fromYDoc(ydoc)`, then `doc.tools` |
| `parseDocument`, `documentToDocx`, `fileToYDoc` | `ReviseDocument.open(file)`, `doc.export({ format: "docx" })`, `doc.ydoc` |
| `exportDocument(room, { mode })`, `ydocToDocx(room)` | `ReviseDocument.fromYDoc(room)`, then `doc.export({ format, mode })` |
| `seedYDocFromFile(room, bytes, filename)` | `seedYDoc(room, { bytes, filename })` |
| `encodeYDoc`, `decodeYDoc` | Y.js itself: `Y.encodeStateAsUpdate(doc.ydoc)`; `ReviseDocument.restore(update)` opens a bare update |
Three differences when porting:
- `revise` methods are async: `await` them.
- Tools return `{ ok: true, value }` or `{ ok: false, error }` instead of
throwing. `doc.tools.call()` is the typed form; `doc.tools.run()` takes a
tool name and JSON from a model.
- `doc.export()` returns `{ bytes, fidelity, … }`: write `.bytes`.
`jsdom` is no longer a peer of the editor; `revise` brings what it needs.
`doc.snapshot()` is `revise`'s own storage format, not a Y.js update: keep
`Y.encodeStateAsUpdate(doc.ydoc)` for anything that speaks Y.js, such as a
sync server or the editor.
The [2.3 backend reference](https://revise.io/developer/docs/editor-sdk/v2.3/api/backend) still documents the
`/server` entry point.
Source: https://revise.io/developer/docs/editor-sdk/api/backend
---
# Agent tool reference
> The document-local tools the SDK exposes to a model.
Get the live schemas with `editor.tools.getDefinitions()`. They are generated
from the same source as Revise's production agent. The descriptions below are
summaries; the schemas are authoritative.
Every tool accepts an optional `document_id`. Omit it for the active document.
On Node, [`revise`](https://revise.io/developer/docs/revise/library) runs the headless subset:
`doc.tools.definitions()` lists it. A `revise` document is one document:
- Its schemas and descriptions omit `document_id`.
- It excludes `get_selection` and `view_image`.
- It has no delegation, attachment import or storage, account document APIs,
or viewport, canvas, and UI operations.
- Untrusted calls use `doc.tools.run(name, input)` and receive
`{ ok: true, value } | { ok: false, error }`; `doc.tools.call` is the typed
form.
The definitions list exactly what `doc.tools.run` accepts.
## Reading and searching
The user's current semantic selection: text, caret or range positions,
portable target segments, active formatting, comment IDs, suggestion IDs. No
geometry.
Read consecutive blocks from a 0-based start index. The usual entry point.
Read specific blocks by ID.
Search text within one document. Use it to locate a term before editing.
Results for the active document include a `search_result_id` that stands for
the whole unpaginated match set. Pass it to `replace`,
`replace_block`, `style_blocks`, or `remove_blocks` to act on every match in
one call. Displayed matches are paginated.
Find highlighted blocks, optionally filtered by color. Highlights are not
visible in loaded content, so this is the only way to find "the highlighted
parts". Returns 25 blocks per page.
Word, paragraph, block, and rendered page counts without loading body text.
Primary totals treat pending suggestions as accepted. When that differs from
the visible Review state, the result also includes the original projection.
Use it instead of reading the document to count.
The same measurements for an inclusive block range.
View a document image by block ID when its `src` is hidden.
Read tools return HTML with block IDs, not flattened text. The mutation tools
below target those IDs.
## Editing text
The default tool for editing text inside a block: wording, sentences, typos,
punctuation, and rewrites that keep formatting. It keeps block identity and
untouched formatting. Use `replace_block` only when the block's type or
structure changes. Each operation can carry a comment, attached to every
suggestion it produces. Edits across blocks need one call per block, or one
call with `search_result_id`, which applies the same edits in every matched
block.
Insert one or more HTML blocks before or after a reference block. Notes can be
inline: `1 ` creates the footnote in the same
call.
Replace one or more consecutive blocks with new HTML blocks, for structural
or block-type changes. Keeps the block type unless asked otherwise. Name a
range with `id` and `end_id` (inclusive, in the same container). With
`search_result_id`, the one-block replacement applies to every matched
block. A reversed, unknown, or cross-container range fails with
`invalid_block_range` before anything changes.
Remove blocks by consecutive range (`id` through `end_id`, inclusive, in the
same container), an exact ID batch
(`ids`), everything from one block through the document tail (`through_end`),
or every block in a search result (`search_result_id`). The old `remove_block`
name still executes as an alias but is not listed in the definitions.
Append HTML to an existing paragraph.
Split a paragraph at the first occurrence of a substring.
Join two adjacent paragraphs, preserving the inline formatting of both.
## Formatting
Bulk formatting with CSS-lite selectors: `*`, `#blockId`, `p`, `h1` to `h6`,
`li`, lists, tables, and inline selectors such as `b`, `i`, `code`, `mark`,
and `a`, optionally scoped (`p b`, `#blockId strong`). Use it instead of
rewriting text to restyle it. Pass `search_result_id` to scope the selectors
to a search result. `*` then means each matched block.
Strip removable inline emphasis from the targets.
## Structure and layout
Change the document title.
Preset, page size, orientation, margins, page numbers, spacing, pageless mode.
Lengths are strings with explicit units (`"1in"`, `"2.54cm"`, `"72px"`). Page
numbers can be `decimal`, `lowerRoman`, or `upperRoman` per section
(`pageNumberFormat`). With `sectionIndex`, `pageNumberStart` restarts
numbering and `pageNumberRestart: false` makes a section continue. Native
`pageBorders` configures individual `top`, `right`, `bottom`, and `left` edges
(`style`, `color`, `widthPt`, `spacePt`), with `offsetFrom` (`page` or
`text`), `display` (`allPages`, `firstPage`, `notFirstPage`), and `zOrder`
(`front` or `back`). Borders remain editable in Page Layout and appear in PDF
and DOCX exports. Use `clearPageBorders: true` to remove them. Omitted fields
leave existing borders unchanged.
Set header and footer content by zone (left, center, right). `{PAGE}` and
`{PAGES}` are live fields. An empty string clears one zone. `slot` targets
the `default`, `first`, or `even` page header or footer, and a one-based
`sectionIndex` edits one section's own. `image` places a logo in a zone:
`image_id` of an image already in the document, with optional `alt`,
`width`, and `height`. `clear: true` without `slot` or `sectionIndex`
removes that side from every page and section. A call that changes nothing
fails with "No changes detected".
Insert a numbered superscript reference and its note body. Endnotes are a
separate stream.
## Tables
Read a bounded part of a table by `table_id`. `mode: "summary"` returns
dimensions and per-column empty, filled, and covered-cell counts with no cell contents.
`mode: "rows"` (the default) returns up to 50 rows, optionally limited to a
column range, with merged-anchor spans and a `next_read` continuation. `mode: "cell_text"` pages
through one oversized cell. Only cells returned in full by a `rows` read can
be edited with `edit_table_cells`. A summary, a partial column range, or a
partial `cell_text` window does not authorize edits to anything else. The
native Revise agent's task-declaration arguments are not part of this
schema. A call that passes one fails with `unsupported_argument`.
Set the plain text of up to 50 cells in one call, addressed by `table_id`,
stable `row_id`, and 0-based `column`. `text` is literal: `007`, `1e5`,
``, tabs, and newlines are written exactly as given, never parsed as HTML
or numbers. `mode: "fill_if_empty"` skips and reports cells that already have
content. `mode: "replace"` overwrites cells read in their current state. An
unread, unknown, duplicate, covered (merged), or changed target rejects the
whole batch with no changes. Cells with rich content are refused; use the
block tools on their nested block IDs. In suggesting mode each cell is a
separate reviewable suggestion.
Removes one or more rows in one call. Name rows by `rowIds` (a row ID or an
array of them); row IDs stay valid after other rows are inserted or removed.
`rowIndices` is a fallback for rows read at the current table layout. Every
cell of each row must have been read. An unknown, duplicate, or unread row
rejects the whole call with no changes. The earlier `remove_table_row` name
with `{ tableId, rowIndex }` still works.
Read before you write. `edit_table_cells` needs a `read_table` rows read of
its targets on the same session. The row and column tools need the target
`` in loaded context.
## Comments
Leave review feedback anchored to one location without changing the text.
Use it for critique instead of rewriting a passage the user did not ask to
change.
To **start** a thread, pass the block `id` and exactly one `anchor` form:
`{ text }` for a short exact range, `{ start_text, end_text }` for a longer
one, or `{ whole_block: true }`. To **reply**, pass `reply_to_comment_id` (any
comment ID in the thread) with no `id` and no `anchor`. The reply uses the
thread's anchor.
A new thread that overlaps an existing unresolved thread is rejected. To
comment on the same passage with a different point, list the overlapping
thread IDs in `acknowledge_existing_thread_ids`.
## Delegation
Hand a complete task to Revise's multi-turn agent loop. Call it through
`tools.execute()` like any other tool. It is excluded from
`getDefinitions()`, so pass it to your model explicitly. The delegated browser
agent can use its internal `render_document_pages` tool to inspect bounded
appearance or layout renders from the mounted canvas. See [delegating to the
Revise agent](https://revise.io/developer/docs/editor-sdk/guides/delegated-agent).
## Not exposed as tools
Review navigation, comment-panel state, focus, viewport geometry, zoom, and
direct canvas rendering are not in the tool catalog. Agents make semantic
edits, and your UI presents them. The delegated Revise agent is the one
exception: it uses `render_document_pages` internally to inspect pages on the
mounted browser canvas. See the [handle reference](https://revise.io/developer/docs/editor-sdk/api/handle).
Source: https://revise.io/developer/docs/editor-sdk/api/agent-tools
---
# Types
> The exported TypeScript surface.
Everything below is exported from the package root:
```ts
import type { ReviseEditorHandle, ReviseToolbarState } from "@reviseio/editor";
```
## Documents
```ts
type ReviseDocumentSource = File | Blob | ArrayBuffer;
/** DOCX is the high-fidelity path; ODT and RTF also bring comments and
* tracked changes in. */
type ReviseDocumentFormat = "docx" | "odt" | "rtf" | "markdown" | "txt" | "html";
interface ReviseDocumentInput {
/** Stable, host-owned. Names this document in APIs, tools, and callbacks. */
id: string;
/** The file to open, in any supported format. Optional: a document joining
* an established collaborative room has no file to open. */
source?: ReviseDocumentSource;
/** The original name for `source`, still accepted. Pass one or the other. */
docx?: ReviseDocumentSource;
/** Share this document over a Yjs transport you own. */
collaboration?: ReviseCollaborationConfig;
/** Overrides the editor-wide role for this document. */
role?: ReviseDocumentRole;
/** Inferred from filename, then MIME type, defaulting to DOCX. */
format?: ReviseDocumentFormat;
title?: string;
documentMode?: ReviseDocumentMode;
readOnly?: boolean;
zoom?: ReviseZoom;
defaultCommentsOpen?: boolean;
agent?: ReviseAgentConfig;
}
interface ReviseOpenDocumentOptions {
/** Newly opened documents become active by default. */
activate?: boolean;
}
interface ReviseOpenDocument {
id: string;
title: string;
status: "opening" | "ready" | "error";
active: boolean;
error?: string;
}
interface ReviseDocumentCollectionState {
activeDocumentId: string | null;
documents: ReviseOpenDocument[];
}
type ReviseDocumentScoped = T & { forDocument(documentId: string): T };
```
## Collaboration
`ReviseYDoc` and `ReviseAwareness` are typed `any` so the public types do not
depend on Yjs. Cast them to `Y.Doc` and `Awareness` in your code.
```ts
type ReviseYDoc = any;
type ReviseAwareness = any;
interface ReviseCollaborationProvider {
document?: ReviseYDoc;
doc?: ReviseYDoc;
getYDoc?: () => ReviseYDoc;
awareness?: ReviseAwareness | null;
}
type ReviseCollaborationConnect = (
ydoc: ReviseYDoc,
awareness: ReviseAwareness,
) => ReviseCollaborationProvider | { destroy?: () => void } | void;
interface ReviseCollaborationConfig {
/** Recommended. The editor builds the document; you attach a transport. */
connect?: ReviseCollaborationConnect;
/** A provider that already owns its document. Validated before use. */
provider?: ReviseCollaborationProvider;
/** A document you own. Validated before use; prefer `connect`. */
ydoc?: ReviseYDoc;
/** Remote carets and presence. Pass null to opt out. */
awareness?: ReviseAwareness | null;
/** The transaction origin your transport applies remote updates with.
* Required when passing `ydoc` without a `provider`. */
remoteOrigin?: unknown;
/** Whether the transport finished its initial sync. The document stays
* closed until this is true. */
synced?: boolean;
/** "if-empty" (default) writes the source file into an empty room;
* "never" always joins. */
seed?: "if-empty" | "never";
}
interface RevisePeer {
clientId: number;
isLocal: boolean;
id?: string;
name?: string;
email?: string;
image?: string;
color: string;
}
interface ReviseCollaborationState {
enabled: boolean;
synced: boolean;
peers: RevisePeer[];
}
```
The [collaboration guide](https://revise.io/developer/docs/editor-sdk/guides/collaboration) explains when to use
`connect`, `provider`, or `ydoc`.
## Roles
```ts
/** What a participant may do. Unlike `documentMode`, it cannot be changed
* from inside the editor. */
type ReviseDocumentRole = "editor" | "suggester" | "viewer";
interface ReviseRolePolicy {
readonly modes: readonly ReviseDocumentMode[];
readonly canEdit: boolean;
readonly canEditDirectly: boolean;
readonly canResolveSuggestions: boolean;
readonly canComment: boolean;
}
declare function rolePolicy(role: ReviseDocumentRole): ReviseRolePolicy;
/** Thrown by a delegated `agent.run()` when a role forbids it.
* (`tools.execute()` returns `{ ok: false, error: { code: "role_not_permitted" } }`
* instead of throwing.) */
declare class ReviseRoleError extends Error {
readonly role: ReviseDocumentRole;
readonly action: string;
}
```
## Tracked changes
```ts
interface ReviseTrackedChange {
id: string;
/** Word's three revision kinds, plus "move" for a linked move pair. A
* replacement appears as both a delete and an insert, as it does in Word.
* A linked move is ONE change: `deletedText` is the text at the source,
* `insertedText` the text at the destination, and resolving it settles
* both locations atomically. */
kind: "insert" | "delete" | "format" | "move";
/** A person, "Revise Agent", an external agent, or "Anonymous". */
author: string;
authorType: "human" | "ai";
agentModel?: string;
/** Epoch milliseconds. */
createdAt?: number;
description?: string;
/** Blocks this change touches, in document order. */
blockIds: string[];
/** For kind "move": blocks the text moved out of, in document order. */
moveSourceBlockIds?: string[];
/** For kind "move": blocks the text moved into, in document order. */
moveDestinationBlockIds?: string[];
insertedText?: string;
deletedText?: string;
commentThreadId?: string;
}
```
## Settings
```ts
interface ReviseTrackedChangesSettings {
/** Start review with deleted text shown inline (strikethrough). Default
* false. */
showRemovalsInReview?: boolean;
/** "revise" (default): per-kind colors with a soft background tint.
* "word": classic redlines. All revisions in red, insertions underlined,
* deletions struck through, no background tint. */
markupStyle?: "revise" | "word";
}
interface ReviseEditorSettings {
trackedChanges?: ReviseTrackedChangesSettings;
}
```
## Modes and zoom
```ts
type ReviseDocumentMode = "editing" | "suggesting" | "viewing";
type ReviseToolbarMode = "native" | "compact" | "floating" | "none";
type ReviseZoom = number | "fit-width";
interface ReviseZoomState {
zoom: ReviseZoom;
scale: number; // effective canvas scale; computed for fit-width
}
```
## Toolbar and view state
```ts
interface ReviseToolbarState {
ready: boolean;
readOnly: boolean;
documentMode: ReviseDocumentMode;
activeTab: "edit" | "layout" | "insert" | "tools" | "review";
hasSelection: boolean;
canUndo: boolean;
canRedo: boolean;
formatting: Record & {
bold?: boolean;
italic?: boolean;
underline?: boolean;
strikethrough?: boolean;
color?: string;
backgroundColor?: string;
fontFamily?: string;
};
heading?: 1 | 2 | 3 | 4 | 5 | 6;
alignment?: "left" | "center" | "right" | "justify";
lineSpacing?: number;
fontSizePt?: number;
blockType: { type: string; variant?: string } | null;
review: {
open: boolean;
targetCount: number;
currentTargetSuggestionCount: number;
allOpenSuggestionCount: number;
showRemovals: boolean;
viewMode: "all-markup" | "final" | "original";
};
}
interface ReviseViewState {
ready: boolean;
title: string;
documentMode: ReviseDocumentMode;
readOnly: boolean;
commentsOpen: boolean;
commentCount: number;
reviewOpen: boolean;
reviewTargetCount: number;
}
```
## Review and comments
```ts
interface ReviseReviewState {
open: boolean;
commentsOpen: boolean;
targetCount: number;
currentTargetSuggestionCount: number;
allOpenSuggestionCount: number;
openSuggestionIds: string[];
currentSuggestionIds: string[];
visibleAgentSuggestionIds: string[];
activeCommentId: string | null;
showRemovals: boolean;
viewMode: "all-markup" | "final" | "original";
commentThreads: ReviseCommentThread[];
}
interface ReviseCommentAnchor {
blockId: string;
start: number;
end: number;
}
interface ReviseCommentRecord {
id: string;
author: string;
initials?: string;
authorId?: string;
authorImageUrl?: string;
createdAt: string;
bodyMd: string;
mentions?: ReviseCommentMention[];
parentId?: string;
relatedSuggestionIds?: string[];
resolved?: boolean;
}
interface ReviseCommentMention {
/** A Revise account id, or "revise" for the agent. */
id: string;
type: "user" | "agent";
/** The text it stands for, including "@". */
label: string;
/** UTF-16 offsets into bodyMd. */
start: number;
end: number;
email?: string;
}
interface ReviseCommentThread {
root: ReviseCommentRecord;
replies: ReviseCommentRecord[];
anchor: ReviseCommentAnchor | null;
relatedSuggestionIds: string[];
}
```
## Selection
```ts
interface ReviseSelectionSnapshot {
documentId: string | null;
anchor: ReviseSelectionPosition | null;
focus: ReviseSelectionPosition | null;
start: ReviseSelectionPosition | null;
end: ReviseSelectionPosition | null;
target: ReviseTextTarget | null;
selectionTarget: ReviseSelectionTarget | null;
activeMarks: string[];
activeCommentIds: string[];
activeChangeIds: string[];
quotedText: string;
text: string; // alias for quotedText
collapsed: boolean;
empty: boolean; // alias for collapsed
}
type ReviseSelectionCapture = ReviseSelectionSnapshot & {
documentId: string;
target: ReviseTextTarget;
selectionTarget: ReviseSelectionTarget;
};
type ReviseSelectionRestoreResult =
| { success: true }
| { success: false; reason: string };
```
## Tools
```ts
interface ReviseEditorToolDefinition {
name: string;
description: string;
inputSchema: Record;
}
interface ReviseEditorToolExecutionOptions {
documentId?: string;
directMode?: boolean;
}
// The shared tool contract (the same in revise's doc.tools):
type ReviseToolResult =
| { ok: true; value: ReviseToolResponse }
| { ok: false; error: ReviseToolFailure };
interface ReviseToolResponse {
callId: string;
tool: Name;
message: string | null;
data: ReviseToolData;
context: { format: "revise-html"; html: string } | null;
suggestionIds: string[] | null;
}
interface ReviseToolFailure {
callId: string;
tool: Name;
code: string;
message: string;
}
interface ReviseSuggestionDecision {
resolved: string[];
missing: string[];
unresolved: string[];
}
```
## Agent
```ts
/** Configure one transport — turnStream, baseUrl, or token — or agent runs
* fail fast with `agent_not_configured` and send nothing. */
interface ReviseAgentConfig {
/** Author name stamped on comments and tracked changes made through the
* tool surface. Defaults to "Revise Agent". */
name?: string;
/** Your own Revise agent backend. Without it, runs go to Revise's hosted
* agent, which needs a token. */
baseUrl?: string;
/** Required for Revise's hosted agent; an anonymous baseUrl needs none. */
token?: string;
conversationId?: string;
provider?: string;
model?: string;
/** Your own model or gateway. A new value applies to the next run. */
turnStream?: ReviseAgentTurnStream;
/** Skip the run-completion accounting call to baseUrl. Implied by
* turnStream. */
disableMetrics?: boolean;
}
interface ReviseAgentEvent {
state: "idle" | "streaming" | "complete" | "error";
activeTool: string | null;
status: string | null;
error?: string;
messages: ReviseAgentMessage[];
}
interface ReviseAgentRunResult extends ReviseAgentEvent {
actionCount: number;
}
```
## Fonts
```ts
interface ReviseFontDefinition {
family: string; // canonical, stored in the document and written to DOCX
label?: string; // picker label; defaults to family
cssFamily?: string; // browser preview stack; defaults to family
fontWeight?: number | string;
}
```
Source: https://revise.io/developer/docs/editor-sdk/api/types
---
# Revise API
> Edit and convert documents through a REST API.
Revise API reads, reviews, edits, and converts documents through a hosted REST service. Submit a job, poll its status or receive a webhook, then download the result. It works from any language. The TypeScript client also handles uploads and polling.
The production base URL is `https://revise.io/api`. All account endpoints below it begin with `/v1` and use bearer authentication.
Upload Markdown, convert it to PDF, and download the result.
Request schemas, response schemas, and examples for every endpoint.
## Choose an operation
| Task | Endpoint | Result |
| --- | --- | --- |
| Ask a question, edit text, or review a document | `POST /v1/prompt` | Prompt receipt, assistant response, and document artifacts when requested |
| Convert a document | `POST /v1/convert` | Conversion receipt and an output artifact |
| Supply a document | `POST /v1/files` | Uploaded file ID |
| Retrieve finished output | `GET /v1/artifacts/{id}/content` | File bytes, or JWE ciphertext for encrypted output |
Prompts accept DOCX, Markdown, HTML, and plain text. Edits produce both clean and tracked-changes DOCX files. Convert PDF, images, RTF, or ODT to a supported prompt format first. [Conversions](https://revise.io/developer/docs/revise-api/conversions) can produce DOCX, PDF, Markdown, HTML, or text.
## Requirements
- **Access:** create an [API key](https://revise.io/developer/docs/revise-api/authentication) and fund your account in the [console](https://revise.io/console).
- **Cost:** prompts charge a request fee, tool CPU, and inference; conversions use flat or per-page pricing. See [pricing and usage](https://revise.io/developer/docs/revise-api/pricing).
- **Models:** choose managed inference or use a provider key stored on your account. See [models and provider keys](https://revise.io/developer/docs/revise-api/models).
- **Limits:** uploads are limited to 18 MiB and scanned PDFs to 100 pages. Jobs share your account's queue and concurrency limits. See [job lifecycle](https://revise.io/developer/docs/revise-api/jobs).
- **Retention:** results expire 24 hours after terminal completion. Store outputs you need to keep; see [files and retention](https://revise.io/developer/docs/revise-api/files) and optional [output encryption](https://revise.io/developer/docs/revise-api/encryption).
The API returns finished results. It does not stream tokens or document edits.
The reference covers every public account endpoint, including cancellation, continuation, usage, and webhooks. Download the [OpenAPI specification](https://revise.io/api/v1/openapi.json) to generate a client in your language.
## TypeScript client
The guides show HTTP examples and examples for the `@reviseio/api` package. Its resource methods map to REST endpoints. Its `edit` and `convert` helpers combine upload, submission, and polling and return a lazy `Source` for the output.
```bash
npm install @reviseio/api
```
See [the TypeScript client](https://revise.io/developer/docs/revise-api/typescript) for `Source`, configuration, and helper behavior. API keys belong on your server.
## Revise Editor SDK
[Revise Editor SDK](https://revise.io/developer/docs/editor-sdk/introduction) embeds a collaborative editor in your application. Revise API is the hosted HTTP service described in this section. Each product has its own installation and authentication instructions.
## Support
For integration questions or capacity requirements, contact [support@revise.io](mailto:support@revise.io). For a specific request, include its job ID, error code, and approximate UTC time. Don't include API keys.
Source: https://revise.io/developer/docs/revise-api/introduction
---
# Authentication
> Authenticate REST requests with a Revise API key.
Create a key in the [API console](https://revise.io/console/api-keys) and add credit in [billing](https://revise.io/console/billing). Copy the secret when it is created; it is not shown again. The public TypeScript package needs no npm access token.
## Bearer authentication
Send the key on every account request, including artifact downloads:
```bash
export REVISE_API_KEY='your-api-key'
curl --fail-with-body 'https://revise.io/api/v1/account' \
-H "Authorization: Bearer $REVISE_API_KEY"
```
Use HTTPS in production. Store the key in server-side configuration. Don't put it in browser bundles, query strings, or public repositories, and don't send it to a remote input document's URL.
Keys grant access to the API account's resources. Files and jobs are account-scoped, so another active key on the same account can retrieve them. Use separate keys per service or environment to identify submissions and revoke access independently. Separate keys share the same storage and balance.
To rotate a key, create a replacement, update your application, verify an account request, then revoke the old key in the console. Expired or revoked keys return `401`. Provider keys are configured separately; see [models and provider keys](https://revise.io/developer/docs/revise-api/models).
## Request bodies
JSON requests use `Content-Type: application/json`. File uploads use `multipart/form-data`; let your HTTP library generate the boundary. Actions with no request body, such as cancellation and deletion, send no JSON body. Successful `204` responses have no JSON to parse.
Uploads, job creation, webhook registration, and manual delivery retries require an `Idempotency-Key`. Give each logical operation a stable key and reuse it only with the same request. See [errors and retries](https://revise.io/developer/docs/revise-api/errors).
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const account = await revise.account.get();
```
The default client base URL is `https://revise.io/api`. If you override it, omit the trailing `/v1`; the client adds endpoint paths itself. The client generates idempotency keys when omitted, but explicit keys let your application recover across process restarts.
Source: https://revise.io/developer/docs/revise-api/authentication
---
# REST quickstart
> Upload a document, submit a conversion, and retrieve the PDF.
This example converts Markdown to PDF with cURL. It costs $0.005.
Create an [API key](https://revise.io/developer/docs/revise-api/authentication) and set `REVISE_API_KEY` in your shell. Your account needs a positive credit balance. Each response returns the ID for the next request. Replace the example IDs with your own.
## 1. Upload a document
Send the Markdown directly from standard input. The multipart filename tells Revise which format to parse.
```bash
curl --fail-with-body 'https://revise.io/api/v1/files' \
-H "Authorization: Bearer $REVISE_API_KEY" \
-H 'Idempotency-Key: report-42-upload' \
-F 'file=@-;filename=report.md;type=text/markdown' <<'MARKDOWN'
# Quarterly report
Revenue increased this quarter.
MARKDOWN
```
Save the returned `id`, such as `file_…`. Uploads are ephemeral by default and can be claimed by one job. Use a new idempotency key for a different input document.
## 2. Request a PDF
```bash
curl --fail-with-body 'https://revise.io/api/v1/convert' \
-H "Authorization: Bearer $REVISE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: report-42-convert' \
-d '{"input":{"file_id":"file_REPLACE_ME"},"output":{"format":"pdf"}}'
```
The response is HTTP `202` with a queued conversion receipt. Save its `id`. A replayed request can return a completed receipt, so check the status.
## 3. Wait for completion
```bash
curl --fail-with-body 'https://revise.io/api/v1/convert/conversion_REPLACE_ME' \
-H "Authorization: Bearer $REVISE_API_KEY"
```
Repeat while status is `queued` or `running`. Wait at least one second between polls, and honor `Retry-After` when present. Stop on `succeeded`, `failed`, or `cancelled`. Inspect `error` on failure. On success, the downloadable metadata is in `output.artifact`.
## 4. Download the artifact
```bash
curl --fail-with-body --location \
'https://revise.io/api/v1/artifacts/art_REPLACE_ME/content' \
-H "Authorization: Bearer $REVISE_API_KEY" \
-o report.pdf
```
Verify the downloaded byte count and SHA-256 against `output.artifact.bytes` and `output.artifact.sha256` before using the file. The API endpoint requires the same account's bearer key and may redirect to a signed storage URL. `--location` follows that redirect; cURL drops Authorization when the destination origin changes. Do not use `--location-trusted`. See [files and artifacts](https://revise.io/developer/docs/revise-api/files).
## TypeScript
```ts
import { ReviseClient, Source } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const pdf = await revise.convert(
Source.fromText("# Quarterly report\nRevenue increased this quarter.", {
filename: "report.md",
}),
"pdf",
{ idempotencyKey: "report-42" },
);
await pdf.save("report.pdf", { signal: AbortSignal.timeout(60_000) });
```
The helper uploads, submits, and polls. `save()` downloads, verifies integrity, and writes the result. Use `bytes()` instead to send the output in a response or store it elsewhere. Results expire 24 hours after completion. To edit documents instead, see [prompts](https://revise.io/developer/docs/revise-api/prompts).
Source: https://revise.io/developer/docs/revise-api/quickstart
---
# Pricing and usage
> Understand charges, account credit, and usage reports.
Revise API uses account credit. Add funds and manage billing in the [console](https://revise.io/console/billing); the [pricing console](https://revise.io/console/pricing) shows the current schedule. Completed jobs return settled charges in `usage.cost`.
## Prompt pricing
Each prompt has three charge components:
| Component | Rate |
| --- | --- |
| Base fee | Graduated by accepted prompt count per account and UTC calendar month; see below. |
| Tool CPU | $0.05 per CPU-second. Model waiting and queue time are not CPU time. |
| Managed inference | Model and native search usage at provider rates plus 5%. |
| Accepted prompts in the month | Base fee per prompt |
| --- | --- |
| 1–1,000 | $0.05 |
| 1,001–5,000 | $0.035 |
| 5,001–10,000 | $0.02 |
| 10,001 onward | $0.01 |
Tiers are graduated, not retroactive. For 1,200 accepted prompts, the base fees for successful work are 1,000 × $0.05 + 200 × $0.035 = **$57**, plus CPU and inference. Each accepted prompt counts once across all account keys, including later failures or cancellations. Replays and resumes do not count again. Conversions do not count toward these tiers.
With your own provider key, Revise charges base and CPU fees; the provider bills inference separately. `usage.inference.billed_by` reports the payer. Native web search is enabled for prompts and can contribute to inference charges. Model input/output, cache activity, reasoning, and conversation compaction can also affect usage.
## Conversion pricing
| Input | Revise charge |
| --- | --- |
| DOCX, Markdown, HTML, text, RTF, ODT | $0.005 per conversion |
| PDF or image, managed scanning | $0.02 per page, including scan inference |
| PDF or image, your own OpenAI key | $0.005 per page; provider inference billed separately |
A 10-page managed PDF scan costs **$0.20**. Conversions have no additional tool CPU or Revise inference line item: the conversion fee appears under `base_fee_usd`. See [conversion limits](https://revise.io/developer/docs/revise-api/conversions#pricing-and-limits).
## Settlement and credit
Jobs are admitted and start only while the account balance is positive. Spending limits are thresholds for stopping work, not prepaid reservations. Settlement debits the actual charge once the job becomes terminal; in-flight work can take the balance below zero. Add credit to allow further work.
Successful partial prompts are billable. Cancellation or a customer limit can also incur charges for work already performed. Cancelling a queued job before any attempt starts has no charge. For a running conversion, cancellation charges pages already submitted for scanning, or the flat fee for deterministic conversion. Revise infrastructure and input-validation failures are not billed by Revise; your provider may still charge for calls made using your key. Always read the receipt's settled cost.
`usage.cost.itemized` contains disjoint `base_fee_usd`, `cpu_fee_usd`, and `inference_fee_usd` values that sum to `total_usd`. USD values are exact decimal strings; preserve them or use decimal arithmetic. Rates are fixed at admission and remain on historical receipts after content deletion.
## Usage reports
[`GET /v1/usage`](https://revise.io/developer/docs/revise-api/reference/get-usage) aggregates settled prompts **and conversions** by completion time. Queued, running, and paused jobs are excluded. The default is the past 30 days in daily UTC buckets.
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const report = await revise.usage.get({
start: "2026-09-01T00:00:00Z",
end: "2026-10-01T00:00:00Z",
bucket: "day",
group_by: "api_key_id",
});
console.log(report.totals.cost.total_usd);
for (const row of report.data) {
console.log(row.bucket_start, row.dimensions, row.cost.total_usd);
}
```
`start` is inclusive and `end` exclusive. Choose `hour`, `day`, `month`, or `none`; ranges may span at most 366 days, or 31 days for hourly buckets. Up to three comma-separated grouping dimensions are supported. Reports are limited to 1,000 rows, with no pagination; `422` means you must narrow the query or use fewer groups/coarser buckets.
Metadata can tag requests by customer or application, but it is retained only with request content. After deletion or expiry, metadata filters no longer match and metadata groups become `null`. **Store your own job-to-customer mapping for long-term attribution.** Account totals remain available. Model/provider dimensions describe the admitted configuration, including deterministic conversions; they do not prove that inference occurred.
Token counters can be `null` in aggregates containing older receipts without those counters. This does not make their charge totals incomplete. Cached input and cache-creation tokens are subsets of input tokens, not additional tokens to add again.
## Account and ledger
- [`GET /v1/account`](https://revise.io/developer/docs/revise-api/reference/get-account) returns the current signed USD credit balance and authenticated key ID.
- [`GET /v1/account/ledger`](https://revise.io/developer/docs/revise-api/reference/get-ledger) returns the latest 50 credit entries, with no pagination. `prompt_id` or `conversion_id` identifies the associated job; funding entries have neither.
- [`GET /v1/account/topups/{id}`](https://revise.io/developer/docs/revise-api/reference/get-topup) reads an existing top-up's status. Initiate funding and configure provider keys through the console.
Source: https://revise.io/developer/docs/revise-api/pricing
---
# Prompts and document edits
> Read, review, and edit documents, with clean and tracked DOCX output.
[`POST /v1/prompt`](https://revise.io/developer/docs/revise-api/reference/create-prompt) requires a nonempty `prompt`. Use it without a target for text requests, or supply one document with the access your workflow needs.
| `document_access` | Behavior |
| --- | --- |
| `read` (default) | Read the document and return an assistant response. |
| `comment` | Read and add review comments without editing the document text. |
| `edit` | Edit and comment; automatically export document artifacts. |
For read or comment requests, set `export_document: true` if you also need an exported document. Comment/edit access and export require a target; to draft a new document, upload a starting file first.
## Inputs and outputs
Prompt targets support **DOCX, UTF-8 Markdown, HTML, and plain text**, including `.markdown` and `.htm` aliases. Uploading other formats does not make them readable by a prompt: [convert](https://revise.io/developer/docs/revise-api/conversions) PDF, images, RTF, or ODT to DOCX first.
Supply `inputs: [{"role":"target","file_id":"file_…"}]`, or use an eligible `artifact_id`. Exactly one target is supported. Remote URLs and additional reference files are not REST inputs; the TypeScript `Source.fromUrl` helper downloads and uploads on your application's behalf.
Document exports always contain two DOCX artifacts:
- `clean`: pending revisions accepted.
- `tracked_changes`: revisions kept, for review in Word or another compatible editor.
Select by `variant`, not array order. Convert the chosen artifact separately if you need PDF, Markdown, HTML, or text. Tracked output can include revisions already present in the input; clean output accepts those pending revisions too.
## Edit a document
First [upload the file](https://revise.io/developer/docs/revise-api/files), then submit its ID:
```bash
curl --fail-with-body 'https://revise.io/api/v1/prompt' \
-H "Authorization: Bearer $REVISE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: contract-42-edit' \
-d '{
"prompt":"Change the payment term to 30 days. Preserve the remaining wording.",
"document_access":"edit",
"inputs":[{"role":"target","file_id":"file_REPLACE_ME"}]
}'
```
Poll [`GET /v1/prompt/{id}`](https://revise.io/developer/docs/revise-api/reference/get-prompt). A `succeeded` job can be partial. Inspect `incomplete`, `stop_reason`, and the assistant's `message.content` before treating the instruction as complete. `stop_reason` identifies `spending_limit` or `time_limit` when applicable.
The same edit in TypeScript:
```ts
import { ReviseClient, Source } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const edited = await revise.edit(
Source.fromPath("contract.docx"),
"Change the payment term to 30 days. Preserve the remaining wording.",
{ idempotencyKey: "contract-42" },
);
console.log(edited.result);
await edited.save("contract-clean.docx");
await edited.variant("tracked_changes").save("contract-review.docx");
```
## Review without editing
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const review = await revise.prompts.run({
prompt: "Flag ambiguous payment terms and explain each concern in a comment.",
document_access: "comment",
export_document: true,
inputs: [{ role: "target", file_id: "file_REPLACE_ME" }],
}, { idempotencyKey: "contract-42-review" });
console.log(review.message?.content);
for (const comment of review.comments ?? []) {
console.log(comment.id, comment.parentId, comment.bodyMd);
}
```
`comments` is a flat list of native comment records; replies identify their parent with `parentId`. `changes` lists pending tracked changes, including IDs, kinds, affected `blockIds`, and available inserted/deleted text. These native objects use camelCase fields. The DOCX carries document anchors and review markup.
To reduce JSON response size, set `response_options.include_comments` or `include_changes` to `false`. This omits those fields from receipts without removing comments or revisions from the document. Omitted options inherit from a predecessor or reused artifact; otherwise both default to `true`.
## Continue or branch
| Input | Use it when |
| --- | --- |
| `previous_prompt_id` | Continuing the latest successful request in the same conversation, with its working document and available conversation context. |
| `inputs[].artifact_id` | Starting a new conversation from a selected document variant, without the earlier chat history. |
| `inputs[].file_id` | Starting from an uploaded original or reusable template. |
A continuation must use the current successful conversation head, with no other request active. A stale or busy predecessor returns `409 stale_or_busy_conversation`; `error.details.current_head_prompt_id` identifies the current head. Do not combine `previous_prompt_id` with `inputs`. Access still defaults to `read`, so repeat `document_access: "edit"` for further edits.
Inspect `continuation` to see whether reasoning state was reused, reset, or compacted. Provider/model choices inherit from the predecessor when omitted; explicitly changing them can affect continuity.
Only artifacts with `input_eligible: true` can be reused directly. Request content must still be retained. Encrypted output disables both continuation and direct artifact reuse; see [output encryption](https://revise.io/developer/docs/revise-api/encryption).
Source: https://revise.io/developer/docs/revise-api/prompts
---
# File conversions
> Convert document formats or reconstruct editable content from PDFs and images.
[`POST /v1/convert`](https://revise.io/developer/docs/revise-api/reference/create-conversion) accepts an uploaded `file_id` and an output format. It returns a conversion receipt to poll. Conversion inputs cannot be artifact IDs; the TypeScript helper downloads and reuploads previous results automatically.
## Supported formats
| Input | Processing | Output |
| --- | --- | --- |
| DOCX, Markdown, HTML, plain text, RTF, ODT | Deterministic format conversion | `docx`, `pdf`, `md`, `html`, `txt` |
| PDF | AI reconstruction from page images | `docx`, `md`, `html`, `txt` |
| JPEG, PNG, WebP, GIF | AI reconstruction from an image | `docx`, `pdf`, `md`, `html`, `txt` |
Same-format conversion is rejected, including PDF to PDF. `.markdown` and `.htm` are recognized aliases. Text, Markdown, and HTML uploads must be UTF-8. RTF, ODT, and image formats are input-only.
Deterministic conversion uses the document's structure and supported formatting. PDF and image scanning reconstructs headings, paragraphs, lists, and tables. It does not reproduce the page pixel for pixel. Each image is one scanned page.
Scanning requires OpenAI. Managed scans use the default OpenAI model. With your own OpenAI key, you can choose another catalogued OpenAI model. Deterministic conversions reject nonempty `inference` settings. See [models and provider keys](https://revise.io/developer/docs/revise-api/models).
## Submit and retrieve
```bash
curl --fail-with-body 'https://revise.io/api/v1/convert' \
-H "Authorization: Bearer $REVISE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: export-42' \
-d '{"input":{"file_id":"file_REPLACE_ME"},"output":{"format":"pdf"}}'
```
Poll until `succeeded`, `failed`, or `cancelled`. On success, download `output.artifact`. Its variant is `converted`. The receipt's `mode` is `deterministic` or `vision`; `usage.conversion` records the billed unit, quantity, and rate.
```ts
import { ReviseClient, Source } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const edited = await revise.edit(
Source.fromPath("contract.docx"),
"Change the payment term to 30 days.",
{ idempotencyKey: "contract-42-edit" },
);
const pdf = await revise.convert(edited, "pdf", {
idempotencyKey: "contract-42-pdf",
});
await pdf.save("contract.pdf");
```
Use `revise.conversions.create(body)` to submit without waiting, or `revise.conversions.run(body)` to wait for success when you already have a file ID. Download/reupload steps are subject to the 18 MiB input limit, even when a previous output was larger.
## Pricing and limits
Deterministic conversions cost **$0.005 each**. Scans cost **$0.02 per page**, including managed inference, or **$0.005 per page** plus your provider's inference charge when using your own OpenAI key. Conversions have no separate CPU charge and do not count toward prompt request-fee tiers. See [pricing and usage](https://revise.io/developer/docs/revise-api/pricing).
Uploads are limited to **18 MiB**, PDFs to **100 pages**, and conversion output to **50 MiB before encryption**. JWE encoding increases downloaded size. There is no default spending cap. If you set `max_platform_cost_usd`, a scan that exceeds it fails with `conversion_limit_exceeded` before inference.
PDF structure is checked at upload and again during processing. A failed job can report:
| Code | Action |
| --- | --- |
| `pdf_page_limit_exceeded` | Split the PDF into files of at most 100 pages. |
| `pdf_password_protected` | Remove the password before uploading. |
| `invalid_pdf` | Export a valid PDF and retry. |
| `empty_pdf` | Supply a PDF with at least one page. |
| `conversion_output_too_large` | Split or simplify the input to reduce output size. |
These validation failures are not billed by Revise. Cancellation after processing starts can be billed; inspect settled usage rather than inferring cost from status alone. After retention expiry or deletion, the receipt remains but the artifact is unavailable and some format fields may be empty.
Source: https://revise.io/developer/docs/revise-api/conversions
---
# Models and provider keys
> Choose a model and decide who bills its inference.
Prompts support models from OpenAI, Anthropic, Google Gemini, and xAI. [`GET /v1/models`](https://revise.io/developer/docs/revise-api/reference/list-models) returns the supported IDs and current defaults without consuming inference or credits.
```bash
curl --fail-with-body 'https://revise.io/api/v1/models?provider=openai' \
-H "Authorization: Bearer $REVISE_API_KEY"
```
Pass the returned `provider` and `id` as `inference.provider` and `inference.model`. Use `gemini` as the public provider name for Google models.
- Omit `inference` to use the API default for a new conversation.
- Specify only a provider, or set `model: "default"`, to use that provider's default.
- Pin both fields when reproducible model selection matters. The receipt records the resolved selection.
The catalog's `default` is the API-wide default, even on a filtered response. `default_for_provider` marks each provider's default. The catalog does not report live health, capacity, or pricing.
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const catalog = await revise.models.list();
const result = await revise.prompts.run({
prompt: "Rewrite this sentence more clearly: We will revert in due course.",
inference: catalog.default,
}, { idempotencyKey: "rewrite-42" });
console.log(result.message?.content, result.inference);
```
## Use your own provider key
Configure a provider key in the [API console](https://revise.io/console/api-keys). Continue authenticating HTTP requests with your **Revise API key**; provider credentials are stored on the account, not sent in job bodies.
| `inference.billing` | Selection |
| --- | --- |
| Omitted or `revise` | Use the account's stored key for the resolved provider if present; otherwise use managed inference. |
| `provider` | Require that stored key. Missing credentials return `400 provider_key_required`. |
Setting `billing: "revise"` does not override a stored provider key. When your key is used, your provider bills inference directly and Revise charges only the applicable platform fees. Check `usage.inference.billed_by` and [pricing](https://revise.io/developer/docs/revise-api/pricing).
Each job keeps the credential selected at submission. Deleting or replacing that credential can fail its next model call with `credential_unavailable`; it does not fall back to managed inference. Provider rejection can appear as `provider_credential_rejected`.
## Conversion scanning
PDF and image conversions use **OpenAI only**. Managed scanning uses the default OpenAI model. With your own OpenAI key, scanning can use another OpenAI model listed by the catalog. Other selections return `model_unavailable`.
Deterministic conversions do not use inference and reject nonempty inference settings. See [conversions](https://revise.io/developer/docs/revise-api/conversions).
Source: https://revise.io/developer/docs/revise-api/models
---
# Files, artifacts, and retention
> Manage uploaded inputs, verified outputs, and content lifetime.
Files are uploaded inputs. Artifacts are job outputs. Their IDs and endpoints are separate and scoped to your API account.
## Upload a file
Send exactly one multipart field named `file` with a filename. The filename extension determines format; Content-Type is advisory. An optional `lifetime` field accepts `ephemeral` (default) or `persistent`.
```bash
curl --fail-with-body 'https://revise.io/api/v1/files' \
-H "Authorization: Bearer $REVISE_API_KEY" \
-H 'Idempotency-Key: template-42-upload' \
-F 'file=@template.docx' \
-F 'lifetime=persistent'
```
Uploads must be nonempty and at most **18 MiB**. Accounts can retain up to **100 undeleted uploads totaling 256 MiB**. Delete unused persistent files to free capacity. Filenames must be at most 255 bytes and contain no path separators or control characters. DOCX and ODT archive contents are also checked for excessive expanded size.
Use persistent uploads for templates shared across jobs. [`GET /v1/files/{id}`](https://revise.io/developer/docs/revise-api/reference/get-file) returns metadata, including SHA-256, size, status, and lifetime. There is no endpoint to list uploads or download their original bytes; store file IDs and keep your originals.
Upload support is broader than prompt support. See [prompt inputs](https://revise.io/developer/docs/revise-api/prompts#inputs-and-outputs) and [conversion formats](https://revise.io/developer/docs/revise-api/conversions#supported-formats).
## Download an artifact
Get metadata from a completed receipt or [`GET /v1/artifacts/{id}`](https://revise.io/developer/docs/revise-api/reference/get-artifact). It includes `filename`, `content_type`, `bytes`, `sha256`, `variant`, `input_eligible`, and expiry/deletion information.
Fetch [`GET /v1/artifacts/{id}/content`](https://revise.io/developer/docs/revise-api/reference/download-artifact) with bearer authentication. The API returns bytes or redirects to a signed storage URL valid for five minutes. Follow redirects without forwarding Authorization to another origin. With cURL, use `--location`, never `--location-trusted`. Request a fresh download URL through the API if the signed URL expires.
Compare the downloaded length and SHA-256 with metadata before using the file. For encrypted output, these describe ciphertext; the download has MIME `application/jose` and a `.jwe` filename. Artifact metadata still describes the original plaintext format.
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const artifact = await revise.artifacts.download("art_REPLACE_ME", {
signal: AbortSignal.timeout(60_000),
maxBytes: 64 * 1024 * 1024,
});
const bytes = artifact.bytes; // length and SHA-256 verified
```
`artifacts.content(id)` returns a raw Response for streaming; your code owns consuming/closing it and checking integrity. `Source.stream()` buffers and verifies before returning a fresh stream.
## Retention
| Content | Lifetime |
| --- | --- |
| Unclaimed ephemeral upload | 24 hours from upload. |
| Claimed ephemeral upload | Reserved for one job. Deleted on success; retained for 24 hours after failure/cancellation, unless explicitly deleted earlier. |
| Persistent upload | Until explicitly deleted. |
| Job content and artifacts | 24 hours after `succeeded`, `failed`, or `cancelled`, unless deleted earlier. |
An ephemeral file remains claimed even when its job fails. To create a different job from that input, upload the original again. Use persistent files for deliberate reuse. Replaying the original job's idempotency key retrieves that job; it does not rerun it or restore deleted content.
Delete a file with [`DELETE /v1/files/{id}`](https://revise.io/developer/docs/revise-api/reference/delete-file). Delete a job's content with [`DELETE /v1/prompt/{id}/content`](https://revise.io/developer/docs/revise-api/reference/delete-prompt-content) or [`DELETE /v1/convert/{id}/content`](https://revise.io/developer/docs/revise-api/reference/delete-conversion-content). These calls send no body and return `204`; repeated deletion succeeds. Active jobs or pinned content can return `409`.
Content deletion removes retained prompt/result content, customer metadata, document state, artifacts, and claimed ephemeral source data. Persistent templates are excluded. Billing receipts, IDs, status, and operational records remain. Expired/deleted downloads return `410`; a retained metadata record does not guarantee available bytes.
With `retention.delete_after_webhook_id`, acknowledging the designated completion webhook with 2xx can delete the job's content. Download and durably store results before acknowledging it. See [webhooks](https://revise.io/developer/docs/revise-api/webhooks).
Revise and the selected inference provider process plaintext document content. [Output encryption](https://revise.io/developer/docs/revise-api/encryption) protects exported artifact bytes; it does not encrypt receipt text or make input processing end-to-end encrypted. See the [Privacy Policy](https://revise.io/privacy) for service data handling.
Source: https://revise.io/developer/docs/revise-api/files
---
# Job lifecycle and limits
> Poll, cancel, and resume work, and control execution budgets.
A `202` response confirms submission, not completion. Save the job ID and idempotency key. Poll the matching get endpoint or subscribe to [webhooks](https://revise.io/developer/docs/revise-api/webhooks).
| Status | Next step |
| --- | --- |
| `queued`, `running` | Keep polling, at least one second apart. Honor `Retry-After` when present. |
| `succeeded` | Inspect the receipt and download its artifacts before expiry. Prompts can still report partial completion. |
| `failed`, `cancelled` | Work has stopped. Inspect `error` and settled usage. |
| `paused` | Prompt-only state. Resume with higher budgets or cancel. |
## Work and spending limits
Both creation endpoints accept `limits`:
| Field | Default | Meaning |
| --- | --- | --- |
| `max_platform_cost_usd` | No cap | Positive amount only. Prompt base fee plus tool CPU, or conversion flat/page fees. |
| `max_inference_cost_usd` | No cap | Positive amount only. Observed inference spending threshold. With your provider key, it measures provider spend even though Revise does not bill that inference. |
| `max_seconds` | `900` | Work budget in seconds; accepted range 120–3600. Starts when a worker begins the job, excluding queue time. |
Use decimal strings for money. Platform limits must cover at least the applicable base fee, conversion fee, or one scan page. Scans also check that the complete document's page fee fits before inference.
Prompt thresholds stop new model calls based on observed usage. They are **soft limits**. An active response, tool, or export can finish past a threshold. Inspect `incomplete` and `stop_reason` for successful partial prompts. Client timeouts are separate waiting budgets and do not cap billing.
## Queue and concurrency
Prompts and conversions share the account's queue and worker allocation. New console accounts default to one running job and up to 100 queued jobs; account settings can differ. Additional jobs wait for a slot. A paused prompt holds an account slot until resumed, cancelled, or expired.
A full queue returns `429 account_queue_full`. Transfer/API capacity can return `503 server_busy`. Apply bounded backoff with the same idempotency key. Jobs need a positive account balance both for admission and to start running; existing queued work can wait for a top-up. Contact [support](mailto:support@revise.io) for capacity requirements.
## Cancel and resume
Closing the connection or aborting a client request does **not** cancel a server job. Call `POST /v1/prompt/{id}/cancel` or `POST /v1/convert/{id}/cancel` with no body. Cancellation can return `running` at first. Keep polling until the job stops. Cancellation does not undo completed work.
A paused prompt expires after 24 hours if not resumed. To resume, send new **absolute** limits. Omit a spending limit to remove that cap. Neither may decrease, and at least one must increase or be removed. The job keeps its ID and incurs no second base fee. The resume body does not accept `max_seconds`. Conversions cannot be resumed.
## Wait for an existing job
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.prompts.wait("prompt_REPLACE_ME", {
timeoutMs: 120_000,
pollIntervalMs: 1_000,
});
if (result.status === "succeeded") {
console.log(result.incomplete, result.artifacts);
} else {
console.log(result.status, result.error);
}
```
`wait` returns paused, failed, and cancelled receipts. `run`, `edit`, and `convert` require success and throw `ReviseJobError` for those outcomes. All still require checking partial-completion fields on successful prompts.
The `edit`/`convert` deadline covers source resolution, upload, submission, and polling. A returned `Source` downloads later with its own signal. Pass the same external signal to both operations when you need one deadline for the whole workflow.
## Request history
Prompt and conversion lists return newest-first pages with `data` and `next_cursor`. The default page size is 20, maximum 100; `next_cursor: null` ends pagination. Repeat the same filters on each page. Status is live, so pages are not a frozen snapshot.
Prompt lists support status, conversation, submitting key, creation-time, and metadata filters. Conversion lists support status only, plus pagination. Use [usage reports](https://revise.io/developer/docs/revise-api/pricing#usage-reports) for settled cost aggregation by completion time.
Source: https://revise.io/developer/docs/revise-api/jobs
---
# Errors, idempotency, and retries
> Recover interrupted work without submitting a duplicate job.
Request failures return an HTTP error. After a job is admitted, processing failures appear on its receipt. Fetching a failed job returns HTTP `200`.
API errors use this envelope:
```json
{"error":{"code":"insufficient_credit","message":"insufficient credit"}}
```
Handle errors by `error.code`. Log the message and optional `details` for diagnosis. Gateways can return non-JSON errors.
| Response | Action |
| --- | --- |
| `400` | Correct the request or unsupported options. |
| `401`, `403` | Check credentials, expiry/revocation, and account status. |
| `402` | Add account credit. Retrying does not help. |
| `413`, `415` | Reduce input size or correct Content-Type. |
| `422` | Narrow a usage report that exceeds its row limit. |
| `404`, `410` | Check the ID and whether content expired or was deleted. |
| `409` | Check the code. Idempotency conflicts, claimed inputs, and pinned content each need a different fix. |
| `429`, `502`, `503`, `504` | Retry with bounded backoff if the operation is safe to replay. Honor `Retry-After`. |
Common codes are `ephemeral_file_already_claimed` (upload again or use a persistent template), `request_content_in_use` (wait for dependent work), and `model_unavailable` (check the model and operation). For recovery details, see [files](https://revise.io/developer/docs/revise-api/files), [models](https://revise.io/developer/docs/revise-api/models), and [job lifecycle](https://revise.io/developer/docs/revise-api/jobs).
## Idempotency
Send `Idempotency-Key` on uploads, prompt/conversion creation, webhook creation, and manual webhook-delivery retry. Repeating an operation with the same key and body replays it. Changing the body returns a conflict. Upload and job-creation replays include `Idempotent-Replayed: true`; webhook registration and delivery retries do not expose that header.
Use nonempty printable ASCII keys without spaces. Ordinary creation keys allow up to 200 characters; webhook creation/retry keys allow 128. Use distinct keys for each operation. Prompt and conversion creation share one key namespace within the account, across API keys; uploads have a separate namespace. Upload identity includes filename, bytes, and lifetime. New encryption public keys change the request body and therefore need a new operation key. Replays return the current receipt, including any expiry or content deletion, and never rerun a completed job.
Don't retry cancel or resume actions without checking the job, and don't generate a new key after an ambiguous submission failure. If you know the job ID, retrieve that job. If the submission response was lost, replay the identical request with its original key. A failed download is not a reason to create another paid job.
## TypeScript recovery
```ts
import { ReviseClient, Source, ReviseWorkflowError } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
try {
const pdf = await revise.convert(
Source.fromText("# Report", { filename: "report.md" }),
"pdf",
{ idempotencyKey: "report-42", timeoutMs: 120_000 },
);
const bytes = await pdf.bytes({ signal: AbortSignal.timeout(60_000) });
} catch (error) {
if (error instanceof ReviseWorkflowError) {
console.error(error.recovery.jobId, error.recovery.stage);
// Persist recovery securely to resume this operation.
}
throw error;
}
```
Helpers derive `:upload` and `:prompt`/`:convert` keys from a base of at most 192 characters. `onProgress` provides keys, IDs, and the latest receipt synchronously; it cannot await durable storage. Use low-level methods when persistence must complete between network steps.
The client retries HTTP `429/502/503/504` on GET, DELETE, and supported keyed POST calls, with two additional attempts by default. It honors `Retry-After`; if the advised wait exceeds the configured `maxRetryDelayMs` (60 seconds by default), it returns the error instead of retrying early. Set `maxRetries: 0` to disable retries. Transport exceptions and body-read failures are not automatically retried.
`ReviseJobError` extends `ReviseWorkflowError` and exposes the unsuccessful or paused receipt as `.result`. `ReviseWorkflowError` preserves recovery plus its cause. `ReviseApiError` exposes status, code, body, and headers; `ReviseArtifactError` describes integrity failures; `SourceError` describes source resolution failures. Lazy output reads happen after the workflow: retain the returned Source and its receipt to retry reading without resubmitting.
Source: https://revise.io/developer/docs/revise-api/errors
---
# Output encryption
> Request authenticated JWE ciphertext for document artifacts.
Encrypt exported files for an RSA public key by setting `output_encryption` on a prompt or conversion:
```json
{
"output_encryption": {
"format": "jwe",
"public_key_pem": "-----BEGIN PUBLIC KEY-----\n...\n-----END PUBLIC KEY-----"
}
}
```
Supply an RSA SubjectPublicKeyInfo public PEM, 2048–8192 bits. Your application owns the private key; send only the public key. For read/comment prompts, output encryption requires `export_document: true`.
## What is encrypted
Exported artifact bytes are compact JWE using `RSA-OAEP-256` and `A256GCM`. Messages and receipt metadata remain ordinary JSON and may contain document content. Revise and the inference provider process plaintext inputs; output encryption is not end-to-end input encryption.
The download endpoint returns `application/jose` and a `.jwe` filename. Artifact `bytes` and `sha256` describe ciphertext; `content_type` and `filename` in the artifact metadata describe the original plaintext file.
Verify ciphertext length and SHA-256 before decrypting. Use a JWE library restricted to `RSA-OAEP-256` and `A256GCM`, and authenticate the protected header and GCM tag before releasing plaintext. A decoded header alone is not proof of authenticity.
Validate these protected header fields against the expected result:
| Header | Expected value |
| --- | --- |
| `kid` | `artifact.encryption.key_id`, the base64url SHA-256 JWK thumbprint of your public key. |
| `cty` | The original plaintext MIME type. |
| `prompt_id` or `conversion_id` | The job that produced the artifact. |
| `artifact_id` | The selected artifact ID. |
| `variant` | `clean`, `tracked_changes`, or `converted`, matching metadata. |
## Chaining encrypted output
Encrypted artifacts cannot be used directly as another input. Decrypt and authenticate locally, then upload the plaintext if another request needs it.
Even a plaintext artifact cannot be submitted by `artifact_id` to a new encrypted-output prompt. Download and reupload it as a file instead. The TypeScript `edit` helper handles this step for plaintext artifact sources, subject to the 18 MiB upload limit.
```ts
import { readFile } from "node:fs/promises";
import { ReviseClient, Source } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const publicKey = await readFile("recipient-public.pem", "utf8");
const output = await revise.convert(
Source.fromText("# Confidential report", { filename: "report.md" }),
"pdf",
{ outputEncryption: { format: "jwe", public_key_pem: publicKey } },
);
const ciphertext = await output.bytes();
console.log(output.contentType); // application/jose
console.log(output.filename); // ends in .jwe
```
The client retrieves and verifies ciphertext. It does not generate keys or decrypt. Its `Source` reports ciphertext MIME and filenames, including tracked variants, while `source.artifact` retains the original format metadata.
Source: https://revise.io/developer/docs/revise-api/encryption
---
# Webhooks
> Receive, verify, and acknowledge job status notifications.
Register an endpoint with [`POST /v1/webhooks`](https://revise.io/developer/docs/revise-api/reference/create-webhook). Registrations apply to matching jobs across your account. Up to 10 active endpoints are allowed, using HTTPS on port 443 and publicly reachable addresses. Redirects are not followed.
Supported events are `prompt.completed`, `prompt.failed`, `prompt.cancelled`, `prompt.paused`, `conversion.completed`, `conversion.failed`, and `conversion.cancelled`.
```bash
curl --fail-with-body 'https://revise.io/api/v1/webhooks' \
-H "Authorization: Bearer $REVISE_API_KEY" \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: completion-hook-42' \
-d '{"url":"https://example.com/hooks/revise","events":["prompt.completed","prompt.failed","conversion.completed","conversion.failed"]}'
```
Store the returned `signing_secret` securely. It is returned on creation and idempotent replay, not on list responses. The TypeScript equivalent is `revise.webhooks.create(body, { idempotencyKey })`.
## Notification payload
Notifications contain a job reference, not document bytes or the full receipt:
```json
{
"id": "evt_00000000-0000-4000-8000-000000000001",
"type": "prompt.completed",
"timestamp": "2026-09-14T12:00:00Z",
"data": {
"prompt_id": "prompt_00000000-0000-4000-8000-000000000002",
"status": "succeeded",
"delete_on_acknowledgement": false,
"content_expires_at": "2026-09-15T12:00:00Z"
}
}
```
Conversion events use `data.conversion_id` instead of `data.prompt_id`. A paused prompt can have `content_expires_at: null`. Retrieve the receipt with your API key, then download any artifacts. A completion event means `succeeded`; still inspect prompt partial-completion fields.
## Verify signatures
Deliveries include `webhook-id`, `webhook-timestamp` (Unix seconds), and `webhook-signature`. To verify a delivery:
1. Remove `whsec_` from the signing secret and base64-decode the remainder.
2. Use the raw request body. Don't parse and reserialize the JSON before verifying.
3. Compute HMAC-SHA256 over `webhook-id + "." + webhook-timestamp + "." + rawBody`.
4. Base64-encode the digest and compare the `v1,` signature using a constant-time comparison.
5. Check timestamp freshness and deduplicate accepted notification IDs.
A five-minute timestamp tolerance rejects stale attempts if your clocks are synchronized. Retries keep the notification ID and body but carry a fresh timestamp/signature. Each registered endpoint receives its own notification ID, so use the job ID as well when coordinating shared side effects across receivers.
Verify before parsing or trusting the payload. The client package manages webhook registrations and delivery operations; receiver verification belongs to your application.
## Acknowledgements and retries
Return 2xx within **30 seconds** after durably accepting the notification. Non-2xx responses and network failures trigger retries. Deliveries can be duplicated or arrive out of order. Retrieve the job to confirm its current state.
Automatic delivery makes up to 11 attempts within 24 hours. Retry delays start at 10 seconds and grow to 8 hours, with jitter. Exact delivery timing is not guaranteed.
List delivery history with [`GET /v1/webhooks/{id}/deliveries`](https://revise.io/developer/docs/revise-api/reference/list-webhook-deliveries). History lasts 30 days. `exhausted` deliveries can be manually retried up to three times within seven days of creation, each with its own idempotency key. Manual retries do not restore expired job content.
Deleting a webhook disables it and cancels pending deliveries. An in-flight delivery may already have reached your receiver. There is no update endpoint; register a replacement if the URL or event subscriptions change.
## Delete content after acknowledgement
Set the following on a prompt or conversion to remove its retained content after delivery:
```json
{
"retention": {
"delete_after_webhook_id": "wh_00000000-0000-4000-8000-000000000003"
}
}
```
The endpoint must belong to your account and subscribe to the matching completion event. The payload then has `delete_on_acknowledgement: true`.
**Download and durably store all required output before returning 2xx.** If that cannot finish within the delivery timeout, use ordinary retention and explicitly delete content after background processing. Acknowledgement does not extend the 24-hour content deadline.
Source: https://revise.io/developer/docs/revise-api/webhooks
---
# TypeScript client
> Use @reviseio/api for typed REST calls and document workflows.
`@reviseio/api` is a public package for **Node.js 22+** or compatible runtimes with native fetch, FormData, Blob, Web Crypto, and AbortSignal.any. It has no runtime npm dependencies. Use it on your server. API keys must stay out of browser bundles.
```bash
npm install @reviseio/api
```
## Edit and convert
`edit(source, prompt, options)` and `convert(source, format, options)` resolve input, upload when needed, submit the job, and poll. Both return a `Source` whose output bytes download only when requested.
```ts
import { ReviseClient, Source } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const edited = await revise.edit(
Source.fromPath("contract.docx"),
"Change the payment term to 30 days.",
{ idempotencyKey: "contract-42-edit" },
);
const pdf = await revise.convert(edited, "pdf", {
idempotencyKey: "contract-42-pdf",
});
await pdf.save("contract.pdf", { signal: AbortSignal.timeout(60_000) });
```
Edits select the clean DOCX by default. Set `trackedChanges: true` to select tracked output, or use `edited.variant("tracked_changes")` to access it from the same job. This changes output selection, not whether revisions are recorded. `source.result` contains the receipt; check prompt `incomplete` and `stop_reason`. `source.artifact` contains selected output metadata.
An eligible artifact passes directly into another edit. Converting an artifact downloads and reuploads it because conversions require file IDs. Edits requesting output encryption also reupload plaintext artifact inputs. Encrypted artifacts must be decrypted locally before reuse. Every reupload remains subject to the 18 MiB input limit.
## Create a Source
| Factory | Input |
| --- | --- |
| `Source.fromBytes(bytes, { filename })` | Uint8Array, Buffer, or ArrayBuffer. |
| `Source.fromPath(path, options?)` | Local file, read lazily; Node only. |
| `Source.fromUrl(url, options?)` | HTTP(S) URL; optional source-specific `headers` and `fetch`. |
| `Source.fromBlob(blob, options?)` | Blob or File; a File supplies its name. |
| `Source.fromStream(streamOrFactory, { filename })` | Binary Web stream, Node Readable, or async iterable of Uint8Array. |
| `Source.fromResponse(response, options?)` | Existing fetch Response. |
| `Source.fromText(text, options?)` | UTF-8 text; defaults to `document.txt`. |
| `Source.fromFileId(id, options?)` | Existing upload ID. Original bytes cannot be downloaded. |
| `Source.fromArtifact(metadata, { client, result }?)` | Existing artifact. Bind its client for byte reads; include a receipt to select its other variants. |
Factories accept `filename`, `contentType`, `maxBytes`, and upload `lifetime` options. Use `lifetime: "persistent"` for reusable templates. Options on an existing file ID do not change the server-side file's lifetime or name.
The filename determines input format. URL/Response sources infer it from Content-Disposition or the URL path; supply one for opaque URLs or unnamed Blobs. Source URLs use their own credentials, never your Revise key. Redirects are disabled. Enforce your own network access policy when accepting URLs from users.
## Read output
- `bytes({ signal, maxBytes })` returns an independent Uint8Array.
- `blob(options)` returns an immutable Blob.
- `stream(options)` returns a fresh Web stream **after buffering and verification**.
- `save(path, options)` writes to an explicit local path; Node only.
Source reads verify artifact length and SHA-256. The default read limit is 64 MiB; set `maxBytes` for larger encrypted output. Upload resolution always caps at 18 MiB. These limits bound file size, not total process memory.
Successful reads are cached snapshots. For retryable stream input, provide a factory that opens a new stream after a failure. A failed one-shot stream or Response cannot restart; concurrent readers can share that failure. See [error recovery](https://revise.io/developer/docs/revise-api/errors) before resubmitting work after a timeout.
## Configuration and deadlines
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({
apiKey: process.env.REVISE_API_KEY!,
baseUrl: "https://revise.io/api", // omit /v1
timeoutMs: 1_000_000,
pollIntervalMs: 1_000,
maxRetries: 2,
maxRetryDelayMs: 60_000,
});
```
These values are the defaults. `timeoutMs` applies to workflows and waits, not individual REST calls; supply `{ signal }` to bound an individual call. `edit`/`convert` timeouts cover source resolution through polling. Read the returned Source with its own signal to bound the later download. Aborting locally does not cancel the server job.
High-level options use `outputEncryption`, `responseOptions` (edit only), `trackedChanges` (edit only), `inference`, `limits`, `metadata`, and `retention`. REST-shaped methods use wire names such as `output_encryption`. `onProgress` is synchronous and exposes recovery keys and known IDs at workflow transitions; use low-level calls when you must await durable storage between steps.
## REST methods
The client returns server JSON without renaming fields. Each endpoint page includes an example.
| Resource | Methods |
| --- | --- |
| `files` | `upload`, `get`, `delete` |
| `prompts` | `create`, `get`, `list`, `cancel`, `resume`, `deleteContent`, `wait`, `run` |
| `conversions` | `create`, `get`, `list`, `cancel`, `deleteContent`, `wait`, `run` |
| `artifacts` | `get`, `content`, `download` |
| `account` | `get`, `ledger`, `topup` |
| `models` | `list` |
| `usage` | `get` |
| `webhooks` | `create`, `list`, `delete`, `deliveries`, `retry` |
`create` returns immediately after submission. `wait(idOrReceipt)` returns any non-running receipt, including paused or failed work. `run(body)` requires success and throws `ReviseJobError` otherwise. Use `prompts.run` for text-only, read, or comment requests.
Every endpoint accepts `{ signal, idempotencyKey }` as its final options argument. Creation methods generate a key when omitted; supply a stable key to recover across restarts. Lists preserve `next_cursor`; pass it with the same filters for the next page. Query dates are RFC3339 strings, metadata filters are objects, and `group_by` is a comma-separated string.
`artifacts.download(idOrMetadata, options)` fetches and verifies bytes. `artifacts.content(id)` returns a raw Response that your code must consume and verify. `promptFile(file, body)` and `convertFile(file, body)` provide eager upload/job/download workflows when you want the receipt and all output bytes together; their deadlines include downloads.
The package exports `PromptRequest`, `Prompt`, `ConversionRequest`, `Conversion`, `Artifact`, `UploadedFile`, options/error types, and OpenAPI-generated `components`/`paths`. `getResponseMetadata(result)` exposes HTTP headers, status, Location, and replay information for original JSON results. Source receipt/metadata getters return defensive copies, so response metadata is not attached to those copies.
See [errors and retries](https://revise.io/developer/docs/revise-api/errors) for error classes, retry defaults, and recovery after an ambiguous submission.
Source: https://revise.io/developer/docs/revise-api/typescript
---
# Create a prompt
> Submit a text request or read, comment on, or edit one document.
See [prompt workflows](https://revise.io/developer/docs/revise-api/prompts) for integration guidance.
This call returns an admission receipt. Poll the get endpoint until work stops, then inspect the result and download its artifacts. See [job lifecycle](https://revise.io/developer/docs/revise-api/jobs).
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.prompts.create(
{ prompt: "Write a short welcome message." },
{ idempotencyKey: "welcome-42" },
);
```
`POST https://revise.io/api/v1/prompt`
Submit a text request or read, comment on, or edit one document. Prompt targets support DOCX, Markdown, HTML, and plain text. Convert other upload formats first. Edit access exports clean and tracked-changes DOCX artifacts; read/comment access requires export_document=true to export. A 202 response confirms admission; poll the receipt for completion and inspect incomplete and stop_reason even on success. Use /v1/models for supported providers and models.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Headers
- `Idempotency-Key` (string, required): min length 1. max length 200
## Body
- `prompt` (string, required): Nonempty instruction, at most 1 MiB of UTF-8 bytes. The entire JSON body is limited to 2 MiB.. min length 1. max length 1048576
- `document_access` (string): one of "read", "comment", "edit"
- `previous_prompt_id` (string): Latest successful head of the same conversation, with retained content and no active successor. Cannot be combined with inputs. Inherits provider/model and omitted response options; document_access still defaults to read. Encrypted-output requests cannot be continued.
- `inputs` (array): At most one target, with exactly one file_id or artifact_id. Uploaded targets support DOCX, Markdown, HTML, and text. Direct artifact inputs require input_eligible=true and an unencrypted new request.. max items 1
- `role` (string, required): always "target"
- `file_id` (string)
- `artifact_id` (string)
- `export_document` (boolean): Export clean and tracked-changes DOCX artifacts for read/comment requests. edit always exports. Requires a target.
- `limits` (Limits): Optional execution and spending thresholds. Spending limits are opt-in: omitted cost limits are unlimited. The time limit defaults to 900 seconds. Prompt platform cost is base fee plus tool CPU; conversion platform cost is its flat/page fee. Prompt cost limits may each be at most $500,000; conversion cost limits may each be at most $1,000,000. These thresholds do not reserve account credit.. example {"max_platform_cost_usd":"2.000000000","max_inference_cost_usd":"3.000000000","max_seconds":900}
- `max_platform_cost_usd` (string | number): Platform spending threshold. Must cover the applicable base/conversion fee, or one scan page. A scan checks its full page fee before inference. For prompts, no new model call starts once observed platform spend reaches this threshold; active work may exceed it. Omit for no per-request spending limit. Must be greater than zero when supplied.
- `max_inference_cost_usd` (string | number): Soft inference threshold, including managed prompt premium and search. Active responses can exceed it. With your own provider key it meters provider list-price spend, while Revise charges no inference fee. For managed scans, scan inference is included in the page fee but remains subject to this threshold. Omit for no per-request spending limit. Must be greater than zero when supplied.
- `max_seconds` (integer): Work budget in seconds, starting when a worker begins, excluding queue time. Prompts stop new turns at the deadline; active responses and exports can finish later.. min 120. max 3600
- `inference` (InferenceRequest)
- `provider` (string): Public provider ID. Omit for the API default on a new conversation, or inherit from previous_prompt_id.. one of "openai", "anthropic", "xai", "gemini"
- `model` (string): Exact model ID from /v1/models, or default. Omitting it selects the provider default, except when inherited from a predecessor.
- `billing` (string): Which party pays for inference. Omitted or "revise" is automatic: the account's stored key for the resolved provider is used when present (billed by the provider, no Revise inference charge), otherwise managed inference applies. "provider" requires a stored key for the resolved provider and fails admission with provider_key_required instead of falling back. Provider resolution includes defaults and predecessor inheritance, so an omitted provider still uses the account key.. one of "revise", "provider"
- `metadata` (Metadata): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `output_encryption` (OutputEncryption): Encrypt exported artifact bytes using compact JWE, RSA-OAEP-256 and A256GCM. Only encrypted files are downloadable. Disables prompt continuation and artifact reuse; cannot be combined with previous_prompt_id or artifact_id input. Prompts require editing or export_document=true.
- `format` (string, required): one of "jwe"
- `public_key_pem` (string, required): RSA SubjectPublicKeyInfo PEM (BEGIN PUBLIC KEY), 2048–8192 bits. Private keys, certificates and key URLs are rejected.. max length 16384
- `response_options` (ResponseOptions): Persisted response policy. False omits the corresponding top-level JSON field from submission, polling, history and replay responses; it does not change the document or native state. Omitted options inherit from an unencrypted predecessor or reused artifact, otherwise default to true.
- `include_comments` (boolean)
- `include_changes` (boolean)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
## Responses
### 202 `Prompt`
Admission receipt, including on idempotent replay. May already be running or terminal. Save the ID and poll for the result.
- Header `Location`: Account-authenticated path to retrieve this resource.
- Header `Idempotent-Replayed`: Present with value true on an idempotent replay.
- Header `Retry-After`: Suggested delay before polling, in seconds.
- `id` (string, required)
- `conversation_id` (string, required)
- `previous_prompt_id` (string | null, required)
- `status` (string, required): one of "queued", "running", "paused", "succeeded", "failed", "cancelled"
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `conversation_expires_at` (string | null, required)
- `message` (Message | null, required)
- `continuation` (Continuation, required)
- `reasoning_state` (string, required): one of "new", "reused", "reset", "compacted", "unavailable"
- `history_compacted` (boolean | null, required)
- `reason` (string)
- `comments` (array): Flat native comment records; replies use parentId. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `author` (string, required)
- `initials` (string)
- `authorId` (string)
- `authorImageUrl` (string)
- `createdAt` (string, required)
- `bodyMd` (string, required): Comment text in Markdown.
- `parentId` (string): Parent comment ID for a reply; absent for a root comment.
- `agentRequestId` (string)
- `agentConversationId` (string)
- `agentName` (string)
- `agentModel` (string)
- `suggestionId` (string)
- `anchorBlockId` (string)
- `agentTriggerIds` (array)
- `relatedSuggestionIds` (array)
- `resolved` (boolean)
- `authorType` (string): one of "human", "ai"
- `importedId` (integer)
- `citations` (array)
- `id` (string, required)
- `url` (string, required)
- `title` (string, required)
- `startIndex` (integer)
- `endIndex` (integer)
- `mentions` (array)
- `id` (string, required)
- `type` (string, required): one of "user", "agent"
- `label` (string, required)
- `start` (integer, required)
- `end` (integer, required)
- `email` (string)
- `changes` (array): Pending native tracked changes. Replacements can appear as linked insertion/deletion entries. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `kind` (string, required): one of "insert", "delete", "format", "move"
- `blockIds` (array, required)
- `commentThreadId` (string)
- `author` (string, required)
- `agentModel` (string)
- `description` (string)
- `insertedText` (string)
- `deletedText` (string)
- `authorType` (string, required): one of "human", "ai"
- `createdAt` (number): Unix time in milliseconds, when recorded.
- `moveSourceBlockIds` (array)
- `moveDestinationBlockIds` (array)
- `artifacts` (array, required)
- `id` (string, required)
- `prompt_id` (string)
- `conversion_id` (string)
- `format` (string, required): one of "docx", "pdf", "md", "html", "txt"
- `filename` (string, required): Original plaintext filename, even when encrypted. Encrypted downloads append .jwe.
- `content_type` (string, required): Original plaintext MIME type. Encrypted downloads use application/jose.
- `variant` (string, required): one of "tracked_changes", "clean", "converted"
- `sha256` (string, required): SHA-256 of the exact downloadable bytes (ciphertext for encrypted artifacts).
- `bytes` (integer, required): Size of the downloadable bytes, including JWE encoding overhead when encrypted.
- `download_url` (string, required): Account-authenticated API path. The endpoint may redirect to signed storage; never forward bearer credentials to storage.
- `input_eligible` (boolean, required): True only for an unexpired, undeleted, unencrypted prompt artifact. Conversion artifacts are not directly reusable; download and upload them first.
- `expires_at` (string | null, required)
- `encryption` (ArtifactEncryption)
- `format` (string, required): one of "jwe"
- `alg` (string, required): one of "RSA-OAEP-256"
- `enc` (string, required): one of "A256GCM"
- `key_id` (string, required): Base64url SHA-256 JWK thumbprint of the recipient public key (RFC7638).
- `deleted_at` (string | null, required)
- `usage` (Usage | null, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `inference` (object, required)
- `provider` (string, required): one of "openai", "anthropic", "xai", "gemini"
- `model` (string, required)
- `billing` (string, required): The billing that applied at admission; fixed for the life of the request even if account keys change later.. one of "revise", "provider"
- `limits` (object, required)
- `max_platform_cost_usd` (string): example "0.050000000"
- `max_inference_cost_usd` (string): example "0.050000000"
- `max_seconds` (integer, required)
- `error` (object | null, required)
- `api_key_id` (string | null, required): Non-secret ID of the key that originally submitted this prompt; null when not recorded. Idempotent retries preserve the original key.
- `output_encrypted` (boolean, required): True disables continuation and reuse. Artifacts are encrypted for the supplied public key.
- `content_expires_at` (string | null, required): 24 hours after terminal completion. Result content and new continuation become unavailable at this deadline.
- `content_deleted_at` (string | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
- `incomplete` (boolean, required): True when the request returned a partial result.
- `stop_reason` (string): Limit that ended further agent work. The current result is returned; successful partial results incur the base fee even without inference, plus actual billable usage.. one of "spending_limit", "time_limit"
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/create-prompt
---
# Get a prompt
> Read status, receipt, and unexpired result.
See [prompt workflows](https://revise.io/developer/docs/revise-api/prompts) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.prompts.get("prompt_id");
```
`GET https://revise.io/api/v1/prompt/{id}`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 200 `Prompt`
Current status and receipt. Retained result content is omitted after expiry or deletion; settled usage remains.
- `id` (string, required)
- `conversation_id` (string, required)
- `previous_prompt_id` (string | null, required)
- `status` (string, required): one of "queued", "running", "paused", "succeeded", "failed", "cancelled"
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `conversation_expires_at` (string | null, required)
- `message` (Message | null, required)
- `continuation` (Continuation, required)
- `reasoning_state` (string, required): one of "new", "reused", "reset", "compacted", "unavailable"
- `history_compacted` (boolean | null, required)
- `reason` (string)
- `comments` (array): Flat native comment records; replies use parentId. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `author` (string, required)
- `initials` (string)
- `authorId` (string)
- `authorImageUrl` (string)
- `createdAt` (string, required)
- `bodyMd` (string, required): Comment text in Markdown.
- `parentId` (string): Parent comment ID for a reply; absent for a root comment.
- `agentRequestId` (string)
- `agentConversationId` (string)
- `agentName` (string)
- `agentModel` (string)
- `suggestionId` (string)
- `anchorBlockId` (string)
- `agentTriggerIds` (array)
- `relatedSuggestionIds` (array)
- `resolved` (boolean)
- `authorType` (string): one of "human", "ai"
- `importedId` (integer)
- `citations` (array)
- `id` (string, required)
- `url` (string, required)
- `title` (string, required)
- `startIndex` (integer)
- `endIndex` (integer)
- `mentions` (array)
- `id` (string, required)
- `type` (string, required): one of "user", "agent"
- `label` (string, required)
- `start` (integer, required)
- `end` (integer, required)
- `email` (string)
- `changes` (array): Pending native tracked changes. Replacements can appear as linked insertion/deletion entries. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `kind` (string, required): one of "insert", "delete", "format", "move"
- `blockIds` (array, required)
- `commentThreadId` (string)
- `author` (string, required)
- `agentModel` (string)
- `description` (string)
- `insertedText` (string)
- `deletedText` (string)
- `authorType` (string, required): one of "human", "ai"
- `createdAt` (number): Unix time in milliseconds, when recorded.
- `moveSourceBlockIds` (array)
- `moveDestinationBlockIds` (array)
- `artifacts` (array, required)
- `id` (string, required)
- `prompt_id` (string)
- `conversion_id` (string)
- `format` (string, required): one of "docx", "pdf", "md", "html", "txt"
- `filename` (string, required): Original plaintext filename, even when encrypted. Encrypted downloads append .jwe.
- `content_type` (string, required): Original plaintext MIME type. Encrypted downloads use application/jose.
- `variant` (string, required): one of "tracked_changes", "clean", "converted"
- `sha256` (string, required): SHA-256 of the exact downloadable bytes (ciphertext for encrypted artifacts).
- `bytes` (integer, required): Size of the downloadable bytes, including JWE encoding overhead when encrypted.
- `download_url` (string, required): Account-authenticated API path. The endpoint may redirect to signed storage; never forward bearer credentials to storage.
- `input_eligible` (boolean, required): True only for an unexpired, undeleted, unencrypted prompt artifact. Conversion artifacts are not directly reusable; download and upload them first.
- `expires_at` (string | null, required)
- `encryption` (ArtifactEncryption)
- `format` (string, required): one of "jwe"
- `alg` (string, required): one of "RSA-OAEP-256"
- `enc` (string, required): one of "A256GCM"
- `key_id` (string, required): Base64url SHA-256 JWK thumbprint of the recipient public key (RFC7638).
- `deleted_at` (string | null, required)
- `usage` (Usage | null, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `inference` (object, required)
- `provider` (string, required): one of "openai", "anthropic", "xai", "gemini"
- `model` (string, required)
- `billing` (string, required): The billing that applied at admission; fixed for the life of the request even if account keys change later.. one of "revise", "provider"
- `limits` (object, required)
- `max_platform_cost_usd` (string): example "0.050000000"
- `max_inference_cost_usd` (string): example "0.050000000"
- `max_seconds` (integer, required)
- `error` (object | null, required)
- `api_key_id` (string | null, required): Non-secret ID of the key that originally submitted this prompt; null when not recorded. Idempotent retries preserve the original key.
- `output_encrypted` (boolean, required): True disables continuation and reuse. Artifacts are encrypted for the supplied public key.
- `content_expires_at` (string | null, required): 24 hours after terminal completion. Result content and new continuation become unavailable at this deadline.
- `content_deleted_at` (string | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
- `incomplete` (boolean, required): True when the request returned a partial result.
- `stop_reason` (string): Limit that ended further agent work. The current result is returned; successful partial results incur the base fee even without inference, plus actual billable usage.. one of "spending_limit", "time_limit"
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-prompt
---
# Cancel a prompt
> Send no body.
See [prompt workflows](https://revise.io/developer/docs/revise-api/prompts) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.prompts.cancel("prompt_id");
```
`POST https://revise.io/api/v1/prompt/{id}/cancel`
Send no body. Queued work stops immediately; running work may still be stopping when the response returns. Poll until terminal. Completed jobs remain completed. Cancellation can incur charges for work already performed.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 202 `Prompt`
Cancel work; no request body
- `id` (string, required)
- `conversation_id` (string, required)
- `previous_prompt_id` (string | null, required)
- `status` (string, required): one of "queued", "running", "paused", "succeeded", "failed", "cancelled"
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `conversation_expires_at` (string | null, required)
- `message` (Message | null, required)
- `continuation` (Continuation, required)
- `reasoning_state` (string, required): one of "new", "reused", "reset", "compacted", "unavailable"
- `history_compacted` (boolean | null, required)
- `reason` (string)
- `comments` (array): Flat native comment records; replies use parentId. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `author` (string, required)
- `initials` (string)
- `authorId` (string)
- `authorImageUrl` (string)
- `createdAt` (string, required)
- `bodyMd` (string, required): Comment text in Markdown.
- `parentId` (string): Parent comment ID for a reply; absent for a root comment.
- `agentRequestId` (string)
- `agentConversationId` (string)
- `agentName` (string)
- `agentModel` (string)
- `suggestionId` (string)
- `anchorBlockId` (string)
- `agentTriggerIds` (array)
- `relatedSuggestionIds` (array)
- `resolved` (boolean)
- `authorType` (string): one of "human", "ai"
- `importedId` (integer)
- `citations` (array)
- `id` (string, required)
- `url` (string, required)
- `title` (string, required)
- `startIndex` (integer)
- `endIndex` (integer)
- `mentions` (array)
- `id` (string, required)
- `type` (string, required): one of "user", "agent"
- `label` (string, required)
- `start` (integer, required)
- `end` (integer, required)
- `email` (string)
- `changes` (array): Pending native tracked changes. Replacements can appear as linked insertion/deletion entries. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `kind` (string, required): one of "insert", "delete", "format", "move"
- `blockIds` (array, required)
- `commentThreadId` (string)
- `author` (string, required)
- `agentModel` (string)
- `description` (string)
- `insertedText` (string)
- `deletedText` (string)
- `authorType` (string, required): one of "human", "ai"
- `createdAt` (number): Unix time in milliseconds, when recorded.
- `moveSourceBlockIds` (array)
- `moveDestinationBlockIds` (array)
- `artifacts` (array, required)
- `id` (string, required)
- `prompt_id` (string)
- `conversion_id` (string)
- `format` (string, required): one of "docx", "pdf", "md", "html", "txt"
- `filename` (string, required): Original plaintext filename, even when encrypted. Encrypted downloads append .jwe.
- `content_type` (string, required): Original plaintext MIME type. Encrypted downloads use application/jose.
- `variant` (string, required): one of "tracked_changes", "clean", "converted"
- `sha256` (string, required): SHA-256 of the exact downloadable bytes (ciphertext for encrypted artifacts).
- `bytes` (integer, required): Size of the downloadable bytes, including JWE encoding overhead when encrypted.
- `download_url` (string, required): Account-authenticated API path. The endpoint may redirect to signed storage; never forward bearer credentials to storage.
- `input_eligible` (boolean, required): True only for an unexpired, undeleted, unencrypted prompt artifact. Conversion artifacts are not directly reusable; download and upload them first.
- `expires_at` (string | null, required)
- `encryption` (ArtifactEncryption)
- `format` (string, required): one of "jwe"
- `alg` (string, required): one of "RSA-OAEP-256"
- `enc` (string, required): one of "A256GCM"
- `key_id` (string, required): Base64url SHA-256 JWK thumbprint of the recipient public key (RFC7638).
- `deleted_at` (string | null, required)
- `usage` (Usage | null, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `inference` (object, required)
- `provider` (string, required): one of "openai", "anthropic", "xai", "gemini"
- `model` (string, required)
- `billing` (string, required): The billing that applied at admission; fixed for the life of the request even if account keys change later.. one of "revise", "provider"
- `limits` (object, required)
- `max_platform_cost_usd` (string): example "0.050000000"
- `max_inference_cost_usd` (string): example "0.050000000"
- `max_seconds` (integer, required)
- `error` (object | null, required)
- `api_key_id` (string | null, required): Non-secret ID of the key that originally submitted this prompt; null when not recorded. Idempotent retries preserve the original key.
- `output_encrypted` (boolean, required): True disables continuation and reuse. Artifacts are encrypted for the supplied public key.
- `content_expires_at` (string | null, required): 24 hours after terminal completion. Result content and new continuation become unavailable at this deadline.
- `content_deleted_at` (string | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
- `incomplete` (boolean, required): True when the request returned a partial result.
- `stop_reason` (string): Limit that ended further agent work. The current result is returned; successful partial results incur the base fee even without inference, plus actual billable usage.. one of "spending_limit", "time_limit"
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/cancel-prompt
---
# Resume a prompt
> Resume a paused prompt with new absolute cost limits.
See [prompt workflows](https://revise.io/developer/docs/revise-api/prompts) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.prompts.resume("prompt_id", {
limits: { max_platform_cost_usd: "4.00", max_inference_cost_usd: "6.00" },
});
```
`POST https://revise.io/api/v1/prompt/{id}/resume`
Resume a paused prompt with new absolute cost limits. Omit a cost limit to remove that cap. Neither limit may decrease and at least one must increase or be removed; explicit limits must sum to at most $1,000,000. Pauses expire after 24 hours. Resumption keeps the job ID and does not incur another base fee. max_seconds is not accepted. An Idempotency-Key is not required; repeating previously applied limits replays that resume.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Body
- `limits` (object, required)
- `max_platform_cost_usd` (string | number): Must be greater than zero when supplied.
- `max_inference_cost_usd` (string | number): Must be greater than zero when supplied.
## Responses
### 202 `Prompt`
Raise absolute cost limits on a paused prompt
- `id` (string, required)
- `conversation_id` (string, required)
- `previous_prompt_id` (string | null, required)
- `status` (string, required): one of "queued", "running", "paused", "succeeded", "failed", "cancelled"
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `conversation_expires_at` (string | null, required)
- `message` (Message | null, required)
- `continuation` (Continuation, required)
- `reasoning_state` (string, required): one of "new", "reused", "reset", "compacted", "unavailable"
- `history_compacted` (boolean | null, required)
- `reason` (string)
- `comments` (array): Flat native comment records; replies use parentId. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `author` (string, required)
- `initials` (string)
- `authorId` (string)
- `authorImageUrl` (string)
- `createdAt` (string, required)
- `bodyMd` (string, required): Comment text in Markdown.
- `parentId` (string): Parent comment ID for a reply; absent for a root comment.
- `agentRequestId` (string)
- `agentConversationId` (string)
- `agentName` (string)
- `agentModel` (string)
- `suggestionId` (string)
- `anchorBlockId` (string)
- `agentTriggerIds` (array)
- `relatedSuggestionIds` (array)
- `resolved` (boolean)
- `authorType` (string): one of "human", "ai"
- `importedId` (integer)
- `citations` (array)
- `id` (string, required)
- `url` (string, required)
- `title` (string, required)
- `startIndex` (integer)
- `endIndex` (integer)
- `mentions` (array)
- `id` (string, required)
- `type` (string, required): one of "user", "agent"
- `label` (string, required)
- `start` (integer, required)
- `end` (integer, required)
- `email` (string)
- `changes` (array): Pending native tracked changes. Replacements can appear as linked insertion/deletion entries. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `kind` (string, required): one of "insert", "delete", "format", "move"
- `blockIds` (array, required)
- `commentThreadId` (string)
- `author` (string, required)
- `agentModel` (string)
- `description` (string)
- `insertedText` (string)
- `deletedText` (string)
- `authorType` (string, required): one of "human", "ai"
- `createdAt` (number): Unix time in milliseconds, when recorded.
- `moveSourceBlockIds` (array)
- `moveDestinationBlockIds` (array)
- `artifacts` (array, required)
- `id` (string, required)
- `prompt_id` (string)
- `conversion_id` (string)
- `format` (string, required): one of "docx", "pdf", "md", "html", "txt"
- `filename` (string, required): Original plaintext filename, even when encrypted. Encrypted downloads append .jwe.
- `content_type` (string, required): Original plaintext MIME type. Encrypted downloads use application/jose.
- `variant` (string, required): one of "tracked_changes", "clean", "converted"
- `sha256` (string, required): SHA-256 of the exact downloadable bytes (ciphertext for encrypted artifacts).
- `bytes` (integer, required): Size of the downloadable bytes, including JWE encoding overhead when encrypted.
- `download_url` (string, required): Account-authenticated API path. The endpoint may redirect to signed storage; never forward bearer credentials to storage.
- `input_eligible` (boolean, required): True only for an unexpired, undeleted, unencrypted prompt artifact. Conversion artifacts are not directly reusable; download and upload them first.
- `expires_at` (string | null, required)
- `encryption` (ArtifactEncryption)
- `format` (string, required): one of "jwe"
- `alg` (string, required): one of "RSA-OAEP-256"
- `enc` (string, required): one of "A256GCM"
- `key_id` (string, required): Base64url SHA-256 JWK thumbprint of the recipient public key (RFC7638).
- `deleted_at` (string | null, required)
- `usage` (Usage | null, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `inference` (object, required)
- `provider` (string, required): one of "openai", "anthropic", "xai", "gemini"
- `model` (string, required)
- `billing` (string, required): The billing that applied at admission; fixed for the life of the request even if account keys change later.. one of "revise", "provider"
- `limits` (object, required)
- `max_platform_cost_usd` (string): example "0.050000000"
- `max_inference_cost_usd` (string): example "0.050000000"
- `max_seconds` (integer, required)
- `error` (object | null, required)
- `api_key_id` (string | null, required): Non-secret ID of the key that originally submitted this prompt; null when not recorded. Idempotent retries preserve the original key.
- `output_encrypted` (boolean, required): True disables continuation and reuse. Artifacts are encrypted for the supplied public key.
- `content_expires_at` (string | null, required): 24 hours after terminal completion. Result content and new continuation become unavailable at this deadline.
- `content_deleted_at` (string | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
- `incomplete` (boolean, required): True when the request returned a partial result.
- `stop_reason` (string): Limit that ended further agent work. The current result is returned; successful partial results incur the base fee even without inference, plus actual billable usage.. one of "spending_limit", "time_limit"
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/resume-prompt
---
# List prompts
> Returns Prompt receipts newest first, ordered by created_at then ID.
See [prompt workflows](https://revise.io/developer/docs/revise-api/prompts) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.prompts.list({ limit: 20, metadata: { customer: "acme" } });
```
`GET https://revise.io/api/v1/prompts`
Returns Prompt receipts newest first, ordered by created_at then ID. Pass next_cursor with the same filters for the next page; null means the end. Cursors belong to the authenticated account. Filters combine with AND. Status is current, not a frozen snapshot. Metadata filters stop matching after content expiry or deletion. Unknown or repeated query parameters return 400.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Query parameters
- `limit` (integer): min 1. max 100
- `cursor` (string): The next_cursor returned by the previous page.
- `status` (string): one of "queued", "running", "paused", "succeeded", "failed", "cancelled"
- `conversation_id` (string): Exact conv_ ID.
- `api_key_id` (string): Exact key_ ID of the submitting key. Older unattributed prompts do not match.
- `created_after` (string): Inclusive lower creation-time bound (RFC3339 with timezone).
- `created_before` (string): Exclusive upper creation-time bound (RFC3339 with timezone).
- `metadata` (object): Exact AND matches, e.g. metadata[customer]=Acme. Keys up to 64 bytes; values up to 512 bytes.
## Responses
### 200 `object`
A page of prompts
- `data` (array, required): max items 100
- `id` (string, required)
- `conversation_id` (string, required)
- `previous_prompt_id` (string | null, required)
- `status` (string, required): one of "queued", "running", "paused", "succeeded", "failed", "cancelled"
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `conversation_expires_at` (string | null, required)
- `message` (Message | null, required)
- `continuation` (Continuation, required)
- `reasoning_state` (string, required): one of "new", "reused", "reset", "compacted", "unavailable"
- `history_compacted` (boolean | null, required)
- `reason` (string)
- `comments` (array): Flat native comment records; replies use parentId. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `author` (string, required)
- `initials` (string)
- `authorId` (string)
- `authorImageUrl` (string)
- `createdAt` (string, required)
- `bodyMd` (string, required): Comment text in Markdown.
- `parentId` (string): Parent comment ID for a reply; absent for a root comment.
- `agentRequestId` (string)
- `agentConversationId` (string)
- `agentName` (string)
- `agentModel` (string)
- `suggestionId` (string)
- `anchorBlockId` (string)
- `agentTriggerIds` (array)
- `relatedSuggestionIds` (array)
- `resolved` (boolean)
- `authorType` (string): one of "human", "ai"
- `importedId` (integer)
- `citations` (array)
- `id` (string, required)
- `url` (string, required)
- `title` (string, required)
- `startIndex` (integer)
- `endIndex` (integer)
- `mentions` (array)
- `id` (string, required)
- `type` (string, required): one of "user", "agent"
- `label` (string, required)
- `start` (integer, required)
- `end` (integer, required)
- `email` (string)
- `changes` (array): Pending native tracked changes. Replacements can appear as linked insertion/deletion entries. Omitted when disabled by response_options or when content is unavailable.
- `id` (string, required)
- `kind` (string, required): one of "insert", "delete", "format", "move"
- `blockIds` (array, required)
- `commentThreadId` (string)
- `author` (string, required)
- `agentModel` (string)
- `description` (string)
- `insertedText` (string)
- `deletedText` (string)
- `authorType` (string, required): one of "human", "ai"
- `createdAt` (number): Unix time in milliseconds, when recorded.
- `moveSourceBlockIds` (array)
- `moveDestinationBlockIds` (array)
- `artifacts` (array, required)
- `id` (string, required)
- `prompt_id` (string)
- `conversion_id` (string)
- `format` (string, required): one of "docx", "pdf", "md", "html", "txt"
- `filename` (string, required): Original plaintext filename, even when encrypted. Encrypted downloads append .jwe.
- `content_type` (string, required): Original plaintext MIME type. Encrypted downloads use application/jose.
- `variant` (string, required): one of "tracked_changes", "clean", "converted"
- `sha256` (string, required): SHA-256 of the exact downloadable bytes (ciphertext for encrypted artifacts).
- `bytes` (integer, required): Size of the downloadable bytes, including JWE encoding overhead when encrypted.
- `download_url` (string, required): Account-authenticated API path. The endpoint may redirect to signed storage; never forward bearer credentials to storage.
- `input_eligible` (boolean, required): True only for an unexpired, undeleted, unencrypted prompt artifact. Conversion artifacts are not directly reusable; download and upload them first.
- `expires_at` (string | null, required)
- `encryption` (ArtifactEncryption)
- `format` (string, required): one of "jwe"
- `alg` (string, required): one of "RSA-OAEP-256"
- `enc` (string, required): one of "A256GCM"
- `key_id` (string, required): Base64url SHA-256 JWK thumbprint of the recipient public key (RFC7638).
- `deleted_at` (string | null, required)
- `usage` (Usage | null, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `inference` (object, required)
- `provider` (string, required): one of "openai", "anthropic", "xai", "gemini"
- `model` (string, required)
- `billing` (string, required): The billing that applied at admission; fixed for the life of the request even if account keys change later.. one of "revise", "provider"
- `limits` (object, required)
- `max_platform_cost_usd` (string): example "0.050000000"
- `max_inference_cost_usd` (string): example "0.050000000"
- `max_seconds` (integer, required)
- `error` (object | null, required)
- `api_key_id` (string | null, required): Non-secret ID of the key that originally submitted this prompt; null when not recorded. Idempotent retries preserve the original key.
- `output_encrypted` (boolean, required): True disables continuation and reuse. Artifacts are encrypted for the supplied public key.
- `content_expires_at` (string | null, required): 24 hours after terminal completion. Result content and new continuation become unavailable at this deadline.
- `content_deleted_at` (string | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
- `incomplete` (boolean, required): True when the request returned a partial result.
- `stop_reason` (string): Limit that ended further agent work. The current result is returned; successful partial results incur the base fee even without inference, plus actual billable usage.. one of "spending_limit", "time_limit"
- `next_cursor` (string | null, required)
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/list-prompts
---
# Delete prompt content
> Send no body.
See [prompt workflows](https://revise.io/developer/docs/revise-api/prompts) for integration guidance.
Deletes retained content, not the billing receipt. This is separate from cancelling active work. See [retention](https://revise.io/developer/docs/revise-api/files#retention).
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
await revise.prompts.deleteContent("prompt_id");
```
`DELETE https://revise.io/api/v1/prompt/{id}/content`
Send no body. Delete retained prompt/result content, customer metadata, document state, artifacts, and the claimed ephemeral upload. Persistent source templates are excluded. Status and accounting receipts remain. Repeated deletion succeeds; active or pinned content returns 409.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 204
Deleted; status, receipt and idempotency tombstone remain
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/delete-prompt-content
---
# List conversions
> Newest first, ordered by creation time then ID.
See [conversion formats and limits](https://revise.io/developer/docs/revise-api/conversions) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.conversions.list({ limit: 20 });
```
`GET https://revise.io/api/v1/convert`
Newest first, ordered by creation time then ID. Pass next_cursor with the same status filter for the next page; null means the end. Only status, limit, and cursor are supported. Cursors belong to the authenticated account.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Query parameters
- `limit` (integer): min 1. max 100
- `cursor` (string)
- `status` (string): one of "queued", "running", "succeeded", "failed", "cancelled"
## Responses
### 200 `object`
Conversion page
- `data` (array, required)
- `id` (string, required)
- `object` (string, required): always "conversion"
- `status` (string, required): one of "queued", "running", "succeeded", "failed", "cancelled"
- `mode` (string, required): Empty after request content has been deleted.. one of "deterministic", "vision", ""
- `input` (object, required)
- `file_id` (string, required)
- `format` (string, required): Empty after request content has been deleted.. one of "pdf", "docx", "md", "html", "txt", "rtf", "odt", "image", ""
- `filename` (string, required)
- `bytes` (integer, required)
- `sha256` (string, required)
- `output` (object, required)
- `format` (string, required): Empty after request content has been deleted.. one of "docx", "pdf", "md", "html", "txt", ""
- `artifact` (Artifact | null, required)
- `output_encrypted` (boolean, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `usage` (Usage | null, required)
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `content_expires_at` (string | null, required)
- `content_deleted_at` (string | null, required)
- `error` (object | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
- `next_cursor` (string | null, required)
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/list-conversions
---
# Create a conversion
> Convert one uploaded file to docx, pdf, md, html, or txt.
See [conversion formats and limits](https://revise.io/developer/docs/revise-api/conversions) for integration guidance.
This call returns an admission receipt. Poll the get endpoint until work stops, then inspect the result and download its artifacts. See [job lifecycle](https://revise.io/developer/docs/revise-api/jobs).
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.conversions.create(
{ input: { file_id: "file_id" }, output: { format: "pdf" } },
{ idempotencyKey: "convert-42" },
);
```
`POST https://revise.io/api/v1/convert`
Convert one uploaded file to docx, pdf, md, html, or txt. Same-format conversion is rejected. DOCX, Markdown, HTML, text, RTF, and ODT use deterministic conversion at $0.005 each. PDF and image inputs use semantic scanning: $0.02 per page with managed inference on the default OpenAI model, or $0.005 per page plus provider inference when using your own OpenAI key. Only your own key permits another catalogued OpenAI model. Deterministic conversions reject nonempty inference settings.
Uploads are limited to 18 MiB, PDFs to 100 pages, and output to 50 MiB before encryption. There is no default spending cap; an explicitly set insufficient page budget fails with conversion_limit_exceeded before inference. PDF processing errors include pdf_page_limit_exceeded, pdf_password_protected, invalid_pdf, and empty_pdf. These validation failures are unbilled. Cancellation or customer limits can incur charges for work already performed; read settled usage. Conversions have no additional CPU or Revise inference fee and do not count toward prompt base-fee tiers.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Headers
- `Idempotency-Key` (string, required): min length 1. max length 200
## Body
- `input` (object, required)
- `file_id` (string, required)
- `output` (object, required)
- `format` (string, required): one of "docx", "pdf", "md", "html", "txt"
- `limits` (Limits): Optional execution and spending thresholds. Spending limits are opt-in: omitted cost limits are unlimited. The time limit defaults to 900 seconds. Prompt platform cost is base fee plus tool CPU; conversion platform cost is its flat/page fee. Prompt cost limits may each be at most $500,000; conversion cost limits may each be at most $1,000,000. These thresholds do not reserve account credit.. example {"max_platform_cost_usd":"2.000000000","max_inference_cost_usd":"3.000000000","max_seconds":900}
- `max_platform_cost_usd` (string | number): Platform spending threshold. Must cover the applicable base/conversion fee, or one scan page. A scan checks its full page fee before inference. For prompts, no new model call starts once observed platform spend reaches this threshold; active work may exceed it. Omit for no per-request spending limit. Must be greater than zero when supplied.
- `max_inference_cost_usd` (string | number): Soft inference threshold, including managed prompt premium and search. Active responses can exceed it. With your own provider key it meters provider list-price spend, while Revise charges no inference fee. For managed scans, scan inference is included in the page fee but remains subject to this threshold. Omit for no per-request spending limit. Must be greater than zero when supplied.
- `max_seconds` (integer): Work budget in seconds, starting when a worker begins, excluding queue time. Prompts stop new turns at the deadline; active responses and exports can finish later.. min 120. max 3600
- `inference` (InferenceRequest)
- `provider` (string): Public provider ID. Omit for the API default on a new conversation, or inherit from previous_prompt_id.. one of "openai", "anthropic", "xai", "gemini"
- `model` (string): Exact model ID from /v1/models, or default. Omitting it selects the provider default, except when inherited from a predecessor.
- `billing` (string): Which party pays for inference. Omitted or "revise" is automatic: the account's stored key for the resolved provider is used when present (billed by the provider, no Revise inference charge), otherwise managed inference applies. "provider" requires a stored key for the resolved provider and fails admission with provider_key_required instead of falling back. Provider resolution includes defaults and predecessor inheritance, so an omitted provider still uses the account key.. one of "revise", "provider"
- `metadata` (Metadata): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `output_encryption` (OutputEncryption): Encrypt exported artifact bytes using compact JWE, RSA-OAEP-256 and A256GCM. Only encrypted files are downloadable. Disables prompt continuation and artifact reuse; cannot be combined with previous_prompt_id or artifact_id input. Prompts require editing or export_document=true.
- `format` (string, required): one of "jwe"
- `public_key_pem` (string, required): RSA SubjectPublicKeyInfo PEM (BEGIN PUBLIC KEY), 2048–8192 bits. Private keys, certificates and key URLs are rejected.. max length 16384
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
## Responses
### 202 `Conversion`
Queued conversion
- Header `Location`: Account-authenticated path to retrieve this resource.
- Header `Idempotent-Replayed`: Present with value true on an idempotent replay.
- Header `Retry-After`: Suggested delay before polling, in seconds.
- `id` (string, required)
- `object` (string, required): always "conversion"
- `status` (string, required): one of "queued", "running", "succeeded", "failed", "cancelled"
- `mode` (string, required): Empty after request content has been deleted.. one of "deterministic", "vision", ""
- `input` (object, required)
- `file_id` (string, required)
- `format` (string, required): Empty after request content has been deleted.. one of "pdf", "docx", "md", "html", "txt", "rtf", "odt", "image", ""
- `filename` (string, required)
- `bytes` (integer, required)
- `sha256` (string, required)
- `output` (object, required)
- `format` (string, required): Empty after request content has been deleted.. one of "docx", "pdf", "md", "html", "txt", ""
- `artifact` (Artifact | null, required)
- `output_encrypted` (boolean, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `usage` (Usage | null, required)
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `content_expires_at` (string | null, required)
- `content_deleted_at` (string | null, required)
- `error` (object | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/create-conversion
---
# Get a conversion
> Read conversion status, receipt, and output artifact.
See [conversion formats and limits](https://revise.io/developer/docs/revise-api/conversions) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.conversions.get("conversion_id");
```
`GET https://revise.io/api/v1/convert/{id}`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 200 `Conversion`
Conversion
- `id` (string, required)
- `object` (string, required): always "conversion"
- `status` (string, required): one of "queued", "running", "succeeded", "failed", "cancelled"
- `mode` (string, required): Empty after request content has been deleted.. one of "deterministic", "vision", ""
- `input` (object, required)
- `file_id` (string, required)
- `format` (string, required): Empty after request content has been deleted.. one of "pdf", "docx", "md", "html", "txt", "rtf", "odt", "image", ""
- `filename` (string, required)
- `bytes` (integer, required)
- `sha256` (string, required)
- `output` (object, required)
- `format` (string, required): Empty after request content has been deleted.. one of "docx", "pdf", "md", "html", "txt", ""
- `artifact` (Artifact | null, required)
- `output_encrypted` (boolean, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `usage` (Usage | null, required)
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `content_expires_at` (string | null, required)
- `content_deleted_at` (string | null, required)
- `error` (object | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-conversion
---
# Cancel a conversion
> Send no body.
See [conversion formats and limits](https://revise.io/developer/docs/revise-api/conversions) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.conversions.cancel("conversion_id");
```
`POST https://revise.io/api/v1/convert/{id}/cancel`
Send no body. Queued work stops immediately; running work may still be stopping when the response returns. Poll until terminal. Completed jobs remain completed. Cancellation can incur charges for work already performed.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 202 `Conversion`
Cancellation requested
- `id` (string, required)
- `object` (string, required): always "conversion"
- `status` (string, required): one of "queued", "running", "succeeded", "failed", "cancelled"
- `mode` (string, required): Empty after request content has been deleted.. one of "deterministic", "vision", ""
- `input` (object, required)
- `file_id` (string, required)
- `format` (string, required): Empty after request content has been deleted.. one of "pdf", "docx", "md", "html", "txt", "rtf", "odt", "image", ""
- `filename` (string, required)
- `bytes` (integer, required)
- `sha256` (string, required)
- `output` (object, required)
- `format` (string, required): Empty after request content has been deleted.. one of "docx", "pdf", "md", "html", "txt", ""
- `artifact` (Artifact | null, required)
- `output_encrypted` (boolean, required)
- `metadata` (Metadata, required): Up to 16 string values for application attribution. Keys are 1–64 UTF-8 bytes; values at most 512 UTF-8 bytes. Metadata is deleted with request content; persist your own job mapping for long-term attribution.. example {"customer":"acme","environment":"production"}
- `usage` (Usage | null, required)
- `created_at` (string, required)
- `completed_at` (string | null, required)
- `content_expires_at` (string | null, required)
- `content_deleted_at` (string | null, required)
- `error` (object | null, required)
- `retention` (RetentionOptions)
- `delete_after_webhook_id` (string, required): Account-owned endpoint subscribed to prompt.completed or conversion.completed, matching the request kind. Its successful acknowledgement deletes request content and ephemeral source. Download everything before returning 2xx.
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/cancel-conversion
---
# Delete conversion content
> Send no body.
See [conversion formats and limits](https://revise.io/developer/docs/revise-api/conversions) for integration guidance.
Deletes retained content, not the billing receipt. This is separate from cancelling active work. See [retention](https://revise.io/developer/docs/revise-api/files#retention).
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
await revise.conversions.deleteContent("conversion_id");
```
`DELETE https://revise.io/api/v1/convert/{id}/content`
Send no body. Delete retained conversion content, customer metadata, artifact bytes, and the claimed ephemeral upload. Persistent source templates are excluded. Status and accounting receipts remain. Repeated deletion succeeds; active or pinned content returns 409.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 204
Deleted; status, receipt and idempotency tombstone remain
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/delete-conversion-content
---
# Upload a file
> Upload one nonempty file with a supported extension: DOCX, MD/Markdown, HTML/HTM, TXT, RTF, ODT, PDF, JPEG/JPG, PNG, WebP, or GIF.
See [files and retention](https://revise.io/developer/docs/revise-api/files) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.files.upload(
{ filename: "report.md", data: new TextEncoder().encode("# Quarterly report") },
{ idempotencyKey: "upload-42" },
);
```
`POST https://revise.io/api/v1/files`
Upload one nonempty file with a supported extension: DOCX, MD/Markdown, HTML/HTM, TXT, RTF, ODT, PDF, JPEG/JPG, PNG, WebP, or GIF. Text, Markdown, and HTML must be UTF-8. Prompt targets accept only DOCX, Markdown, HTML, and text; other formats must be converted first. There is no public upload-list or original-file-download endpoint.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Headers
- `Idempotency-Key` (string, required): min length 1. max length 200
## Body
- `file` (string, required): One nonempty file, at most 18 MiB. Accounts may retain up to 100 undeleted uploads totaling 256 MiB. Filename: at most 255 bytes, with no path separators or control characters. DOCX/ODT expanded archives are limited to 128 MiB; DOCX archives may contain at most 10,000 entries.
- `lifetime` (string): Ephemeral files can be claimed by one request and are deleted immediately on success. Failed or cancelled requests retain their source for 24 hours; unused uploads expire 24 hours after upload. Persistent templates remain until explicit deletion.. one of "ephemeral", "persistent"
## Responses
### 200 `File`
Idempotent upload replay
- Header `Location`: Account-authenticated path to retrieve this resource.
- Header `Idempotent-Replayed`: Present with value true on an idempotent replay.
- `id` (string, required)
- `filename` (string, required): Empty once expired or deleted.
- `bytes` (integer, required)
- `sha256` (string, required)
- `status` (string, required): one of "uploaded", "expired", "deleted"
- `created_at` (string, required)
- `expires_at` (string | null, required)
- `deleted_at` (string | null, required)
- `lifetime` (string, required): one of "ephemeral", "persistent"
### 201 `File`
Uploaded file metadata. Store its ID for job creation or later deletion.
- Header `Location`: Account-authenticated path to retrieve this resource.
- Header `Idempotent-Replayed`: Present with value true on an idempotent replay.
- `id` (string, required)
- `filename` (string, required): Empty once expired or deleted.
- `bytes` (integer, required)
- `sha256` (string, required)
- `status` (string, required): one of "uploaded", "expired", "deleted"
- `created_at` (string, required)
- `expires_at` (string | null, required)
- `deleted_at` (string | null, required)
- `lifetime` (string, required): one of "ephemeral", "persistent"
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/upload-file
---
# Get file metadata
> Read uploaded source metadata.
See [files and retention](https://revise.io/developer/docs/revise-api/files) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.files.get("file_id");
```
`GET https://revise.io/api/v1/files/{id}`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 200 `File`
Read uploaded source metadata
- `id` (string, required)
- `filename` (string, required): Empty once expired or deleted.
- `bytes` (integer, required)
- `sha256` (string, required)
- `status` (string, required): one of "uploaded", "expired", "deleted"
- `created_at` (string, required)
- `expires_at` (string | null, required)
- `deleted_at` (string | null, required)
- `lifetime` (string, required): one of "ephemeral", "persistent"
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-file
---
# Delete a file
> Delete an uploaded file; rejects active use with 409.
See [files and retention](https://revise.io/developer/docs/revise-api/files) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
await revise.files.delete("file_id");
```
`DELETE https://revise.io/api/v1/files/{id}`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 204
Deleted; tombstone retained for idempotency
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/delete-file
---
# Get artifact metadata
> Read metadata for an immutable output variant.
See [downloads and retention](https://revise.io/developer/docs/revise-api/files) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.artifacts.get("art_id");
```
`GET https://revise.io/api/v1/artifacts/{id}`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 200 `Artifact`
Read metadata for an immutable output variant
- `id` (string, required)
- `prompt_id` (string)
- `conversion_id` (string)
- `format` (string, required): one of "docx", "pdf", "md", "html", "txt"
- `filename` (string, required): Original plaintext filename, even when encrypted. Encrypted downloads append .jwe.
- `content_type` (string, required): Original plaintext MIME type. Encrypted downloads use application/jose.
- `variant` (string, required): one of "tracked_changes", "clean", "converted"
- `sha256` (string, required): SHA-256 of the exact downloadable bytes (ciphertext for encrypted artifacts).
- `bytes` (integer, required): Size of the downloadable bytes, including JWE encoding overhead when encrypted.
- `download_url` (string, required): Account-authenticated API path. The endpoint may redirect to signed storage; never forward bearer credentials to storage.
- `input_eligible` (boolean, required): True only for an unexpired, undeleted, unencrypted prompt artifact. Conversion artifacts are not directly reusable; download and upload them first.
- `expires_at` (string | null, required)
- `encryption` (ArtifactEncryption)
- `format` (string, required): one of "jwe"
- `alg` (string, required): one of "RSA-OAEP-256"
- `enc` (string, required): one of "A256GCM"
- `key_id` (string, required): Base64url SHA-256 JWK thumbprint of the recipient public key (RFC7638).
- `deleted_at` (string | null, required)
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-artifact
---
# Download artifact content
> Returns artifact bytes directly or a 302 redirect to a signed storage URL valid for five minutes.
See [downloads and retention](https://revise.io/developer/docs/revise-api/files) for integration guidance.
This TypeScript example fetches metadata, follows a signed download redirect when present, and verifies byte length and SHA-256. For a raw Response use `revise.artifacts.content(id)`. Encrypted content is compact JWE, with `application/jose` MIME and a `.jwe` filename.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.artifacts.download("art_id");
```
`GET https://revise.io/api/v1/artifacts/{id}/content`
Returns artifact bytes directly or a 302 redirect to a signed storage URL valid for five minutes. Follow the redirect without forwarding Authorization across origins; with curl use --location, never --location-trusted. Verify length and SHA-256 against artifact metadata. Deleted or expired content returns 410. Encrypted downloads use application/jose and a .jwe filename.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 200 `string`
The exact output bytes, or compact JWE ciphertext when output encryption was requested.
### 302
Redirect to a short-lived signed artifact download URL. Follow without forwarding bearer credentials.
- Header `Location`
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/download-artifact
---
# Get account
> Read the account credit balance.
See [pricing and account credit](https://revise.io/developer/docs/revise-api/pricing) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.account.get();
```
`GET https://revise.io/api/v1/account`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Responses
### 200 `Account`
The signed credit balance and funding capabilities.
- `id` (string, required)
- `currency` (string, required): always "usd"
- `available_usd` (string, required): Signed fixed-decimal USD credit balance. Requests are admitted only while the balance is positive; the final settled charge of an in-flight request may take it below zero until the next top-up. Spending thresholds stop new calls; the last response and its tools can take the final charge beyond them.. example "0.050000000"
- `pricing_version` (string, required)
- `funding_enabled` (boolean, required)
- `stripe_test_mode` (boolean, required)
- `api_key_id` (string, required): Non-secret ID of the currently authenticated key.
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-account
---
# Get account ledger
> Latest 50 entries with no pagination.
See [pricing and account credit](https://revise.io/developer/docs/revise-api/pricing) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.account.ledger();
```
`GET https://revise.io/api/v1/account/ledger`
Latest 50 entries with no pagination. available_delta_usd is the signed net change in account credit. Prompt and conversion entries expose the corresponding job ID; funding entries have neither. Use /v1/usage for historical charge aggregation.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Responses
### 200 `Ledger`
Read the latest 50 credit entries
- `data` (array, required)
- `id` (integer, required)
- `kind` (string, required)
- `prompt_id` (string | null, required): Prompt linked to this entry, or null. Follow with GET /v1/prompt/{id}; conversion_id is null for prompt entries. Both IDs are null for funding entries.
- `conversion_id` (string | null, required): Conversion linked to this entry, or null. Follow with GET /v1/convert/{id}; prompt_id is null for conversion entries.
- `available_delta_usd` (string, required): Signed net change to the credit balance for this entry. Historical reservation holds and their releases net to zero.. example "0.050000000"
- `created_at` (string, required)
- `limit` (number, required): always 50
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-ledger
---
# Get a top-up
> Read an existing funding order.
See [pricing and account credit](https://revise.io/developer/docs/revise-api/pricing) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.account.topup("topup_id");
```
`GET https://revise.io/api/v1/account/topups/{id}`
Read an existing funding order. Amounts are integer cents. Create top-ups and manage payment settings in the API console. Returns 503 funding_unavailable when funding is not enabled.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 200 `FundingOrder`
Read funding status without payment secrets
- `id` (string, required)
- `amount_cents` (integer, required)
- `automatic` (boolean, required)
- `status` (string, required): one of "pending", "succeeded", "failed", "needs_reconciliation", "requires_action"
- `created_at` (string, required)
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-topup
---
# List models
> Return supported prompt model IDs and defaults without consuming inference or credit.
See [model selection and provider keys](https://revise.io/developer/docs/revise-api/models) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.models.list();
```
`GET https://revise.io/api/v1/models`
Return supported prompt model IDs and defaults without consuming inference or credit. Pass provider and id as inference.provider and inference.model. The complete list is sorted by provider then model ID. Optional provider filtering uses gemini for Google models. default is always the API-wide default; default_for_provider marks each provider default. This catalog does not report prices, live provider health, credentials, or capacity. Managed PDF/image scanning accepts only the default OpenAI model; other catalogued OpenAI scan models require your own key.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Query parameters
- `provider` (string): Optional exact public provider.. one of "openai", "anthropic", "xai", "gemini"
## Responses
### 200 `ModelList`
Model catalog
- `data` (array, required)
- `id` (string, required): example "gpt-6-luna"
- `provider` (string, required): one of "openai", "anthropic", "xai", "gemini"
- `name` (string, required): example "GPT-6 Luna"
- `default_for_provider` (boolean, required): example true
- `default` (object, required)
- `provider` (string, required): one of "openai", "anthropic", "xai", "gemini"
- `model` (string, required)
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/list-models
---
# Get usage
> Aggregate settled prompts and conversions by completion time.
See [usage reports](https://revise.io/developer/docs/revise-api/pricing#usage-reports) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.usage.get({ start: "2026-09-01T00:00:00Z", end: "2026-10-01T00:00:00Z" });
```
`GET https://revise.io/api/v1/usage`
Aggregate settled prompts and conversions by completion time. Queued, running, and paused work is excluded. Totals reconcile to historical receipts, not current rates. Filters combine with AND; rows sort by UTC bucket and dimensions. The default window is the last 30 days.
Reports return at most 1,000 rows without pagination. Larger results return 422; narrow the range or use fewer groups/coarser buckets. Queries time out after five seconds with 503. Customer metadata expires with job content: metadata filters then stop matching and metadata dimensions become null, while account totals remain. Store job-to-customer mappings locally for long-term attribution. Model/provider dimensions describe admitted configuration, including deterministic conversions, rather than proving inference occurred. Unknown, empty, or repeated parameters return 400; empty metadata filter values are allowed.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Query parameters
- `start` (string): Inclusive completion time. Defaults to 30 days before end. RFC3339 timezone required.
- `end` (string): Exclusive completion time. Defaults to now. Range must be positive and at most 366 days; hourly ranges at most 31 days.
- `bucket` (string): UTC calendar buckets, or no time bucketing. Empty time buckets are omitted.. one of "hour", "day", "month", "none"
- `group_by` (string): Comma-separated distinct dimensions, maximum three: api_key_id, provider, model, status, document_access, pricing_version, metadata.KEY (KEY 1–64 bytes). Missing values form null groups. Example: api_key_id,metadata.customer.
- `status` (string): one of "succeeded", "failed", "cancelled"
- `conversation_id` (string): Exact conv_ ID.
- `api_key_id` (string): Exact key_ ID of the submitting key. Older unattributed prompts do not match.
- `metadata` (object): Exact AND matches, e.g. metadata[customer]=Acme. Keys up to 64 bytes; values up to 512 bytes.
- `provider` (string): Exact requested provider. Gemini uses the public name gemini.. one of "openai", "anthropic", "xai", "gemini"
- `model` (string): Exact requested model ID; use with provider to disambiguate.. max length 256
- `document_access` (string): Exact admitted document permission; not inferred from tool behavior.. one of "read", "comment", "edit"
- `pricing_version` (string): Exact pricing version pinned at admission.. max length 256
## Responses
### 200 `UsageReport`
Complete filtered usage report
- `start` (string, required): example "2026-09-06T00:00:00Z"
- `end` (string, required): example "2026-09-08T00:00:00Z"
- `time_basis` (string, required): always "completed_at". example "completed_at"
- `bucket` (string, required): one of "hour", "day", "month", "none". example "day"
- `group_by` (array, required): max items 3. example ["metadata.customer"]
- `totals` (UsageTotals, required): example {"request_count":5,"tool_cpu_seconds":"5.000000","inference_usage":{"input_tokens":6000,"cached_input_tokens":2000,"cache_creation_input_tokens":0,"output_tokens":1000,"web_search_requests_by_unit":{"calls":5,"queries":0,"grounded_prompts":0}},"cost":{"total_usd":"0.550000000","itemized":{"base_fee_usd":"0.250000000","cpu_fee_usd":"0.250000000","inference_fee_usd":"0.050000000"}}}
- `request_count` (integer, required): min 0. example 5
- `tool_cpu_seconds` (string, required): example "5.000000"
- `inference_usage` (AggregatedInferenceUsage | null, required): Null if any receipt in this aggregate lacks inference counters; charge totals remain complete. Empty aggregates have zero counters.. example {"input_tokens":6000,"cached_input_tokens":2000,"cache_creation_input_tokens":0,"output_tokens":1000,"web_search_requests_by_unit":{"calls":5,"queries":0,"grounded_prompts":0}}
- `cost` (Cost, required): Settled customer charges. Itemized base, CPU and inference fees are disjoint and sum exactly to total_usd. Inference includes model processing and web search. No overlapping platform subtotal. Balances and limits are separate from cost.. example {"total_usd":"0.305000000","itemized":{"base_fee_usd":"0.250000000","cpu_fee_usd":"0.005000000","inference_fee_usd":"0.050000000"}}
- `total_usd` (string, required): example "0.305000000"
- `itemized` (object, required)
- `base_fee_usd` (string, required): example "0.250000000"
- `cpu_fee_usd` (string, required): example "0.005000000"
- `inference_fee_usd` (string, required): example "0.050000000"
- `data` (array, required): max items 1000. example [{"bucket_start":"2026-09-06T00:00:00Z","bucket_end":"2026-09-07T00:00:00Z","dimensions":{"metadata.customer":"acme"},"request_count":2,"tool_cpu_seconds":"2.000000","inference_usage":{"input_tokens":2400,"cached_input_tokens":800,"cache_creation_input_tokens":0,"output_tokens":400,"web_search_requests_by_unit":{"calls":2,"queries":0,"grounded_prompts":0}},"cost":{"total_usd":"0.220000000","itemized":{"base_fee_usd":"0.100000000","cpu_fee_usd":"0.100000000","inference_fee_usd":"0.020000000"}}},{"bucket_start":"2026-09-07T00:00:00Z","bucket_end":"2026-09-08T00:00:00Z","dimensions":{"metadata.customer":"acme"},"request_count":3,"tool_cpu_seconds":"3.000000","inference_usage":{"input_tokens":3600,"cached_input_tokens":1200,"cache_creation_input_tokens":0,"output_tokens":600,"web_search_requests_by_unit":{"calls":3,"queries":0,"grounded_prompts":0}},"cost":{"total_usd":"0.330000000","itemized":{"base_fee_usd":"0.150000000","cpu_fee_usd":"0.150000000","inference_fee_usd":"0.030000000"}}}]
- `bucket_start` (string | null, required): example "2026-09-06T00:00:00Z"
- `bucket_end` (string | null, required): example "2026-09-07T00:00:00Z"
- `dimensions` (object, required): example {"metadata.customer":"acme"}
- `request_count` (integer, required): min 0. example 2
- `tool_cpu_seconds` (string, required): example "2.000000"
- `inference_usage` (AggregatedInferenceUsage | null, required): Null if any receipt in this aggregate lacks inference counters; charge totals remain complete. Empty aggregates have zero counters.. example {"input_tokens":2400,"cached_input_tokens":800,"cache_creation_input_tokens":0,"output_tokens":400,"web_search_requests_by_unit":{"calls":2,"queries":0,"grounded_prompts":0}}
- `cost` (Cost, required): Settled customer charges. Itemized base, CPU and inference fees are disjoint and sum exactly to total_usd. Inference includes model processing and web search. No overlapping platform subtotal. Balances and limits are separate from cost.. example {"total_usd":"0.305000000","itemized":{"base_fee_usd":"0.250000000","cpu_fee_usd":"0.005000000","inference_fee_usd":"0.050000000"}}
- `total_usd` (string, required): example "0.305000000"
- `itemized` (object, required)
- `base_fee_usd` (string, required): example "0.250000000"
- `cpu_fee_usd` (string, required): example "0.005000000"
- `inference_fee_usd` (string, required): example "0.050000000"
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/get-usage
---
# Create a webhook
> Register up to 10 active account-wide endpoints.
See [payloads, signatures, and delivery behavior](https://revise.io/developer/docs/revise-api/webhooks) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.webhooks.create(
{
url: "https://example.com/hooks/revise",
events: ["prompt.completed", "conversion.completed"],
},
{ idempotencyKey: "webhook-42" },
);
```
`POST https://revise.io/api/v1/webhooks`
Register up to 10 active account-wide endpoints. Use HTTPS port 443 and public addresses; redirects are not followed. Store signing_secret on creation: it is returned on idempotent replay but omitted from lists. Matching status transitions produce signed JSON notifications containing the job ID, not document content. Deliveries have a 30-second timeout and up to 11 attempts within 24 hours; duplicate or out-of-order delivery is possible. See the webhook guide for payloads, HMAC verification, and acknowledgement-based deletion.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Headers
- `Idempotency-Key` (string, required): min length 1. max length 128
## Body
- `url` (string, required): example "https://app.example.com/hooks/revise"
- `events` (array, required): min items 1. max items 7
## Responses
### 201 `Webhook`
Webhook and signing secret, including on idempotent replay. No Idempotent-Replayed header is returned.
- `id` (string, required): example "wh_21ff52db-63f9-4a1c-a1ab-8d1a9f11c201"
- `url` (string, required): example "https://example.com/webhooks/revise"
- `events` (array, required): min items 1. max items 7. example ["prompt.completed","prompt.failed"]
- `created_at` (string, required): example "2026-09-08T12:00:00Z"
- `deleted_at` (string | null, required): example null
- `signing_secret` (string): Returned only on creation and its idempotent replay. Store securely.. example "whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/create-webhook
---
# List webhooks
> List active webhook endpoints (no signing secrets).
See [payloads, signatures, and delivery behavior](https://revise.io/developer/docs/revise-api/webhooks) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.webhooks.list();
```
`GET https://revise.io/api/v1/webhooks`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Responses
### 200 `WebhookList`
Success
- `data` (array, required)
- `id` (string, required): example "wh_21ff52db-63f9-4a1c-a1ab-8d1a9f11c201"
- `url` (string, required): example "https://example.com/webhooks/revise"
- `events` (array, required): min items 1. max items 7. example ["prompt.completed","prompt.failed"]
- `created_at` (string, required): example "2026-09-08T12:00:00Z"
- `deleted_at` (string | null, required): example null
- `signing_secret` (string): Returned only on creation and its idempotent replay. Store securely.. example "whsec_AAECAwQFBgcICQoLDA0ODxAREhMUFRYXGBkaGxwdHh8="
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/list-webhooks
---
# Delete a webhook
> Disable endpoint and cancel pending deliveries.
See [payloads, signatures, and delivery behavior](https://revise.io/developer/docs/revise-api/webhooks) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
await revise.webhooks.delete("wh_id");
```
`DELETE https://revise.io/api/v1/webhooks/{id}`
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Responses
### 204
Deleted (idempotent); an in-flight request may already have reached the receiver
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/delete-webhook
---
# List webhook deliveries
> Newest first, scoped cursor.
See [payloads, signatures, and delivery behavior](https://revise.io/developer/docs/revise-api/webhooks) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.webhooks.deliveries("wh_id", { limit: 20 });
```
`GET https://revise.io/api/v1/webhooks/{id}/deliveries`
Newest first, scoped cursor. Retained for 30 days. Payloads, target response bodies, and secrets are not returned.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
## Query parameters
- `limit` (integer): min 1. max 100
- `cursor` (string)
## Responses
### 200 `WebhookDeliveryList`
Success
- `data` (array, required)
- `id` (string, required): example "del_36106431-4db2-459a-92d9-08ce744d5aac"
- `event_id` (string, required): example "evt_b70793a8-0a0c-4e8f-827d-f1ddba77703f"
- `event_type` (string, required): example "prompt.completed"
- `status` (string, required): one of "pending", "sending", "delivered", "exhausted", "cancelled". example "pending"
- `attempts` (integer, required): min 0. example 2
- `next_attempt_at` (string | null, required): example "2026-09-08T12:02:00Z"
- `last_http_status` (integer | null, required): example 503
- `last_error` (string | null, required): example "http_error"
- `created_at` (string, required): example "2026-09-08T12:00:00Z"
- `delivered_at` (string | null, required): example null
- `next_cursor` (string | null, required)
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/list-webhook-deliveries
---
# Retry a webhook delivery
> Send no body.
See [payloads, signatures, and delivery behavior](https://revise.io/developer/docs/revise-api/webhooks) for integration guidance.
## TypeScript
```ts
import { ReviseClient } from "@reviseio/api";
const revise = new ReviseClient({ apiKey: process.env.REVISE_API_KEY! });
const result = await revise.webhooks.retry("wh_id", "delivery_id", {
idempotencyKey: "delivery-retry-42",
});
```
`POST https://revise.io/api/v1/webhooks/{id}/deliveries/{delivery_id}/retry`
Send no body. Retry an exhausted delivery for an active endpoint: at most three manual cycles within seven days of creation. Each cycle has up to 11 attempts within 24 hours, bounded by the seven-day deadline. Reuse the same key to replay a retry. Notification ID and payload are preserved; deleted content is never restored. No Idempotent-Replayed header is returned.
## Authorization
`Authorization: Bearer ` with an API key from https://revise.io/console/api-keys.
## Path parameters
- `id` (string, required)
- `delivery_id` (string, required)
## Headers
- `Idempotency-Key` (string, required): min length 1. max length 128
## Responses
### 202 `WebhookRetry`
Success
- `id` (string, required)
### default `Error`
Error
- `error` (object, required)
- `code` (string, required)
- `message` (string, required)
- `details` (object)
Source: https://revise.io/developer/docs/revise-api/reference/retry-webhook-delivery