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:
archivedandtemplatecarry noomitemptyon purpose, and that is a contract rather than an accident. A client that reads a missingtemplateas “this document has no type” will striptemplate: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.
Links
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).