Skip to content

Document JSON

format=json: a document and its changes as structured data, for code that acts on them. Not for display: show the PDF, or embed the HTML.

View as Markdown

Showing a redline? Use the PDF, or embed the HTML for an interactive view. This format is for code that acts on the changes.

format=json is a document as structured data: its paragraphs, headings and tables in order, its text with every tracked change marked, and beside the body its headers, footers, notes, comments and revisions. It is the same listing as the Markdown, shaped for code. Every endpoint that carries a document serves it:

Ask for it with format=json and download it from file.downloads[].url, like any other format. The OpenAPI specification publishes its shape as components.schemas.StructuredDocument.

What it is for

Use it for what your code decides about the changes: counting insertions and deletions, listing the paragraphs that changed, routing a document when a clause you care about changed, linking a change or paragraph to a record in your own app (and to the embedded HTML's vs:scrollTo), and reading the locators an edit names.

def changes(segments):
    """Every tracked change in a list of segments, nested changes included."""
    for segment in segments:
        if segment.get("type") in ("insertion", "deletion", "moved_from", "moved_to"):
            yield segment
        yield from changes(segment.get("content", []))


def paragraphs(blocks):
    """Every paragraph and heading, table cells' included."""
    for block in blocks:
        if block["type"] == "table":
            for row in block["rows"]:
                for cell in row["cells"]:
                    yield from paragraphs(cell["blocks"])
        else:
            yield block


redline = requests.get(json_url).json()
changed = [p for p in paragraphs(redline["blocks"]) if any(changes(p.get("content", [])))]
print(f"{len(redline.get('revisions', []))} changes in {len(changed)} paragraphs")
for paragraph in changed:
    if paragraph.get("label", "").startswith("7"):
        print("Section 7 changed:", paragraph["id"])

It is not for display. It describes the document's structure and changes, not how the document is laid out in pages, so a viewer drawn from it re-implements Word's layout and still shows less than the PDF and the HTML redline Version Story already renders.

The shape

{
  "schema_version": 1,
  "blocks": [
    {
      "type": "paragraph",
      "index": 1,
      "id": "f4378834652a478eb5c67af7fa1b9eea",
      "style_id": "Normal",
      "content": [
        { "text": "This Non-Disclosure Agreement (this “Agreement”) is entered into between " },
        { "type": "deletion", "revision": { "index": 0, "id": "3e9a1c7b5d2f4e60a8b1c2d3e4f50617" },
          "content": [{ "text": "[COMPANY NAME]", "editable": false, "format": { "bold": true } }] },
        { "type": "insertion", "revision": { "index": 1, "id": "9f2c4e1a7b3d4c8e9a1f2b3c4d5e6f70" },
          "content": [{ "text": "Acme, Inc.", "format": { "bold": true } }] },
        { "text": ", a Delaware corporation …" }
      ]
    }
  ],
  "notes": [
    {
      "part": { "kind": "footnote", "index": 0, "id": "2b7d9e1f3a5c4e6f8b0d2f4a6c8e0b1d" },
      "label": "1",
      "paragraph": { "index": 1, "id": "f4378834652a478eb5c67af7fa1b9eea" },
      "text_before": "This Non-Disclosure Agreement (this “Agreement”) is entered into between Acme, Inc.",
      "blocks": [
        {
          "type": "paragraph",
          "index": 0,
          "id": "b6ac5dabcec1413aa880e0a3a2d9f237",
          "style_id": "FootnoteText",
          "content": [
            { "type": "deletion", "revision": { "index": 2, "id": "c71d2e3f4a5b4c6d8e9f0a1b2c3d4e5f" }, "content": [{ "text": "Formerly ", "editable": false }] },
            { "text": "Acme Holdings LLC." }
          ]
        }
      ]
    }
  ],
  "revisions": [
    { "index": 0, "id": "3e9a1c7b5d2f4e60a8b1c2d3e4f50617", "type": "deletion", "author": "Dana Reyes", "date": "2026-10-22T03:37:30Z",
      "on": ["text"], "text": "[COMPANY NAME]",
      "paragraph": { "index": 1, "id": "f4378834652a478eb5c67af7fa1b9eea" } },
    { "index": 1, "id": "9f2c4e1a7b3d4c8e9a1f2b3c4d5e6f70", "type": "insertion", "author": "Dana Reyes", "date": "2026-10-22T03:37:30Z",
      "on": ["text"], "text": "Acme, Inc.",
      "paragraph": { "index": 1, "id": "f4378834652a478eb5c67af7fa1b9eea" } },
    { "index": 2, "id": "c71d2e3f4a5b4c6d8e9f0a1b2c3d4e5f", "type": "deletion", "author": "Dana Reyes", "date": "2026-10-22T03:37:30Z",
      "on": ["text"], "text": "Formerly ",
      "part": { "kind": "footnote", "index": 0, "id": "2b7d9e1f3a5c4e6f8b0d2f4a6c8e0b1d" },
      "paragraph": { "part": { "kind": "footnote", "index": 0, "id": "2b7d9e1f3a5c4e6f8b0d2f4a6c8e0b1d" }, "index": 0, "id": "b6ac5dabcec1413aa880e0a3a2d9f237" } }
  ]
}

Beside blocks and revisions, the document carries headers, footers, notes, comments, settings, list_styles, paragraph_styles and final_section, each present when the document has any. A merge's document adds kind after schema_version.

Locators

Every index counts from 0 within its container: paragraphs and tables within their part (the body, or one header, footer or note), rows within their table, cells within their row, headers and footers and notes of a kind, and sections, comment threads, pictures, conflicts and revisions within the document. Everything with an index also carries an id, which it keeps when content moves. The redline's Markdown and HTML give each the same index and id.

Where the listing points at something else, it prints that thing's locator, the object an edit sends as it is:

Points atLocator
A body paragraph{"index": 12, "id": "…"}, or {"id": "…"} for one the listing gives no index
A header, footer or note paragraph{"part": {...}, "index": 0, "id": "…"}
A header or footer{"kind": "header", "index": 0, "id": "…", "type": "default", "section": {"index": 0, "id": "…"}}: its position among the headers (or footers), in order of the first section that shows each, its page type and that first section
A note{"kind": "footnote", "index": 0, "id": "…"}: its position among the notes of its kind
A revision{"index": 4, "id": "…"}

What Word shows is a label, never a position: a list paragraph's generated number (2.4(a)) and a note's mark.

Blocks

blocks is the body in document order. Each block's type is paragraph, heading or table.

FieldMeaning
indexA paragraph's position among its part's paragraphs, counted from 0, table cells' included; a table's among its part's tables, an outer table before those nested in it. The {"index": n} an edit names
idIts own identifier, kept when content moves. The Markdown and the embedded HTML give it the same id
style_idIts paragraph style
labelA numbered paragraph's list label as Word shows it (2.4(a)). It is not part of the text
heading_levelA heading's level, from its Heading 1 to 9 style
list_levelA numbered paragraph's list level, 1 to 9 as Word counts them
contentIts text, as a list of segments
paragraph_format, section, fields, bookmarksIts formatting where it sets its own, the section that ends at it, and the fields and bookmarks in it

Tables

A table holds rows, each row holds cells, and each cell holds its own blocks, so a cell's content is read exactly like the body.

{
  "type": "table", "index": 0, "id": "5b0e91c4…", "style_id": "TableGrid",
  "rows": [
    { "index": 0, "id": "6e127ae6…", "header": true, "cells": [
      { "index": 0, "id": "a91c03d2…", "column": 0, "blocks": [] },
      { "index": 1, "id": "c4d8e1f0…", "column": 1, "span": 2, "blocks": [] }
    ] },
    { "index": 1, "id": "0d7f52b9…", "change": { "type": "insertion", "revision": { "index": 3, "id": "0a4b8c2d6e1f4a3b9c7d5e2f1a0b9c8d" } }, "cells": [] }
  ]
}
FieldOnMeaning
index, idRow, cellA row's position in its table, and a cell's in its row, counted from 0; and its id
headerRowtrue for a header row, repeated on each page
columnCellThe grid column the cell starts at, counted from 0. A column locator names it, never the cell's index
spanCellThe grid columns a merged cell covers, present when more than one. The next cell's column is this cell's plus its span
changeRow, cellA tracked insertion or deletion of the whole row or cell: {"type", "revision": {"index", "id"}}
table_format, cell_formatTable, cellThe borders it sets itself

Segments

A segment without a type is unchanged text, with text and, where they apply, format and link. A segment with one is one of:

typeMeaning
insertionText the later version added
deletionText the later version removed, marked "editable": false
moved_from / moved_toThe two halves of one relocation, both naming the same revision
conflictIn a merge, competing changes from different revisions over the same text, with the conflict's index and id. Surface these to a person; don't resolve them automatically
equationA Word equation, as below
tab, breakA tab, or a line, page or column break (kind)
picture, objectA picture, with its index, id, size and alt_text, or a chart, shape, text box or embedded object

A change is {"type", "revision": {"index": n, "id": "…"}} with its text when it holds plain text, or its content when its segments carry more: formatting, a link, a break, another change, or a deletion's "editable": false. Its author and date are on its entry in revisions, once. Changes nest: a revision that edits inside another revision's text sits in that change's content, so both attributions survive.

In a merge's document, competing changes are wrapped in {"type": "conflict", "index": n, "id": "…", "content": [...]}. Where two revisions replaced the same words, the merge keeps one deletion of them, and that deletion also lists, as revisions, the revision of every change in its conflict:

{ "type": "conflict", "index": 0, "id": "8c1e5a9d3b7f4e2a6c0d8b4f2e6a1c3d", "content": [
  { "type": "deletion", "revision": { "index": 3, "id": "0a4b8c2d6e1f4a3b9c7d5e2f1a0b9c8d" }, "revisions": [{ "index": 3, "id": "0a4b8c2d6e1f4a3b9c7d5e2f1a0b9c8d" }, { "index": 4, "id": "5e6f7a8b9c0d4e1f8a2b3c4d5e6f7081" }, { "index": 5, "id": "d2e3f4a5b6c74d8e9f0a1b2c3d4e5f60" }],
    "content": [{ "text": "[•]", "editable": false }] },
  { "type": "insertion", "revision": { "index": 4, "id": "5e6f7a8b9c0d4e1f8a2b3c4d5e6f7081" }, "text": "April 15, 2027" },
  { "type": "insertion", "revision": { "index": 5, "id": "d2e3f4a5b6c74d8e9f0a1b2c3d4e5f60" }, "text": "December 31, 2026" }
] }

Equations

A Word equation is one segment, {"type": "equation", "content": [...]}. Its content is the equation in a linear form, as text segments marked "editable": false: (T-D)/T is a fraction, F_m a subscript, x^2 a superscript, ∑_(i=1)^n (q_i p_i) a sum with its limits. A tracked change inside the equation is a change segment naming its revision.

{ "type": "equation", "content": [
  { "text": "(T-D)/", "editable": false },
  { "type": "deletion", "revision": { "index": 6, "id": "7b8c9d0e1f2a4b3c8d4e5f6a7b8c9d0e" }, "content": [{ "text": "T", "editable": false }] },
  { "type": "insertion", "revision": { "index": 7, "id": "e1f2a3b4c5d64e7f8a9b0c1d2e3f4a5b" }, "content": [{ "text": "N", "editable": false }] }
] }

No text selector matches an equation's text, so no text edit changes it. The Markdown writes it as <equation>…</equation>.

Revisions

revisions lists every tracked change once per accept or reject decision, in document order: the body first, then headers and footers section by section, footnotes and endnotes. A replacement is two entries, a deletion and an insertion. Counting revisions is counting changes the way a reviewer does.

FieldMeaning
indexIts position, counted from 0
idIts identifier. Every change that is part of it names it as {"index": n, "id": "…"}, and add_comment, accept_revision and reject_revision take the same locator, by index, id or both. The redline's HTML marks every piece of the change with this id, both halves of a move included
typeinsertion, deletion, move (both halves of a move) or property (a formatting, paragraph, numbering, table or section property change)
author, dateWho made it and when. In a merge's document, author is the revision's label, such as Dana Reyes — nda_from_counsel.docx
onWhat it changes: text, paragraph_mark, paragraph, numbering, section, table, table_row or table_cell. paragraph_mark is a change to a paragraph's mark: one inserted or deleted, as a split, a join or a whole paragraph added or removed makes, or its formatting
textThe first 200 characters of the text inserted, deleted, moved or formatted, a paragraph mark as a newline; for another property change, the text of what it changes
partIts part, for a revision in a header, footer or note
paragraphThe locator of the paragraph it starts in
table, row, cellThe table, row or cell it changes, by {"id"}, when it starts in no paragraph
sectionThe section a section property change changes, {"index": n, "id": "…"}

comments lists the comment threads, each with its index, id, author, date, text, the paragraph its anchor starts in, anchor_text, resolved and replies.

The older comparison JSON

A comparison's response also carries a json in its deprecated top-level downloads: the comparison's older rendering, kept for the integrations built on it. New code reads the json in file, above.

It has schema_version, kind and blocks, and nothing else. A list paragraph's label is number, with its list_id. Its paragraphs have their text, formatting, tracked changes and level, but no index, heading_level, list_level, bookmarks, paragraph formatting, links, fields, pictures, objects or equations, and its tables and cells have no position. Each change segment carries its id (Word's own revision id), author, revision (the document version that introduced it) and date, with text, or content when changes nest. A whole table row that was inserted or deleted carries a change on the row itself. To reconstruct either version from it:

  • The base document: everything except insertion segments.
  • The revised document: everything except deletion segments, and table rows whose change is a deletion.

Spreadsheet and presentation comparisons have no JSON.