Embed API
The embed puts the Proof of Thought editor in your page as an iframe served from embed.proof.exalto.ai. Your page passes it a document id and a way to get session tokens; everything the person does inside it goes straight to Proof. For a walkthrough, see Embed the editor.
Install
Section titled “Install”From npm, for pages you bundle:
npm install @exalto/proof-embedimport { mountEditor, mountPresenceSettings, openPresenceSettings } from "@exalto/proof-embed";Or with a script tag, which defines window.Proof with the same three functions:
<script src="https://embed.proof.exalto.ai/v1.js"></script><script> const editor = Proof.mountEditor(document.getElementById("editor"), { document: "doc_8Hq2Lm4R", getToken });</script>The package is a few kilobytes: it creates the iframe and passes messages to it. The editor itself always loads from embed.proof.exalto.ai, so it stays current without a new release of your page. v1.js and @exalto/proof-embed 1.x only change in ways that keep your code working.
mountEditor
Section titled “mountEditor”const editor = mountEditor(element, options);element is the container the iframe goes in; it replaces anything inside it. mountEditor returns the editor at once and loads it in the background: wait for the ready event before calling methods that need the document.
const editor = mountEditor(document.querySelector("#editor")!, { document: "doc_8Hq2Lm4R", getToken: async () => { const response = await fetch("/proof/session?doc=doc_8Hq2Lm4R", { method: "POST" }); return (await response.json()).token; }, comments: "right", colorScheme: "auto",});Options
Section titled “Options”| Option | Type | Default | What it does |
|---|---|---|---|
document |
string |
required | The document to open. |
getToken |
() => Promise<string> |
required | Returns a fresh session token for this document and the person viewing it. |
mode |
"edit" or "suggest" |
"edit" where the role allows |
Whether the person’s changes are made or proposed. A suggester always suggests and a viewer only reads, whatever you pass. |
colorScheme |
"light", "dark" or "auto" |
"light" |
"auto" follows the person’s system setting. |
toolbar |
"header", "floating" or "none" |
"header" |
Where the format bar goes: along the top, floating at the bottom, or nowhere (Markdown shortcuts and the ⌘K palette still work). |
comments |
"right", "left" or "hidden" |
"right" |
Where comment and suggestion cards go. Where the frame is too narrow, they go in a panel. "hidden" hides comments but not suggestions. |
rails |
"left", "right" or "hidden" |
"left" |
Where the attribution rail goes, beside each line. |
highlight |
boolean |
false |
Tint text by who wrote it. |
height |
"auto" or "fill" |
"auto" |
See Sizing. |
target |
{ kind, id } |
none | A comment (kind: "comment", a thread id) or suggestion to open once the document has loaded, as from a notification. |
autofocus |
boolean |
true |
Put the caret in the text once it loads. Pass false when your page shows something first, and call focus() when it’s ready. |
origin |
string |
where v1.js came from, or https://embed.proof.exalto.ai |
Where Proof serves the editor. |
These shape the editor around your own page’s controls:
| Option | Type | What it does |
|---|---|---|
headerStart, headerEnd |
number |
Pixels kept free at the start and end of the editor’s header, for controls of your own laid over the frame, such as a menu button or a notification bell. |
pageLeft |
number |
How far, in pixels, the frame’s left edge is from your page’s, as beside a sidebar. The editor’s dialogs then centre on your page rather than the frame, as far as the frame allows. Update it with setOptions when the sidebar resizes. |
commands |
{ title, items }[] |
Groups of your own items for the editor’s ⌘K palette. Each item is { id, label, hint?, checked?, disabled? }; choosing one sends a command event with its id. |
shortcuts |
string[] |
Keys your page listens for, such as "mod+k", that the editor passes on as a shortcut event while it has focus. |
sealNote |
{ text, action? } or null |
A line for the seal dialog, such as how long a guest’s documents last, with an optional action: { label, command } that sends a command event. |
trashActions |
boolean |
For a document in the person’s trash, the card at the foot offers Restore and Delete forever, sending command events restore and delete. |
Change any of these after mounting with editor.setOptions({ ... }). What you set is kept if the frame reloads, as it does when the person’s access changes.
Tokens
Section titled “Tokens”The editor calls getToken when it mounts, and again two minutes before each token lapses (tokens last 15 minutes), so a page left open keeps working; an open connection carries on with the new token. If a call fails, the editor keeps the person’s changes and the embed tries again after 1, 2, 5, 10 and 30 seconds. If it still fails, as when your server refuses because the person lost access, the editor emits an error event with code token_rejected and stops asking.
Your server makes the token with POST /v1/sessions. Check that the signed-in person may open the document before you do: the token is how Proof knows who is writing.
Events
Section titled “Events”const stop = editor.on("title", ({ title }) => (document.title = `${title} · Acme Notes`));stop(); // stops listeningon returns a function that removes the handler.
| Event | Payload | When |
|---|---|---|
ready |
{ document, role, mode, user } |
The document has loaded. role is "owner", "editor", "suggester" or "viewer"; user is the person’s Proof id. |
title |
{ title, name } |
The document’s title changed: name is the name an editor gave it, or null; title is that, or its first line. Also sent once after ready. |
save-state |
{ state, unsaved } |
"saving" while changes are on their way, "saved" once Proof has them, "offline" while the connection is down. unsaved is true while some change hasn’t reached Proof, for warning before the page closes. Changes made offline are kept and sent on reconnecting. |
status |
{ mode, comments, openComments, sealed, role, trashed } |
What your own controls show of the editor, whenever it changes: its mode, whether comments are shown and how many are open, whether it has a seal, the person’s role, and whether it’s in their trash. |
seal |
{ code, url, line } |
The person sealed the document with the editor’s Seal button. The seal.created webhook follows. |
open-document |
{ document, href } |
The person followed a link to another document of yours. Navigate to it in your own way; the editor doesn’t. |
thread-read |
{ thread } |
The person read a thread, by opening it or writing in it: clear your notifications of it. |
command |
{ id } |
The person chose one of your commands, or a sealNote or trash action. |
shortcut |
{ key } |
The person pressed one of your shortcuts in the editor. |
presence-settings |
{} |
The person asked for presence settings, from a check-in or the breakdown. Open them, framed or in their window. |
overlay |
{ open } |
A dialog opened in the frame, or closed: dim your own controls around it if you like. |
settings |
options | The person changed a display setting from inside the editor, such as the theme or where comments go: keep it, and pass it back next time. |
height |
{ height } |
The document’s height changed. With height: "auto", the embed resizes the frame itself. |
error |
{ code, message } |
Something stopped the editor. See below. |
Error codes
Section titled “Error codes”| Code | What happened |
|---|---|
token_rejected |
getToken failed for good, or returned a token Proof doesn’t accept. |
no_access |
The token’s person can no longer open the document. |
document_deleted |
The document was deleted while it was open. |
Handle the event to show your own message, or to send the person elsewhere. On a page whose origin isn’t one of your embed origins, the browser refuses to show the frame at all, and no event comes.
Methods
Section titled “Methods”| Method | What it does |
|---|---|
editor.focus() |
Puts the caret back where it was in the document. |
editor.setMode(mode) |
Switches between "edit" and "suggest", within what the role allows. |
editor.print() |
Prints the document, typeset, without the editor around it. |
editor.export(format) |
Resolves to the document as "markdown" or "html" text, or as a Blob for "docx". Pending suggestions are left out. |
editor.openBreakdown() |
Opens the breakdown behind the live seal: where the words came from, presence, prompts and sittings. |
editor.openSeal() |
Opens the owner’s seal dialog: seal, choose who reads its seals, withdraw them. |
editor.openPalette() |
Opens the ⌘K palette. |
editor.showTarget({ kind, id }) |
Scrolls to a comment or suggestion and opens its card. |
editor.comment() |
Starts a comment on the selection. |
editor.toggleComments() |
Shows comments beside the text or hides them; in the narrow panel, opens or closes it. |
editor.setOptions(options) |
Changes options after mounting. |
editor.destroy() |
Removes the iframe and its listeners. Changes already made are kept. |
Methods return promises, which resolve once the editor has done it.
editor.role and editor.mode hold the current role and mode once ready has fired.
Presence settings
Section titled “Presence settings”Security keys must be added on Proof’s own domain, so people add them, and choose how often to check in, in Proof’s presence settings: framed in your own settings page, or in a small window of their own. Both take a token from POST /v1/sessions with purpose: "presence".
mountPresenceSettings
Section titled “mountPresenceSettings”const presence = mountPresenceSettings(element, { getToken: () => fetch("/proof/presence-session", { method: "POST" }).then((r) => r.json()).then((r) => r.token), platform: "acme", mode: "live",}, (choice) => console.log(choice)); // { keys: 1, everyMinutes: 10 }platform is your platform’s slug and mode the key’s mode ("live" by default): framed, presence settings load only on your embed origins. The callback hears each change. presence.destroy() removes the frame. Browsers that won’t create a security key in a frame need the window instead.
openPresenceSettings
Section titled “openPresenceSettings”button.addEventListener("click", async () => { const result = await openPresenceSettings({ getToken: () => fetch("/proof/presence-session", { method: "POST" }).then((r) => r.json()).then((r) => r.token), }); console.log(result); // { keys: 1, everyMinutes: 10 }});Call it from a click or key press, or the browser blocks the window. The promise resolves when the person closes the window, with how many keys they have and their check-in interval in minutes (null when check-ins are off). See Set up check-ins.
Embed origins
Section titled “Embed origins”The editor loads only on the origins you list under Embed origins in the console. Proof serves it with a Content-Security-Policy: frame-ancestors header naming exactly those origins, so another site can’t frame your users’ documents. List each origin in full, for example https://acme.com and https://www.acme.com. In test mode, http://localhost and http://127.0.0.1 on any port are allowed as well. Set them with PATCH /v1/platform too.
If your own page sends a Content Security Policy, it needs:
| Directive | Value | For |
|---|---|---|
frame-src |
https://embed.proof.exalto.ai |
The editor and the presence window. |
script-src |
https://embed.proof.exalto.ai |
Only with the v1.js script tag. |
The iframe asks for two permissions: publickey-credentials-get, for check-ins with a security key, and clipboard-write, for copying a seal line; framed presence settings ask for publickey-credentials-create too, to add a key. If your page sends a Permissions-Policy header, it must not deny them to https://embed.proof.exalto.ai, or check-ins can’t run.
Sizing
Section titled “Sizing”With height: "auto", the iframe grows and shrinks with the document, and your page scrolls as it would with any long content. Use it when the document sits in the flow of a page.
With height: "fill", the iframe fills its container and the document scrolls inside it. Give the container a height, for example with flex or grid. Use it for a full-window editor beside your own sidebar.
The editor is at least 320 pixels tall either way, and needs 720 pixels of width to put comments beside the text.
Keyboard and focus
Section titled “Keyboard and focus”While the editor has focus, keys go to it: ⌘K (Ctrl+K) opens its palette for this document, ⌘P prints the document, and Tab indents list items. Press Esc, then Tab, to move focus out of the editor to the next control on your page. Shortcuts your page listens for on window don’t fire while the editor has focus, unless you list them in shortcuts.
Appearance
Section titled “Appearance”You choose the colour scheme, where the toolbar, comments and attribution rail go, and room in the header for your own controls. The rest is fixed, so every Proof editor reads the same way to the people who verify what was written in it:
- The editor’s typefaces and spacing.
- The seal’s colours, which stand for where words came from.
- The blocks people can write: headings, lists, checklists, quotes, code, links, images and tables. You can’t add your own.
There is no custom CSS. If your page needs something the options don’t give, tell us.
Storage and cookies
Section titled “Storage and cookies”The editor sets no cookies and needs none: everything rides on the session token. Browsers keep a framed site’s storage apart from the same site opened directly, so the embed doesn’t rely on it. Display settings a person changes in the editor come to your page as a settings event, for you to keep with your other preferences and pass back as options.
Browsers
Section titled “Browsers”The editor supports the two latest versions of Chrome, Edge, Firefox and Safari, on desktop and on iOS and Android. Check-ins with a security key need a browser that allows WebAuthn in a frame, which those versions do. Older browsers aren’t supported.