API reference

Publish, update, and read documents and highlight sets over HTTP. This is the same API Marked uses.

Base URL and credentials

https://share.markedapp.com/api/v1

Published document links use markedb.in, but every API request goes to share.markedapp.com. Create credentials in Settings: Create API token issues a token on its own, while Connect an app issues a token and a device key together. Both are shown once.

HeaderValueRequired for
Authorization Bearer <api_token> Everything except reading a public document or its highlight sets
X-Device-Key <device_key> Creating, updating, and deleting documents, and captures
If-Match <content_hash> Updating or deleting a document

The device key is what makes conflict detection possible: it lets the server tell you which machine published the version you are about to overwrite.

Connecting an app

Open the connect page with a custom-scheme return_to so credentials return to the app instead of being copied by hand. The URL needs a host (myapp://auth/callback, not myapp:login). http, https, and the Marked 2 x-marked: scheme are rejected. Marked still uses x-marked-3://login.

https://share.markedapp.com/auth/marked?return_to=myapp://auth/callback&label=Read%20Later

After the user confirms, the page launches that callback with api_token, device_key, api_base, and label in the query. The same values stay on the page with an Open app button if the OS prompt is dismissed.

Errors

StatusBodyMeaning
400 {"error":"device_key_required"} The request writes a document but sent no device key.
401 {"error":"unauthorized"} Missing, malformed, or revoked token.
404 {"error":"not_found"} No such document, or you are not allowed to see it. Private, expired, and password-protected documents look identical to missing ones.
409 {"error":"conflict", …} The If-Match hash is stale. The body carries the current hash.
413 {"error":"index_too_large"} A view=index request covered more than 5,000 documents or tombstones.
422 {"error":"…"} Validation failed, for example a blank body or an archive with no Markdown in it.
429 empty Too many creates from one IP address in a minute. With an Authorization header, POST /documents allows 120 per minute and POST /documents/import allows 30; without one, each allows 10. Wait and retry.

Documents

A document can be addressed by its public id, its short share code, or its private id (a UUID). Marked stores the private id, since it stays valid regardless of visibility changes.

get /documents

Your documents, most recently updated first, including private ones. Requires a token but no device key. Each entry is the full document JSON below, Markdown body included.

Results are paged: limit defaults to 50 and caps at 200, and page starts at 1. Unusable values fall back to the default rather than erroring. The response stays a plain array, with paging details in the headers — X-Total-Count holds the full count, and Link carries rel="next" and rel="prev" URLs when there are more pages.

curl -i "https://share.markedapp.com/api/v1/documents?limit=25" \
  --header "Authorization: Bearer $MARKED_API_TOKEN"

X-Total-Count: 138
Link: <https://share.markedapp.com/api/v1/documents?limit=25&page=2>; rel="next"

Pass q to search titles and bodies with the same fuzzy full-text ranking as the web library. Search hits omit body_markdown by default and include a short snippet instead. Add include=body when you need the Markdown in the list response. Whitespace-only q is ignored (normal list). Paging (limit, page, X-Total-Count, Link) is unchanged; Link URLs keep q (and include when set).

curl -i "https://share.markedapp.com/api/v1/documents?q=kubernetes&limit=25" \
  --header "Authorization: Bearer $MARKED_API_TOKEN"

Pass folder to list documents in that library folder and its subfolders (the same scope as the web library). The value is a folder path such as Work or Work/Client A. A missing folder returns 404. A known empty folder returns an empty array and X-Total-Count: 0. q still searches inside that scope. Link URLs keep folder when it is set. Omit folder (or send a blank value) to list the whole library.

curl -i "https://share.markedapp.com/api/v1/documents?folder=Work&limit=25" \
  --header "Authorization: Bearer $MARKED_API_TOKEN"

get /documents?view=index

Lean sync index of your library. Optional folder uses the same prefix as the normal list (unknown folder is 404). Token only. Not paged. q and include=body are ignored. Over 5,000 live documents or tombstones in scope is 413 index_too_large.

{
  "documents": [
    {
      "id": "abc123",
      "library_path": "Work/Note",
      "content_hash": "…",
      "updated_at": "2026-09-23T12:00:00.000Z"
    }
  ],
  "tombstones": [
    {
      "id": "old456",
      "library_path": "Work/Gone",
      "deleted_at": "2026-09-22T09:00:00.000Z"
    }
  ]
}

Building a document picker? Use ?q= to search, or list without q for the full menu, then download /documents/:id/textpack for the one the reader chooses. The Markdown in this JSON points at images hosted on the server, while the TextPack bundles them for a self-contained import. title can be null, so supply your own "Untitled" fallback.

get /documents/:id

One document with its Markdown. Public and unlisted documents need no token; private ones need yours. Expired and password-protected documents return 404 to everyone but their owner.

curl https://share.markedapp.com/api/v1/documents/$DOCUMENT_ID \
  --header "Authorization: Bearer $MARKED_API_TOKEN"
{
  "id": "abc123",
  "private_id": "8f2c…",
  "url": "https://markedb.in/x7k2m",
  "content_hash": "9b1e…",
  "visibility": "unlisted",
  "reading_style": "swiss",
  "created_at": "2026-08-20T09:14:02.000Z",
  "updated_at": "2026-08-25T12:00:00.000Z",
  "title": "Shared from Marked",
  "body_markdown": "# Hello from Marked",
  "allow_remix": false
}

created_at and updated_at are ISO 8601 timestamps in UTC with millisecond precision, and both come back on every document response, including list and create.

reading_style is the typography the author published — editorial, manuscript, swiss, contrast, typewriter — or null for no published style, which readers see as Editorial. It comes back on every document response, and an unknown value is rejected with 422. See sharing for what each style looks like.

get /documents/:id/textpack

A TextBundle archive with Markdown and embedded images — the canonical import download for connected apps. Accepts a public id, private id, or share code in :id. Same visibility rules as the JSON endpoint above: bearer token required for private documents you own; public and unlisted documents work without a token. No device key. Add ?hl=<set_id> to include that highlight set as highlights.json.

curl "https://share.markedapp.com/api/v1/documents/$PRIVATE_ID/textpack" \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --output MyDoc.textpack

post /documents

Creates a document and returns 201 with its id, private_id, url, content_hash, visibility, created_at, and updated_at. Store the private id and hash for later updates.

Send JSON when you only have Markdown:

curl https://share.markedapp.com/api/v1/documents \
  --request POST \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "X-Device-Key: $MARKED_DEVICE_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "document": {
      "title": "Shared from Marked",
      "body_markdown": "# Hello from Marked",
      "visibility": "unlisted",
      "allow_remix": false
    }
  }'

Accepted fields are title, body_markdown, visibility, allow_remix, apex_mode, share_slug, reading_style, published_at, library_path, and the collection fields collection_id, collection_slug, or collection_ids. Visibility defaults to private. A body is required and is capped at 500,000 bytes.

library_path files the document in your library. Work/Notes means folder Work; the leaf is always taken from the title, and a taken name gets a number. A value with no slash counts as a folder only if that folder already exists; otherwise the document goes to the root. Omit it to file at the root.

TextPack uploads

Send multipart/form-data with a file field to publish a document that has images. The archive is a zipped TextBundle (.textpack) or any .zip containing a Markdown file. Images are stored as document assets and the Markdown image paths are rewritten to hosted URLs. A highlights.json in Marked's format is imported as your own highlight set.

An info.json in the archive can publish the reading style, either under the com.markedapp.share namespace or as a bare readingStyle key. A document[reading_style] form field overrides it, and an unknown style is ignored. Downloaded TextPacks carry the same file, so a round trip keeps the style.

{
  "version": 2,
  "type": "net.daringfireball.markdown",
  "transient": false,
  "creatorIdentifier": "com.markedapp.share",
  "com.markedapp.share": {
    "documentId": "abc123",
    "url": "https://markedb.in/x7k2m",
    "readingStyle": "swiss"
  }
}
curl https://share.markedapp.com/api/v1/documents \
  --request POST \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "X-Device-Key: $MARKED_DEVICE_KEY" \
  --form "file=@./MyDoc.textpack" \
  --form "document[visibility]=unlisted" \
  --form "document[allow_remix]=false"

post /documents/import

Creates up to 200 documents from Markdown in one request, for seeding a synced folder. Requires a token and a device key. JSON only; use single TextPack creates for documents with images.

curl https://share.markedapp.com/api/v1/documents/import \
  --request POST \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "X-Device-Key: $MARKED_DEVICE_KEY" \
  --header "Content-Type: application/json" \
  --data '{
    "documents": [
      { "title": "Meeting notes", "body_markdown": "# Meeting notes", "library_path": "Work" },
      { "title": "Spec", "body_markdown": "# Spec", "library_path": "Work/Client A" }
    ]
  }'

Each item takes the same fields as a JSON create. Here library_path is always a folder: missing folders are created, a blank value files the document at the root, and the leaf comes from the title (duplicate titles are numbered). Items are created in order, each on its own, so one bad item does not stop the rest. The response lists what was created, by the item's position in your array, plus any per-item errors:

{
  "created": [
    { "index": 0, "id": "abc123", "private_id": "8f2c…", "content_hash": "9b1e…", "library_path": "Work/Meeting notes", … }
  ],
  "errors": [
    { "index": 1, "error": "Body markdown can't be blank" }
  ]
}

The status is 200 when at least one item was created and 422 when every item failed. A missing or empty documents array is 422 documents_required; more than 200 documents is 422 import_too_large. Created rows carry the same fields as a single create, so store each private_id and content_hash against the local file. Imported documents render their HTML in the background; the Markdown, hash, and sync index are available immediately.

patch /documents/:id

Replaces the document. Owner only, and If-Match must carry the content_hash you last saw. Accepts the same JSON or multipart shapes as create.

curl "https://share.markedapp.com/api/v1/documents/$PRIVATE_ID" \
  --request PATCH \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "X-Device-Key: $MARKED_DEVICE_KEY" \
  --header "If-Match: $CONTENT_HASH" \
  --form "file=@./MyDoc.textpack"

A missing or stale hash is a conflict rather than an overwrite:

{
  "error": "conflict",
  "content_hash": "current-hash",
  "updated_by_device_label": "Marked on Sisyphus"
}

Recover by fetching the document (or reading the hash out of the 409 body), deciding whether to keep the server's version, and retrying with the current hash. Each body change also records a version snapshot in the document's history.

delete /documents/:id

Permanently deletes an owned document. Requires a token, device key, and If-Match with the current content_hash. Success is 204 with no body. A tombstone (id and last path) is recorded so sync clients can remove the local file. Missing or stale If-Match is the same 409 as update. A second delete of the same id is 404.

curl "https://share.markedapp.com/api/v1/documents/$DOCUMENT_ID" \
  --request DELETE \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "X-Device-Key: $MARKED_DEVICE_KEY" \
  --header "If-Match: $CONTENT_HASH"

Folders

Library folders are path strings (Work, Work/Client A). Root is not a folder row. Empty folders are included. Compatible apps use this list as a sync picker, then create a path if it does not exist yet.

get /folders

Your folder paths, sorted case-insensitively, including empty folders. Requires a token but no device key.

curl https://share.markedapp.com/api/v1/folders \
  --header "Authorization: Bearer $MARKED_API_TOKEN"

[
  { "path": "Inbox", "locked": false },
  { "path": "Journal", "locked": true },
  { "path": "Work", "locked": false },
  { "path": "Work/Client A", "locked": false }
]

locked is true when the folder or any parent folder is locked from the web library. Documents in a locked folder are always private: creating, updating, or importing one with visibility set to public or unlisted succeeds but stores it as private, so read visibility from the response. Locking and unlocking are only available on the web.

post /folders

Create a folder by full path. Missing ancestors are created. Requires a token and a device key. Returns 201 when the path is new, or 200 if it already exists.

curl https://share.markedapp.com/api/v1/folders \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "X-Device-Key: $MARKED_DEVICE_KEY" \
  --header "Content-Type: application/json" \
  --data '{"folder":{"path":"Work/Client A"}}'

Folder sync

Compatible apps can keep a local folder paired with a Share prefix. Seed the files the server does not have yet with POST /documents/import, up to 200 documents per request, and record each returned id and content_hash. After that, GET /documents?view=index&folder=Work is the snapshot: apply tombstones first, then pull or skip each live row by id and content_hash. An id you know that is in neither documents nor tombstones was moved outside the folder, not deleted; GET /documents/:id returns its new library_path. Push with POST/PATCH as today (library_path is the folder only). Local deletes use DELETE with If-Match. A 409 means the server moved on — pull, do not overwrite or delete. If you still want the rejected local body, publish it as a new document (for example a title ending in (conflicted)). Share does not create that copy.

Captures

post /captures

Turns a web page into a private document, re-hosting its images and recording the page as the permanent source_url. Needs a token and a device key.

curl https://share.markedapp.com/api/v1/captures \
  --request POST \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "X-Device-Key: $MARKED_DEVICE_KEY" \
  --header "Content-Type: application/json" \
  --data '{"url":"https://example.com/article"}'

Returns 201 with id, private_id, url, content_hash, source_url, visibility, created_at, and updated_at. A missing URL returns 422 with {"error":"url_required"}.

Highlight sets

Each set is one reader's highlights and notes on a document. See Highlights and reviews for the concepts behind them. Reading requires only that you can read the document; writing requires a token.

get /documents/:id/highlight_sets

Every set on the document, newest first, without the highlight payloads.

[
  {
    "id": "hl_abc123",
    "document_id": "abc123",
    "url": "https://markedb.in/x7k2m?hl=hl_abc123",
    "label": "Anonymous reviewer",
    "highlight_count": 12,
    "note_count": 3,
    "updated_at": "2026-08-25T12:00:00.000Z",
    "owner": false,
    "parent_set_id": null,
    "content_signature": "4c9f…"
  }
]
FieldMeaning
labelAccount display name, else the reviewer's signature, else "Anonymous reviewer".
ownertrue when the set belongs to the token's user. Filter these out to list other people's reviews.
parent_set_idSet this one was forked from, if any.
content_signatureFingerprint of the whole set. Compare it to detect changes without downloading the payload.

get /documents/:id/highlight_sets/:hl_id

The same fields plus payload, which holds the highlights themselves.

{
  "id": "hl_abc123",
  "payload": {
    "version": 1,
    "highlights": [
      {
        "id": "hl_9f21",
        "kind": "highlight",
        "text": "the passage that was highlighted",
        "color": "yellow",
        "note": "needs a citation",
        "signature": "ED",
        "fingerprint": "b3d1…",
        "range": { "start": {}, "end": {} }
      }
    ]
  }
}

kind is highlight or comment. note is the reader's note (also mirrored as comment for Marked's format), signature is the label the reader typed, and fingerprint identifies the passage so the same highlight can be recognised across sets. Anchoring fields (range, marked_anchor, prefix, suffix) may also be present.

post /documents/:id/highlight_sets

Creates or replaces the token user's own set on that document. Send a highlights array; pass parent_set_id to record that you started from someone else's set.

curl https://share.markedapp.com/api/v1/documents/$DOCUMENT_ID/highlight_sets \
  --request POST \
  --header "Authorization: Bearer $MARKED_API_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{
    "highlights": [
      { "kind": "highlight", "text": "a passage", "color": "yellow", "note": "nice" }
    ]
  }'

patch /documents/:id/highlight_sets/:hl_id

Replaces the highlights in a set you own.

post /documents/:id/highlight_sets/:hl_id/import

Merges someone else's set into your own, deduplicating by fingerprint and leaving their set untouched. Document owners only. Returns your set with its payload.

curl https://share.markedapp.com/api/v1/documents/$DOCUMENT_ID/highlight_sets/$REVIEW_ID/import \
  --request POST \
  --header "Authorization: Bearer $MARKED_API_TOKEN"

Downloads and change checks

Browser-friendly exports live on the document host rather than under /api/v1. Connected apps should prefer GET /api/v1/documents/:id/textpack (above) so bearer tokens unlock private documents; these URLs follow the same visibility rules but only honor a signed-in browser session, not an API token.

URLReturns
/d/:id.md The raw Markdown source.
/d/:id.textpack A TextBundle archive: Markdown plus images. Add ?hl=<set_id> to include that set as highlights.json.
/d/:id/meta content_hash and updated_at — a cheap way to poll for changes.

Tokens are bearer credentials: treat them like passwords, keep them out of logs and analytics, and revoke anything you suspect has leaked. Revoking a token has no effect on your documents.