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 Marked issues a token and a device key together. Both are
shown once.
| Header | Value | Required for |
|---|---|---|
Authorization |
Bearer <api_token> |
Everything except reading a public document or its highlight sets |
X-Device-Key |
<device_key> |
Creating and updating documents, and captures |
If-Match |
<content_hash> |
Updating 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.
Errors
| Status | Body | Meaning |
|---|---|---|
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. |
422 |
{"error":"…"} |
Validation failed, for example a blank body or an archive with no Markdown in it. |
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"
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, and
reading_style. Visibility defaults to private. A body is required and
is capped at 500,000 bytes.
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"
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.
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…"
}
]
| Field | Meaning |
|---|---|
label | Account display name, else the reviewer's signature, else "Anonymous reviewer". |
owner | true when the set belongs to the token's user. Filter these out to list other people's reviews. |
parent_set_id | Set this one was forked from, if any. |
content_signature | Fingerprint 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.
| URL | Returns |
|---|---|
/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.