Concepts
Immutable agent diagrams. A diagram’s history can’t be edited. Every change adds a new numbered version, and Zindex keeps each version with the image it produced until you delete the diagram or its workspace is deleted; closing your account deletes your personal workspace, and a free account unused for 12 months is deleted the same way (Privacy). Any version can be read by number or by date. This page uses the REST API; the MCP tools follow the same rules, though the history by date, the exports and the image options (?engine=current, ?from=) are REST only.
Diagrams and scenes
A diagram is a scene document in DSP, the Diagram Scene Protocol: elements (nodes, edges, groups, frames, text, images and guides) with IDs you choose, plus styles and constraints. Scene schema has its shape. Zindex validates a scene, lays it out and draws it as SVG or PNG. No model runs inside Zindex: validation, layout and drawing are deterministic code.
- Workspace. Each diagram belongs to one workspace, your personal one or a team’s, and every key or sign-in acts in one workspace. Everyone in a workspace can read all of its diagrams; a diagram in another workspace answers
NOT_FOUND(Workspaces). - ID and key. Zindex mints each diagram’s ID,
dgm_and 12 characters. Akeyis your own name for a diagram, such aspayments-architecture, unique among the workspace’s live diagrams. The API and the tools take it wherever they take an ID; version links and image links take the ID only. Saving the same key and content again returns the existing diagram withcreated: false; the same key with different content, or a teammate’s create with a key the workspace already has, is a 409,KEY_EXISTS, whoseidandrevisionname the diagram that has it. - Element IDs. The IDs in the scene are yours and stay with each element across versions, so edits and comparisons name them.
- Brief. Up to 1,000 characters kept with the diagram and versioned with it: what it shows, the conventions to keep, decisions made. A read returns it under the header.
- Family.
diagramFamily(architecture,workflow,entityRelationship,sequence,network,orgchartoruiflow) selects the node types, rules and drawing of that kind of diagram. Without it the diagram is drawn as a generic flowchart, and nothing warns you. See Diagram families.
Versions
- Saving a diagram makes revision 1. Each edit that changes the diagram makes the next revision. An edit that leaves it as it was returns
noop: truewith the current revision and makes none. - A version never changes once made. Going back makes a new version equal to an old one (Going back).
GET /v1/diagrams/{id}?revision=Nreads a version by number, and?asOf=2026-09-26reads the one current at a date.asOftakes a date, meaning the end of that day in UTC, or an ISO 8601 timestamp with an offset, such as2026-09-26T14:00:00Z. A timestamp without an offset, or an impossible date, returns 400. A date before the diagram’s first version returns 404 withcode: "NO_REVISION_AS_OF".revisionandasOfcan’t be combined.- A read returns a compact outline by default: a header, the brief, the last three changes, then one element per line.
knownRevisionreturns only what changed since a version you have, andelementslimits the outline to named elements and their neighbors.view=dspreturns the scene JSON andview=mermaidreturns Mermaid (not for sequence diagrams).
History
GET /v1/diagrams/{id}/revisions lists versions newest first:
{
"id": "dgm_DdfD8FaB6fox",
"currentRevision": 3,
"versions": [
{ "revision": 3, "at": "2026-10-06T00:08:29.127Z", "by": "Ana Lima", "credential": "api_key", "client": "curl/8.7.1", "message": "Rename the ledger", "chain": "sha256:5c1f0e…", "added": 0, "changed": 1, "removed": 1 },
{ "revision": 2, "at": "2026-10-06T00:08:29.114Z", "by": "Ana Lima", "credential": "api_key", "client": "curl/8.7.1", "message": "Add the cache in front of the database", "chain": "sha256:e2b94a…", "added": 1, "changed": 1, "removed": 0 },
{ "revision": 1, "at": "2026-10-06T00:08:28.898Z", "by": "Ana Lima", "credential": "api_key", "client": "curl/8.7.1", "message": "Initial structure", "chain": "sha256:71d3c8…" }
]
}
at: when the version was saved, in ISO 8601.by: who saved it: their name, else their email;"former member"once their account is closed;nullwhen it isn’t known.credential: how:session(the dashboard),api_keyoroauth(a connected app). It never says which key.client: what made it: the MCP client’s name and version, or for a REST call theX-Zindex-Clientheader when sent, else the first product token of theUser-Agent.message: the edit’s message, or null.chain: its chain hash, which covers it and every version before it (Verifying).added,changed,removed: how many elements changed since the version before; revision 1 has none.
limit sets the page size, 1 to 50 and 10 by default, and cursor pages through a long history; nextCursor appears when there are more. ?asOf=2026-04-01 lists the history as it stood then. GET /v1/diagrams/{id}/export downloads every version as JSON Lines, each with its scene, its hash and chain, and its stored image. GET /v1/workspace/export downloads every diagram in the workspace that way, in one file (Export); in a team workspace only its owners can.
Compare
GET /v1/diagrams/{id}/compare?from=1&to=4 lists what changed between two versions. from and to each take a revision number or a date; to defaults to the latest.
{ "id": "dgm_DdfD8FaB6fox", "from": 1, "to": 4, "added": ["cache"], "changed": ["api", "db"], "removed": ["e1"], "scene": ["title"], "authors": [{ "by": "Ana Lima", "client": "curl/8.7.1" }] }
added, changed and removed list element IDs; scene lists the scene-level fields that changed, such as title or brief; authors lists each distinct person and client that saved a version after from, up to to. diagram_history with compare returns the same over MCP.
Edits
An edit is one of: typed operations (ops), the whole diagram as edited Mermaid (mermaid, Mermaid scope), restoreRevision, or a new brief. Every edit carries baseRevision, the version you planned it against (without it the call returns 400), and a one-line message, kept with the version like a commit message (diagram_edit requires it; REST doesn’t). A new brief can go with ops or mermaid.
{
"baseRevision": 3,
"message": "Add the cache in front of the database",
"requestId": "6f1c2e7a-4b0d-4c55-9a51-2f1f8f7c9d10",
"ops": [{ "op": "createNode", "id": "cache", "nodeType": "database", "label": "Cache" }]
}
An edit is all or nothing. If an operation can’t apply, or the version it would make breaks a rule the diagram didn’t break before, the call returns 422 VALIDATION_FAILED with the errors and saves nothing. A saved edit returns warnings for the new version and changed: the elements it added, updated or removed, at most 20 IDs, with more counting the rest.
Conflicts
Zindex applies an edit only when baseRevision is the diagram’s current revision. If another writer got there first, the call returns 409 with code: "REVISION_CONFLICT" and changes nothing:
{
"error": "The diagram is at revision 4, not 2. Re-plan the edit against revision 4.",
"code": "REVISION_CONFLICT",
"revision": 4,
"changes": "dgm_DdfD8FaB6fox \"Payments v2\" r4 2026-10-06T00:08Z sha256:fdb666347f93\nkey: my-diagram\nsince r2: r3 curl/8.7.1 \"Rename the ledger\" | r4 curl/8.7.1 \"Rename the diagram\"\n~ scene: title\n~ db database \"Ledger DB\"\n- e1"
}
revision is the current revision. changes, when your base is older than the current revision, is what changed since it, as a read with knownRevision shows it: a header line, the versions since, then one line per change: + added, ~ changed and - removed, by ID; ~ scene: names changed scene fields. Re-plan a small edit against the current revision and send it with that baseRevision. Don’t replay the same ops without checking them, and don’t resend the stale baseRevision. There is no way to force an overwrite.
A stale baseRevision isn’t always a conflict: if applying your edit to it gives exactly the current content, most often because an earlier attempt of the same edit landed, the call returns 200 with noop: true.
Safe retries
A request can time out after Zindex has applied it. A retry is safe:
requestId. Send your own ID with each edit, such as a UUID: 1 to 255 visible ASCII characters, no spaces, unique per diagram. A retry with the samerequestIdreturns the first attempt’s result withreplayed: trueinstead of applying the edit twice. The samerequestIdwith a different edit returns 422,REQUEST_ID_REUSED, so give a re-planned edit a new one.- No change, no version. An edit whose result equals the current content returns
noop: true, with or without arequestId. - Keys.
POST /v1/diagramswith akeyand the same content as that diagram returns it, when you made it. Without akey, every call makes a new diagram.
A call answered BUSY found every drawing worker busy: wait retryAfter seconds and send it again. DRAW_TIMEOUT means drawing the scene ran past the drawing budget and nothing was saved, so don’t resend the same scene: draw fewer edges into one node, or split the diagram.
Going back
POST /v1/diagrams/my-diagram/revisions
{ "baseRevision": 7, "message": "Back to the March layout", "restoreRevision": 3 }
restoreRevision makes revision 8 equal to revision 3; revisions 4 to 7 stay readable. Give a revision number, or { "asOf": "2026-03-31" } for the version current at a date. It can’t be combined with ops, mermaid or brief. Restoring the content the diagram already has returns noop: true.
Layout
The engine places every node written without a layout, by the method of the diagram’s family. After the first save it keeps each node’s position in the node’s layout, unless a node that grows needs the room, a frame added or deleted round it would leave frames overlapping (below), or an order constraint you add finds its two nodes the wrong way round. New nodes are placed next to their neighbours, and edges are redrawn on every save. Send an autoLayout operation to lay out again; changing layoutStrategy or diagramFamily with updateScene does the same. Sequence diagrams are laid out from their messages on every save, so autoLayout doesn’t move them.
The engine places nodes by the graph’s structure, not by meaning. To keep related nodes together, put them in a frame without a layout: in architecture, workflow, entity-relationship and UI flow diagrams, and diagrams without a family, the engine lays a frame out with its members. A frame added round nodes already placed fits round them where they are. If it would then overlap another frame or take in another node, the engine places those nodes again, together, and the edit’s changed lists them. To put them on a particular side (frontend on the left, backend on the right), use order constraints or coordinates. Layout engine has how frames are laid out and what each family’s layout does.
Images
Each version’s image is drawn once, when the version is saved, and kept, so it never changes. GET /v1/images/{id}@2.svg returns revision 2’s image, and GET /v1/diagrams/{id}/render?revision=2 or ?asOf=2026-04-01 returns links to it. With your API key, add ?engine=current to the image path to draw the version with today’s engine instead; that image isn’t kept. A version saved before Zindex kept images gets one drawn the first time it is needed, marked backfilled.
The image’s address
Every image of a saved version prints where it lives, in its bottom-right corner, the same on every plan:
zindex.ai/d/dgm_7Q2kD9a1B2c3@5 · 2026-04-01
That is the version’s address, zindex.ai/d/{id}@{revision}, and the date the version was saved. Screenshots, pasted PNGs and slides keep it. The ID and revision in it open the version the image shows: GET /v1/diagrams/dgm_7Q2kD9a1B2c3?revision=5, or diagram_get with revision: 5. That needs a key or sign-in for the workspace that holds the diagram (another workspace’s answers NOT_FOUND), and diagram_edit changes the diagram if that access is write. A published diagram also reads with no key: GET /v1/public/diagrams/dgm_7Q2kD9a1B2c3@5 returns its scene. In a browser, https://zindex.ai/d/dgm_7Q2kD9a1B2c3@5 shows the version if the diagram is published. If it isn’t, the page offers to open it in Zindex, where the people in its workspace can see it. In an SVG opened on its own, the line is a link to that address.
- Narrow images. When the line doesn’t fit, the date goes first, then the address is drawn smaller, down to 7 px. It is never shortened: on an image too narrow even then, its start runs off the left edge and the ID and revision stay in view.
- Share links. A share link’s images print the diagram’s ID, not the link’s token. The share link keeps working. The address opens the diagram only for people in its workspace, or for anyone once it is published.
- Where it appears. Images from
GET /v1/images/{id}@{revision}.svgor.png, render links and version links carry it, and so do images drawn on request (?engine=currentor?from=). A preview of an unsaved scene never has it, and no option leaves it off a saved version. - Older images. An image never changes once saved, so versions saved before images carried their address keep the line they were drawn with,
dgm_7Q2kD9a1B2c3 | Rev 5 - 2026-04-01, pluszindex.aiin the bottom-left corner on images saved on Free. Their ID and revision open the version the same way.
Visual diff
Add ?from=1 to a version’s image, with your API key, to draw it with what changed since revision 1 colored: added elements in green, removed as red dashed ghosts, changed in amber, the rest muted, with a legend.
GET /v1/images/{id}@3.svg?from=1
Render links, version links and diagram_render always draw the plain version. See Rendering: Visual diff.
Sharing
A diagram’s Share section has one control, Who can see it, with three choices. Owners and members change it; viewers see only who can see it. People in the diagram’s workspace can always see it, whichever is chosen.
| Only you | Anyone with the link | Public | |
|---|---|---|---|
| Who can see it | Its workspace | Its workspace, and anyone with a share link | Anyone with its address |
| Address | zindex.ai/d/{id}, for the workspace only | zindex.ai/d/{token}, one per link | zindex.ai/d/{id} |
| What a visitor sees | Nothing | Images and dates | Images, dates, title and description |
| The scene JSON | Not shared | Not shared | Anyone, with no key |
| Turning it off | Nothing to turn off | Revoke a link | Choose one of the others |
| Plans | Every plan | Pro and Team | Every plan |
| Content reports | No | Anyone with a link can report it | Anyone can report it |
| Caching | Browser only | Browser only | Other caches too |
What each means:
- Only you. In a team workspace it reads Only people in and the workspace’s name, such as Only people in Acme.
zindex.ai/d/{id}says “diagram not available”, except to people in the workspace who are signed in, whom it takes to the diagram. Nobody can file a content report on it, people in the workspace included: a report needs a published diagram or a live share link. - Anyone with the link. Each share link has its own random token. Its page, titled “Shared diagram”, shows every version’s image and the date it was saved, never the title, description or scene JSON. The diagram’s own address, which its images print, still answers as for Only you. Anyone who has a link can report the diagram from its page for breaking the content policy. Moderators then see what that link shows and no more, while the report is open. If the diagram breaks the policy they revoke that link only and email the person who made it, or the workspace’s owners if that person has left it.
- Public.
zindex.ai/d/{id}is the address every image prints. Anyone can open it, see the title and description, and read the scene JSON with no key (GET /v1/public/diagrams/{id}), so any agent can load or copy the diagram. Anyone, signed in or not, can report it for breaking the content policy; if it does, moderators make it private and revoke every share link to it. Choosing one of the others stops its address and scene JSON at once: Anyone with the link keeps its share links, and Only you revokes them.
Whatever is chosen:
- A 7-day image link (Copy 7-day image link,
diagram_render, orGET /v1/diagrams/{id}/render) shows that version’s image to anyone who has it until it expires. A published diagram’s render links are its version links instead. - Sending a version to Zindex support (Report a problem) works on every diagram.
For both kinds of link:
- A link has no expiry: a share link works until it’s revoked, and a public address until the diagram is made private. An exact version’s image never changes, and a link that follows the latest version shows the newest one.
- Pasted into Slack, Teams or iMessage, a link previews the diagram’s image.
- Search engines are asked not to index these pages.
- Copies people downloaded, and previews already posted, stay with them after a link stops working.
- A team workspace’s owners can turn public sharing off. Then nobody can publish its diagrams or make new share links; what’s already shared stays shared until it’s made private or revoked.
Changing it:
- To Public asks you to confirm first, because anyone can then read the scene JSON.
- From Public to Anyone with the link makes the diagram’s own address private and keeps its share links, or makes one if it has none (Pro and Team).
- To Only you makes it private and, when it has share links, asks you to confirm, then revokes them all.
Agents make these changes a step at a time: diagram_share with publish, unpublish (share links stay), link and unlink (revokes every link), so Only you from Public is unpublish then unlink. Over REST, PUT /v1/diagrams/{id}/sharing publishes or makes private, POST /v1/diagrams/{id}/links makes a link and DELETE /v1/diagrams/{id}/links/{linkId} revokes one (Share links, Published diagrams).
Verifying
Every version has two hashes, each shown as sha256: and 64 lowercase hex digits:
hash, the content hash, is the SHA-256 of the version’s scene in its RFC 8785 canonical form. The scene is the oneview=dspand the export return.chain, the chain hash, is the SHA-256 of the content hash, the hash of the version’s stored image and the chain hash of the version before it. Each chain hash covers the one before, so changing any scene or image changes the chain hash of its version and of every version after it.
Saves and edits return both, and so do reads with view=dsp or view=mermaid, renders and the export. The history shows each version’s chain. chain is null until Zindex has computed it, normally when the version is saved; a render, an export or the next edit computes a missing one. Reads and the history never compute it.
The chain hash of version n is:
chain(n) = SHA-256("zindex-chain/1" + "\n" + chain(p) + "\n" + content(n) + "\n" + image(n))
p is the version before n in the history, content(n) is n’s content hash and image(n) is the SHA-256 of n’s stored SVG. All three hashes are lowercase hex without the sha256: prefix, and the whole string is hashed as UTF-8. The chain starts at the history’s first version, whose chain(p) is the empty string. That is version 1, unless an earlier retention limit deleted the diagram’s oldest versions; then the history’s note says which version the chain starts at.
The chain covers each version’s scene and image. It doesn’t cover at, client or message. It shows whether any version’s scene or image has changed since you last checked, and it can’t prove that the first copy you see is the one Zindex saved.
Checking an export
GET /v1/diagrams/{id}/export holds everything the check needs, so you can run it without Zindex. Its header has "chain": "zindex-chain/1". Then, for each version line in order:
- Canonicalize
scenewith RFC 8785 and take the SHA-256 of the result. It must equalhash. - Take the SHA-256 of the UTF-8 bytes of
image.svg. It must equalimage.hash.GET /v1/images/{id}@{revision}.svgserves the same bytes. - Compute the chain hash from the previous line’s chain hash, this line’s content hash and its image hash, and check that it equals
chain. For the first line, the previous chain hash is the empty string.
When every line passes, keep the last chain. If a later export passes the same check and shows the same chain for that version, no version’s scene or image up to it has changed.
The workspace export, GET /v1/workspace/export, holds each diagram’s export in turn, and a line with "format": "zindex-history/1" starts each one. Check each diagram on its own, as above. Its last line, such as {"end":true,"diagrams":2}, must be there and count every diagram; without it the file was cut short, so download it again.
In Python, with the rfc8785 package, for either export. parse_int=float reads every number as a double, as JavaScript does, so an integer above 2^53 hashes the same as on Zindex’s side:
import hashlib, json, sys
import rfc8785 # pip install rfc8785
def sha256(data: bytes) -> str:
return hashlib.sha256(data).hexdigest()
def check(header, versions):
assert header["chain"] == "zindex-chain/1"
previous = ""
for v in versions:
where = f"{header['id']} revision {v['revision']:.0f}"
content = sha256(rfc8785.dumps(v["scene"]))
image = sha256(v["image"]["svg"].encode("utf-8"))
chain = sha256(f"zindex-chain/1\n{previous}\n{content}\n{image}".encode("utf-8"))
assert v["hash"] == "sha256:" + content, f"content of {where}"
assert v["image"]["hash"] == "sha256:" + image, f"image of {where}"
assert v["chain"] == "sha256:" + chain, f"chain of {where}"
previous = chain
print(f"{header['id']}: revisions {versions[0]['revision']:.0f} to {versions[-1]['revision']:.0f} check out; chain sha256:{previous}")
with open(sys.argv[1], encoding="utf-8") as f:
lines = [json.loads(line, parse_int=float) for line in f]
if lines[0]["format"] == "zindex-account-history/1":
assert lines[-1].get("end"), "the file was cut short"
starts = [i for i, line in enumerate(lines) if line.get("format") == "zindex-history/1"]
assert len(starts) == lines[-1]["diagrams"], "a diagram is missing"
for start, stop in zip(starts, starts[1:] + [len(lines) - 1]):
check(lines[start], lines[start + 1:stop])
else:
check(lines[0], lines[1:])
Test vectors
Two versions, with a tiny scene and SVG standing in for real ones. Each scene is shown in its RFC 8785 form; version 1’s chain hash starts from the empty string, and version 2’s from version 1’s.
version 1
scene {"elements":[],"scene":{"title":"One"},"schemaVersion":"0.1"}
content da7672a47c5015fe141b59e471b59686c4a5fd8acd23cf7af89cb4948c96ff48
svg <svg xmlns="http://www.w3.org/2000/svg"/>
image 900fbe934249ad120004bd24adf66aad8817d89586273c0cc50e187bddebb601
chain 9dbc07dfb1594014ee35f93f18c1b41b1c419bd763675405901d09cbf06be562
version 2
scene {"elements":[],"scene":{"title":"Two"},"schemaVersion":"0.1"}
content 50220ed0ea1ae83c1e62557bea99f5317a7eca1de04bcb4ded7a994f7145cfc5
svg <svg xmlns="http://www.w3.org/2000/svg" width="2"/>
image 194b510093e90fefe62ea40c8479fbdce46469d60609a176e0107fb47efb9309
chain cc0f24d6dd354e155924d7c198e4c66e8d34ad8b477a75c0bdbf3736d1331251