Documentation

Endpoint Reference

Complete reference for all Meldoc REST API endpoints.

Every request requires two headers:

X-Cli-Secret — your integration token (mdc_...).

X-Project-Alias — the project alias to operate on.

Write operations (POST, DELETE) additionally require Content-Type: application/json unless otherwise noted.

Documents

GET /api/v1/tree

Get a full snapshot of the document tree for a project.

Role required: Read

curl -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     https://api.meldoc.io/api/v1/tree

Response:

{
  "cursor": "2026-03-25T12:00:00Z",
  "docs": [
    {
      "id": "a1b2c3d4-...",
      "title": "Getting Started",
      "alias": "getting-started",
      "parentAlias": "",
      "order": 0,
      "is_virtual": false,
      "workflow": "published",
      "visibility": "visible",
      "exposure": "inherit",
      "content": "Document content in markdown...",
      "assets": []
    }
  ]
}
Field Type Description
cursor string Timestamp cursor for incremental sync
docs array All documents in the project
docs[].id string Document UUID
docs[].title string Document title
docs[].alias string Unique document alias
docs[].parentAlias string Parent document alias (empty for root documents)
docs[].order integer Position within parent
docs[].is_virtual boolean Whether this is a virtual (auto-created) parent
docs[].workflow string draft or published
docs[].visibility string visible or hidden
docs[].exposure string inherit, private, unlisted, or public
docs[].content string Document content in markdown
docs[].assets array Content-addressable asset references
docs[].archived boolean Always present. An absent key means a server that does not know about the archive axis — not “not archived”
docs[].archivedAt string When it was archived; absent when it is not
docs[].replacedBy string Successor alias; absent when none
docs[].template string | null Always present, null for a document with no type
docs[].formerAliases array Names this document still answers to — see Renaming & Redirects
docs[].last_sync, docs[].server_version string Sync bookkeeping the CLI writes back into front matter

If you are writing your own sync: archived and template carry no omitempty on purpose, and that is a contract rather than an accident. A client that reads a missing template as “this document has no type” will strip template: from every file it owns the moment it does a forced pull against a server that simply did not fill the key.


GET /api/v1/doc

Get a single document by alias.

Role required: Read

Query parameters:

Parameter Required Description
alias Yes Document alias
include_content No true (default) or false
curl -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     "https://api.meldoc.io/api/v1/doc?alias=getting-started"

Response:

{
  "id": "a1b2c3d4-...",
  "alias": "getting-started",
  "title": "Getting Started",
  "hash": "abc123def456...",
  "updated_at": "2026-03-25T10:30:00Z",
  "content": "Document content...",
  "parent_alias": "",
  "order": 0,
  "workflow": "published",
  "visibility": "visible",
  "exposure": "inherit",
  "assets": []
}
Field Type Description
hash string SHA-256 hash of the content (for delta sync)
updated_at string Last update timestamp (RFC 3339)

Returns 404 if the document does not exist.


POST /api/v1/docs/batch

Get multiple documents by alias with optional delta sync. Useful for fetching only documents that changed since your last sync.

Role required: Read

Limit: 500 documents per request.

curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{
       "docs": [
         { "alias": "getting-started", "hash": "abc123..." },
         { "alias": "api-guide" }
       ],
       "include_content": true
     }' \
     https://api.meldoc.io/api/v1/docs/batch

Request body:

Field Type Description
docs array Documents to fetch
docs[].alias string Document alias
docs[].hash string (Optional) Your current hash — if it matches the server’s, the doc is returned in unchanged
include_content boolean Whether to include document content
updated_since string (Optional) RFC 3339 timestamp — only return documents updated after this time

Response:

{
  "changed": [
    {
      "id": "e5f6g7h8-...",
      "alias": "api-guide",
      "title": "API Guide",
      "hash": "def789...",
      "updated_at": "2026-03-25T11:00:00Z",
      "content": "...",
      "order": 1,
      "workflow": "draft",
      "visibility": "visible",
      "exposure": "inherit"
    }
  ],
  "unchanged": ["getting-started"],
  "missing": []
}
Field Type Description
changed array Documents that are new or have a different hash
unchanged array Aliases where the hash matched (content not re-sent)
missing array Aliases that were not found on the server

POST /api/v1/pull

Incremental pull — get documents that changed since a given cursor. Use this for ongoing synchronization.

Role required: Read

curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{ "cursor": "2026-03-20T00:00:00Z" }' \
     https://api.meldoc.io/api/v1/pull

Request body:

Field Type Description
cursor string (Optional) Cursor from a previous pull or tree response. Omit for a full pull.
force string (Optional) Set to "true" to ignore the cursor and pull everything

Response:

{
  "changes": [
    {
      "op": "upsert",
      "id": "a1b2c3d4-...",
      "title": "Getting Started",
      "alias": "getting-started",
      "order": 0,
      "workflow": "published",
      "visibility": "visible",
      "exposure": "inherit",
      "content": "Updated content..."
    },
    {
      "op": "delete",
      "id": "x9y8z7w6-..."
    }
  ],
  "new_cursor": "2026-03-25T12:00:00Z"
}
Field Type Description
changes array List of changes since the cursor
changes[].op string upsert (created or updated) or delete
new_cursor string Use this cursor in the next pull request

POST /api/v1/push

Create or update documents.

Role required: Write

curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{
       "files": [
         {
           "path": "docs/hello.meldoc.md",
           "alias": "hello",
           "title": "Hello World",
           "content": "Document content here.",
           "sha256": "e3b0c44298fc1c14..."
         }
       ]
     }' \
     https://api.meldoc.io/api/v1/push

Request body:

Field Type Description
files array Documents to push
files[].path string File path (used for display and conflict resolution)
files[].alias string Document alias
files[].parentAlias string (Optional) Parent document alias
files[].title string Document title
files[].content string Document content in markdown
files[].sha256 string SHA-256 hash of the content
files[].order integer (Optional) Position within parent
files[].workflow string (Optional) draft or published
files[].visibility string (Optional) visible or hidden
files[].exposure string (Optional) inherit, private, unlisted, or public
files[].assets array (Optional) Asset references for this document

Response:

{
  "results": [
    {
      "alias": "hello",
      "path": "docs/hello.meldoc.md",
      "id": "f9a8b7c6-...",
      "title": "Hello World",
      "order": 0,
      "status": "created"
    }
  ],
  "server_version": "1.0.1",
  "skipped": []
}
Field Type Description
results[].status string created, updated, synced, conflict, or skipped
results[].aliasChanged boolean true if the alias was modified to avoid a conflict
skipped array Items skipped due to insufficient permissions, or because their parent would place the document inside its own subtree
skipped[].reason string no_create_permission, no_update_permission, no_delete_permission, or parent_cycle

DELETE /api/v1/docs

Delete multiple documents by alias or ID.

Role required: Write and Delete (maintain)

Limit: 500 documents per request.

curl -X DELETE \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{
       "docs": [
         { "alias": "old-page" },
         { "id": "a1b2c3d4-..." }
       ]
     }' \
     https://api.meldoc.io/api/v1/docs

Request body:

Field Type Description
docs array Documents to delete (at least one required)
docs[].alias string Document alias (provide alias or id)
docs[].id string Document UUID (provide alias or id)

Response:

{
  "results": [
    { "alias": "old-page", "id": "x1y2z3-...", "status": "deleted" },
    { "id": "a1b2c3d4-...", "status": "not_found" }
  ],
  "deleted_count": 1,
  "not_found_count": 1,
  "skipped_count": 0
}
Field Type Description
results[].status string deleted, not_found, or skipped
results[].reason string Reason for skipping (e.g., no_delete_permission)

POST /api/v1/docs/archive

Archive a document and every live document nested under it. This is what meldoc archive calls.

Role required: Write (write)

curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{ "alias": "old-runbook", "replacedBy": "new-runbook" }' \
     https://api.meldoc.io/api/v1/docs/archive

Request body:

Field Type Description
alias string Document alias — provide alias or id
id string Document UUID — provide alias or id
replacedBy string Successor. Three-valued: omit the key to leave the current successor alone, send "" to clear it, send an alias or id to set it
dryRun boolean Return the count the write would produce, and write nothing

Response:

{
  "id": "x1y2z3-...",
  "alias": "old-runbook",
  "archivedCount": 4,
  "replacedBy": "new-runbook"
}

archivedCount is the number of documents the call actually changed — the page plus its live descendants. A document already archived contributes nothing to it, so a second identical call answers 0 rather than repeating the first count.


POST /api/v1/docs/unarchive

Take one document back out of the archive. No cascade — unlike archiving, this does not touch the document’s children, because each was archived for its own reason.

Role required: Write (write)

Same request body as archive, same response shape.


GET /api/v1/templates

List the project’s document types. This is what meldoc templates list reads.

Role required: Read

curl -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     https://api.meldoc.io/api/v1/templates

Response:

[
  {
    "id": "t1t2t3-...",
    "projectId": "p1p2p3-...",
    "alias": "runbook",
    "title": "Runbook",
    "description": "Operational procedure",
    "icon": "book",
    "fieldIds": ["f1...", "f2..."]
  }
]

fieldIds is never null — a type with no fields serializes as []. It is ordered, and the order is the type’s field order: position 0 renders first. There is deliberately no second key carrying a position number, because a set and an order in two keys can disagree and then nothing says which one the server stored.


POST /api/v1/links

Process document links (magic links between documents). Typically called after a push to update link relationships.

Role required: Write

curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{
       "links": [
         {
           "from_doc_id": "a1b2c3d4-...",
           "from_doc_alias": "getting-started",
           "kind": "alias",
           "target_doc_alias": "api-guide",
           "raw": "[[api-guide]]",
           "display_text": "API Guide"
         }
       ]
     }' \
     https://api.meldoc.io/api/v1/links

Link fields:

Field Type Description
from_doc_id string Source document UUID
from_doc_alias string Source document alias
kind string path, alias, project_alias, or external
target_doc_alias string Target document alias
target_project_alias string (Optional) Target project for cross-project links
raw string The original link markup, e.g. [[api-guide]]
display_text string Link label shown in the document

Response:

{
  "message": "links processed successfully",
  "processed": 1
}

Assets

POST /api/v1/assets/exists

Check which assets already exist on the server by SHA-256 hash. Use this before uploading to avoid re-uploading files that are already stored.

Role required: Read

curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{
       "sha256_list": [
         "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
         "abc123def456..."
       ]
     }' \
     https://api.meldoc.io/api/v1/assets/exists

Request body:

Field Type Description
sha256_list array SHA-256 hashes to check (each must be 64 hex characters, max 1000 per request)

Response:

{
  "existing": {
    "e3b0c44298fc1c14...": {
      "asset_id": "b1c2d3e4-...",
      "sha256": "e3b0c44298fc1c14...",
      "size_bytes": 24576,
      "mime": "image/png",
      "original_name": "screenshot.png"
    }
  }
}

Hashes not found on the server are omitted from the existing map.


POST /api/v1/assets/upload

Upload an asset file. Uses multipart form data (not JSON).

Role required: Write

curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -F "[email protected]" \
     -F "sha256=e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855" \
     -F "original_name=screenshot.png" \
     https://api.meldoc.io/api/v1/assets/upload

Form fields:

Field Required Description
file Yes The file to upload
sha256 Yes SHA-256 hash of the file (64 hex characters)
original_name No Original filename
doc_id No Document UUID for combined upload + bind
logical_path No Logical path for binding (required if doc_id is set)

Response:

{
  "asset": {
    "asset_id": "b1c2d3e4-...",
    "sha256": "e3b0c44298fc1c14...",
    "size_bytes": 24576,
    "mime": "image/png",
    "original_name": "screenshot.png"
  }
}

The server verifies the SHA-256 hash and rejects uploads where the hash does not match the file content. Files over the upload limit (10 MB by default) are rejected. When an upload is skipped because the workspace storage limit is exceeded, the response is { "skipped": true, "reason": "...", "message": "..." } and asset is omitted.


POST /api/v1/docs/:docId/assets/bindings

Bind assets to a document. Creates or updates the logical path mappings that connect assets to documents. Each item needs a logical_path and the asset’s sha256 (64 hex characters).

Role required: Write

Path parameters:

Parameter Description
docId Document UUID
curl -X POST \
     -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     -H "Content-Type: application/json" \
     -d '{
       "upsert": [
         {
           "logical_path": "assets/screenshot.png",
           "sha256": "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"
         }
       ]
     }' \
     "https://api.meldoc.io/api/v1/docs/a1b2c3d4-.../assets/bindings"

Response:

{
  "updated": 1
}

updated is the number of bindings created or updated.


GET /api/v1/docs/:docId/assets

List all asset bindings for a document.

Role required: Read

Path parameters:

Parameter Description
docId Document UUID
curl -H "X-Cli-Secret: $TOKEN" \
     -H "X-Project-Alias: my-project" \
     "https://api.meldoc.io/api/v1/docs/a1b2c3d4-.../assets"

Response:

{
  "assets": [
    {
      "logical_path": "assets/screenshot.png",
      "sha256": "e3b0c44298fc1c14...",
      "size": 24576,
      "mime_type": "image/png",
      "original_name": "screenshot.png"
    }
  ]
}

What’s next?

API Quick Start — Quick start guide with working examples.

Integration Tokens — Create and manage tokens.

Meldoc CLI — Meldoc CLI (uses the same API under the hood).