Tiptap Engine

June 3, 2026 · View on GitHub

Complete protocol reference for port developers. Every command, event, and response is documented here with payload shapes and JSON examples.

All communication is JSON strings over the platform-specific bridge (WKWebView on iOS, Chromium-based WebView on Android, jsdom in Node.js for testing). Messages fall into three categories:

  • Commands (Port to Engine): carry a unique id, return a response
  • Responses (Engine to Port): correlated to commands by id
  • Events (Engine to Port): pushed asynchronously, not correlated to commands

Table of Contents


Message Format

Command (Port to Engine)

{
  "type": "command",
  "id": "unique-string",
  "name": "commandName",
  "payload": {}
}

Every command must have a unique id. The engine returns a response with the same id. Use any string — UUIDs, incrementing counters, timestamps, anything unique within the session.

Response (Engine to Port)

{
  "type": "response",
  "id": "unique-string",
  "success": true,
  "payload": {},
  "error": { "code": "ERROR_CODE", "message": "Human-readable message" }
}

payload is present on success for query commands. error is present when success is false.

Event (Engine to Port)

{
  "type": "event",
  "name": "eventName",
  "payload": {}
}

Events have no id. They are pushed whenever state changes.


Commands

Lifecycle Commands

init

Create the editor. Must be the first command sent. Emits schemaReady, ready, and stateChanged events before the response.

The engine loads a fixed extension set — Tiptap v3 StarterKit plus the Image node — with default options. The extension set is not selectable or configurable from the port; changing it is a build-time change to the engine.

Payload:

FieldTypeRequiredDefaultDescription
contentobject or stringNoEmpty documentInitial content as Tiptap JSON or HTML string
editablebooleanNotrueWhether the editor starts in editable mode

Example:

{
  "type": "command",
  "id": "1",
  "name": "init",
  "payload": {
    "content": "<p>Hello <strong>world</strong>!</p>",
    "editable": true
  }
}

Response: { "success": true }

Errors:

  • ALREADY_INITIALIZED — The engine is already initialized. Call destroy first.

destroy

Tear down the editor and clean up all resources. After this, the engine accepts a new init command.

Payload: None (empty object).

{ "type": "command", "id": "2", "name": "destroy", "payload": {} }

Response: { "success": true }


setEditable

Toggle read-only mode.

Payload:

FieldTypeRequiredDescription
editablebooleanYesWhether the editor should be editable
{
  "type": "command",
  "id": "3",
  "name": "setEditable",
  "payload": { "editable": false }
}

Response: { "success": true }


Content Commands

setContent

Replace the entire document.

Payload:

FieldTypeRequiredDefaultDescription
contentobject or stringYesNew content as Tiptap JSON or HTML string
emitUpdatebooleanNotrueWhether to emit a stateChanged event
{
  "type": "command",
  "id": "4",
  "name": "setContent",
  "payload": {
    "content": "<h1>New Title</h1><p>New paragraph</p>"
  }
}

Response: { "success": true }


getContent

Request the current document content in a specific format.

Payload:

FieldTypeRequiredDescription
format"json" or "html" or "text"YesOutput format
{
  "type": "command",
  "id": "5",
  "name": "getContent",
  "payload": { "format": "html" }
}

Response:

{
  "success": true,
  "payload": {
    "content": "<p>Hello <strong>world</strong>!</p>"
  }
}

For format: "json", content is a Tiptap JSON document object. For format: "html" and format: "text", content is a string.

Errors:

  • INVALID_FORMAT — Unknown format value.

insertContentAt

Insert content at a specific position or range.

Payload:

FieldTypeRequiredDescription
positionnumber or { "from": number, "to": number }YesWhere to insert
contentobject or stringYesContent to insert (JSON, HTML, or plain text)
{
  "type": "command",
  "id": "6",
  "name": "insertContentAt",
  "payload": {
    "position": 5,
    "content": "<strong>inserted</strong>"
  }
}

Response: { "success": true }


Text Input Commands

insertText

Insert text at the current selection or a given range. This is the primary command for committed keystrokes from the native input system.

Payload:

FieldTypeRequiredDescription
textstringYesThe text to insert
range{ "from": number, "to": number }NoOptional range to replace. If omitted, inserts at current selection.
{
  "type": "command",
  "id": "7",
  "name": "insertText",
  "payload": { "text": "Hello" }
}

With a range (e.g., after IME composition commit):

{
  "type": "command",
  "id": "8",
  "name": "insertText",
  "payload": {
    "text": "replaced",
    "range": { "from": 1, "to": 6 }
  }
}

Response: { "success": true }


deleteRange

Delete content in a range or at the cursor (simple character deletion).

Payload:

FieldTypeRequiredDescription
range{ "from": number, "to": number }NoRange to delete. If omitted, deletes one character before the cursor.

Simple character deletion (no range):

{ "type": "command", "id": "9", "name": "deleteRange", "payload": {} }

Explicit range:

{
  "type": "command",
  "id": "10",
  "name": "deleteRange",
  "payload": { "range": { "from": 5, "to": 10 } }
}

Response: { "success": true }

Note: For user-initiated backspace actions, prefer the backspace command instead. It runs ProseMirror's full keybinding chain which handles structural operations like joining blocks and lifting list items. deleteRange without a range only performs simple single-character deletion.


Generic Execution

exec

Execute any Tiptap command by name. This is the gateway for all formatting, structural, and utility commands. The engine calls editor.chain().focus()[commandName](args).run().

Payload:

FieldTypeRequiredDescription
commandstringYesThe Tiptap command name
argsobjectNoArguments to pass to the command

Toggle bold (no args):

{
  "type": "command",
  "id": "11",
  "name": "exec",
  "payload": { "command": "toggleBold" }
}

Set heading level (with args):

{
  "type": "command",
  "id": "12",
  "name": "exec",
  "payload": { "command": "setHeading", "args": { "level": 2 } }
}

Insert an image:

{
  "type": "command",
  "id": "13",
  "name": "exec",
  "payload": {
    "command": "setImage",
    "args": { "src": "https://example.com/image.png", "alt": "Example" }
  }
}

Response:

{ "success": true, "payload": { "executed": true } }

Errors:

  • UNKNOWN_EXEC_COMMAND — The command name is not available on the editor.

See Command Names for exec for the full list.


Selection Commands

setTextSelection

Set a cursor or text range selection.

Payload:

FieldTypeRequiredDescription
anchornumberYesThe fixed side of the selection
headnumberNoThe moving side. If omitted, equals anchor (cursor).

Cursor at position 5:

{
  "type": "command",
  "id": "14",
  "name": "setTextSelection",
  "payload": { "anchor": 5 }
}

Range selection from 1 to 10:

{
  "type": "command",
  "id": "15",
  "name": "setTextSelection",
  "payload": { "anchor": 1, "head": 10 }
}

Response: { "success": true }


setNodeSelection

Select an entire node at a position (e.g., an image or horizontal rule).

Payload:

FieldTypeRequiredDescription
positionnumberYesPosition of the node to select
{
  "type": "command",
  "id": "16",
  "name": "setNodeSelection",
  "payload": { "position": 5 }
}

Response: { "success": true }


selectAll

Select the entire document.

Payload: None.

{ "type": "command", "id": "17", "name": "selectAll", "payload": {} }

Response: { "success": true }


focus

Set logical focus on the editor.

Payload:

FieldTypeRequiredDefaultDescription
position"start" or "end" or "all" or numberNoCurrent positionWhere to place the cursor on focus
{
  "type": "command",
  "id": "18",
  "name": "focus",
  "payload": { "position": "end" }
}

Response: { "success": true }


blur

Remove logical focus from the editor.

Payload: None.

{ "type": "command", "id": "19", "name": "blur", "payload": {} }

Response: { "success": true }


Keyboard Action Commands

These commands simulate real keypresses through ProseMirror's keybinding infrastructure. They give ports correct structural behavior without needing to understand the document model.

Ports should prefer these over deleteRange (for backspace) and exec('splitBlock') (for enter) when handling user keyboard input, because the keybinding chains handle context-sensitive behavior that single commands miss.

backspace

Execute ProseMirror's full Backspace command chain, which handles:

  • Deleting the current selection (if non-empty)
  • Joining the current block with the previous one (e.g., merging two paragraphs)
  • Lifting list items out of their parent list
  • Selecting and deleting atomic nodes (images, horizontal rules)
  • Deleting a single character before the cursor (when no structural operation applies)

Payload: None.

{ "type": "command", "id": "20", "name": "backspace", "payload": {} }

Response: { "success": true }

When to use backspace vs deleteRange:

  • Use backspace for user-initiated backspace actions (hardware keyboard, on-screen keyboard backspace button). It handles all the structural edge cases automatically.
  • Use deleteRange with an explicit range when the port knows the exact range to delete (e.g., programmatic deletion, clearing a specific span of text).

enter

Simulate an Enter keypress through TipTap's full Enter keybinding chain, which handles:

  • Inserting a newline inside code blocks
  • Creating a paragraph next to non-text blocks (images, horizontal rules)
  • Lifting out of empty blockquotes and list items
  • Splitting list items correctly (creating a new list item, not a new paragraph)
  • Splitting blocks (the default paragraph split behavior)

Payload: None.

{ "type": "command", "id": "21", "name": "enter", "payload": {} }

Response: { "success": true }

When to use enter vs exec('splitBlock'):

  • Use enter for user-initiated enter/return actions. It handles all block types correctly (lists, code blocks, blockquotes, etc.).
  • Use exec('splitBlock') only if you specifically want a plain block split regardless of context (rare).

Query Commands

getState

Request a full state snapshot. Returns the same payload shape as the stateChanged event.

Payload: None.

{ "type": "command", "id": "22", "name": "getState", "payload": {} }

Response:

{
  "success": true,
  "payload": {
    "doc": { "type": "doc", "pos": 0, "end": 16, "content": [...] },
    "selection": { "type": "text", "anchor": 1, "head": 1, "from": 1, "to": 1, "empty": true },
    "activeMarks": [],
    "activeNodes": [{ "type": "paragraph", "attrs": {} }],
    "commandStates": { "toggleBold": { "canExec": true, "isActive": false }, ... },
    "decorations": [],
    "storedMarks": [],
    "editable": true
  }
}

isActive

Check if a mark or node type is active at the current selection.

Payload:

FieldTypeRequiredDescription
namestringYesMark or node type name
attrsobjectNoAttributes to match (e.g., { "level": 2 } for heading)
{
  "type": "command",
  "id": "23",
  "name": "isActive",
  "payload": { "name": "bold" }
}

With attribute matching:

{
  "type": "command",
  "id": "24",
  "name": "isActive",
  "payload": { "name": "heading", "attrs": { "level": 1 } }
}

Response:

{ "success": true, "payload": { "active": true } }

canExec

Check if a command can execute in the current state.

Payload:

FieldTypeRequiredDescription
commandstringYesThe command name to check
argsobjectNoOptional arguments for the check
{
  "type": "command",
  "id": "25",
  "name": "canExec",
  "payload": { "command": "toggleBold" }
}

Response:

{ "success": true, "payload": { "canExec": true } }

getAttributes

Get attributes of a mark or node type at the current selection.

Payload:

FieldTypeRequiredDescription
namestringYesMark or node type name
{
  "type": "command",
  "id": "26",
  "name": "getAttributes",
  "payload": { "name": "heading" }
}

Response:

{ "success": true, "payload": { "attrs": { "level": 2 } } }

Events

schemaReady

Emitted once after init, before ready. Contains the full schema introspection payload for the loaded extension set (StarterKit + Image). This event is the authoritative source for which nodes, marks, and commands are available — ports should read it rather than hardcoding the list.

{
  "type": "event",
  "name": "schemaReady",
  "payload": {
    "nodes": [
      {
        "name": "paragraph",
        "contentExpression": "inline*",
        "group": "block",
        "attrs": [],
        "isLeaf": false,
        "isInline": false,
        "isBlock": true
      }
    ],
    "marks": [
      {
        "name": "bold",
        "attrs": []
      },
      {
        "name": "link",
        "attrs": [
          { "name": "href", "default": null },
          { "name": "target", "default": null }
        ]
      }
    ],
    "commands": [
      {
        "name": "toggleBold",
        "type": "toggle-mark",
        "associatedType": "bold",
        "args": [],
        "group": "formatting",
        "extensionName": "bold"
      },
      {
        "name": "setHeading",
        "type": "set-node",
        "associatedType": "heading",
        "args": [{ "name": "level", "required": true }],
        "group": "blocks",
        "extensionName": "heading"
      }
    ]
  }
}

Command types in the commands array:

TypeMeaningUI Hint
toggle-markToggles an inline mark on/offToggle button
toggle-nodeToggles a block node typeToggle button
set-nodeSets a block node type (no toggle)Dropdown option
wrapWraps selection in a nodeButton
liftLifts content out of a wrapping nodeButton
actionOne-shot action (undo, redo, etc.)Button

ready

Emitted once after init, after schemaReady. Signals the engine is ready for commands.

{ "type": "event", "name": "ready", "payload": {} }

stateChanged

Emitted after every transaction. The primary event ports use to re-render. Contains the complete editor state.

{
  "type": "event",
  "name": "stateChanged",
  "payload": {
    "doc": {
      "type": "doc",
      "pos": 0,
      "end": 16,
      "content": [
        {
          "type": "paragraph",
          "pos": 1,
          "end": 15,
          "content": [
            { "type": "text", "pos": 2, "end": 8, "text": "Hello " },
            {
              "type": "text",
              "pos": 8,
              "end": 13,
              "text": "world",
              "marks": [{ "type": "bold" }]
            },
            { "type": "text", "pos": 13, "end": 14, "text": "!" }
          ]
        }
      ]
    },
    "selection": {
      "type": "text",
      "anchor": 5,
      "head": 5,
      "from": 5,
      "to": 5,
      "empty": true
    },
    "activeMarks": [],
    "activeNodes": [{ "type": "paragraph", "attrs": {} }],
    "commandStates": {
      "toggleBold": { "canExec": true, "isActive": false },
      "undo": { "canExec": true, "isActive": false, "depth": 3 },
      "redo": { "canExec": false, "isActive": false, "depth": 0 }
    },
    "decorations": [],
    "storedMarks": [],
    "editable": true
  }
}

contentChanged

Emitted only when the document content changes (not on selection-only changes). Useful for debounced auto-save.

{
  "type": "event",
  "name": "contentChanged",
  "payload": {
    "doc": { "type": "doc", "pos": 0, "end": 16, "content": [...] }
  }
}

selectionChanged

Emitted on selection-only changes (no document mutation). Lighter than stateChanged — omits the document tree.

{
  "type": "event",
  "name": "selectionChanged",
  "payload": {
    "selection": { "type": "text", "anchor": 5, "head": 5, "from": 5, "to": 5, "empty": true },
    "activeMarks": ["bold"],
    "activeNodes": [{ "type": "paragraph", "attrs": {} }],
    "commandStates": { ... }
  }
}

error

Emitted when an error occurs in the engine.

{
  "type": "event",
  "name": "error",
  "payload": {
    "code": "COMMAND_FAILED",
    "message": "Something went wrong",
    "commandId": "42"
  }
}

commandId is present when the error was caused by a specific command.

extensionEvent

Generic passthrough for extension-specific events. The engine does not interpret these — it forwards them to the port. The fixed extension set does not currently emit any such events; this remains in the protocol so that ports and future engine builds have a defined channel for extension-defined messages.

{
  "type": "event",
  "name": "extensionEvent",
  "payload": {
    "extensionName": "someExtension",
    "eventName": "someEvent",
    "data": {}
  }
}

Responses

Every command gets exactly one response with the matching id.

Success (no payload)

Most mutation commands return a bare success:

{ "type": "response", "id": "1", "success": true }

Success (with payload)

Query commands include their result:

{
  "type": "response",
  "id": "5",
  "success": true,
  "payload": { "content": "<p>Hello</p>" }
}

Error

{
  "type": "response",
  "id": "99",
  "success": false,
  "error": {
    "code": "NOT_INITIALIZED",
    "message": "Engine is not initialized. Send an init command first."
  }
}

Error Codes

CodeMeaning
NOT_INITIALIZEDCommand sent before init or after destroy
ALREADY_INITIALIZEDinit sent when the engine is already running
UNKNOWN_COMMANDUnrecognized command name
UNKNOWN_EXEC_COMMANDThe command passed to exec doesn't exist on the editor
INVALID_FORMATUnknown format in getContent
COMMAND_FAILEDThe command threw an exception during execution

Data Types

AnnotatedNode

Every node in the document JSON carries position annotations.

{
  "type": "paragraph",
  "pos": 1,
  "end": 15,
  "attrs": { },
  "content": [ ... ],
  "marks": [{ "type": "bold", "attrs": {} }],
  "text": "Hello"
}
FieldTypePresentDescription
typestringAlwaysNode type name
posnumberAlwaysProseMirror position where this node's content starts
endnumberAlwaysProseMirror position where this node ends
attrsobjectWhen non-default attributes existNode attributes
contentAnnotatedNode[]On nodes with childrenChild nodes
marksMark[]On text nodes with marksApplied marks
textstringOn text nodesText content

Position rules:

  • For block nodes: pos is after the opening token, end is after the closing token
  • For text nodes: pos is the first character, end - pos equals the text length
  • The document node starts at pos: 0
  • A child's pos equals its parent's pos + 1 (for the first child of a block node)

SelectionState

{
  "type": "text",
  "anchor": 5,
  "head": 10,
  "from": 5,
  "to": 10,
  "empty": false
}
FieldTypeDescription
type"text" or "node" or "all" or "gapcursor"Selection kind
anchornumberFixed side of the selection
headnumberMoving side of the selection
fromnumberStart of selection range (min of anchor, head)
tonumberEnd of selection range (max of anchor, head)
emptybooleanTrue when from equals to (cursor, no range)

CommandState

{ "canExec": true, "isActive": false, "depth": 3 }
FieldTypeDescription
canExecbooleanWhether the command can execute in the current state
isActivebooleanWhether the associated mark/node is active at the selection
depthnumber (optional)Stack depth for undo/redo only

Mark

{ "type": "bold" }
{
  "type": "link",
  "attrs": { "href": "https://example.com", "target": "_blank" }
}
FieldTypePresentDescription
typestringAlwaysMark type name
attrsobjectWhen non-default attributes existMark attributes

ActiveNode

{ "type": "heading", "attrs": { "level": 2 } }

Represents a node type active at the current selection, with its attributes.


Command Names for exec

The engine loads a fixed extension set (Tiptap v3 StarterKit plus Image), so the commands available through exec are fixed. The tables below list them.

The authoritative, machine-readable list for any given engine build is the commands array in the schemaReady event — ports should read that rather than relying on this table, which is maintained by hand and documents the current set for convenience.

Formatting

CommandArgsDescription
toggleBoldToggle bold mark
toggleItalicToggle italic mark
toggleStrikeToggle strikethrough
toggleCodeToggle inline code
toggleUnderlineToggle underline
setLink{ href, target? }Apply link mark
unsetLinkRemove link mark
toggleLink{ href, target? }Toggle link mark

Block Types

CommandArgsDescription
setParagraphConvert to paragraph
toggleHeading{ level }Toggle heading (level 1-6)
setHeading{ level }Set heading without toggle
toggleCodeBlock{ language? }Toggle code block
toggleBlockquoteToggle blockquote wrapping

Lists

CommandArgsDescription
toggleBulletListToggle bullet list
toggleOrderedListToggle ordered list
sinkListItemIndent list item
liftListItemOutdent list item
splitListItemSplit a list item

Insert

CommandArgsDescription
setHorizontalRuleInsert horizontal rule
setHardBreakInsert hard break (shift+enter)
setImage{ src, alt?, title? }Insert image

History

CommandArgsDescription
undoUndo last change
redoRedo last undone change

Selection

CommandArgsDescription
selectAllSelect entire document
focus{ position? }Focus the editor
blurBlur the editor