Skip to main content
Get Started

Appearance

100%
800px
Page shows a markdown document as a sheet of paper you can print or save as a PDF.

Markest API

Markest exposes three HTTP interfaces:

  • The paste API (/api/v1) — authenticated with an API key. Create, list, read and delete your own pastes.
  • The read API (/api/p) — no authentication. Read any paste you could open in a browser: its document list, a document's source, its rendered HTML, or the whole thing as a ZIP.
  • Publishing without an account (/api/v1/drafts) — no key at all. Publish an artifact that lasts a day unless someone claims it into their account.

Everything is served over HTTPS from https://marke.st. All request and response bodies are JSON unless stated otherwise, and all timestamps are ISO 8601 (2026-09-04T10:30:00+00:00).


Authentication

The paste API accepts a key in either header:

Authorization: Bearer mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
X-API-Key: mk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx

Create a key under Account → API keys. Keys look like mk_live_ followed by 48 hexadecimal characters. The full key is shown once, when it is created — only a SHA-256 hash is stored, so it cannot be recovered afterwards. If you lose it, revoke the key and issue another.

API access is a plan feature. If your plan does not include it, no key you create will work.

Permissions

Each key carries an explicit set of permissions. A new key gets create_paste only; tick the others when you create it if you need them.

Permission Allows
create_paste POST /api/v1/pastes, POST /api/v1/pastes/{id}/images
list_own GET /api/v1/pastes
read_own GET /api/v1/pastes/{id}, GET /api/v1/pastes/{id}/versions, GET /api/v1/pastes/{id}/images, GET /api/v1/pastes/{id}/images/{imageId}
delete_own DELETE /api/v1/pastes/{id}, DELETE /api/v1/pastes/{id}/images/{imageId}

A key may also carry an expiry date and can be deactivated. Calling with an expired or inactive key returns 403.

Authentication errors

Status Body Meaning
401 Missing API key. Provide via Authorization: Bearer or X-API-Key header. No key supplied
401 Invalid API key. Key not recognised
403 API key is expired or inactive. Key exists but cannot be used
403 API key does not have <permission> permission. Key lacks the permission for this endpoint

Publishing without an account

Where the site has it switched on, a program with no account, key or sign-in can publish an artifact that is live at once. A file sent as the body is one document, named by the file (or ?name=, else README.md); ?title= gives it a title:

curl -sT report.md https://marke.st/api/v1/drafts/

Several documents go as JSON:

curl -s https://marke.st/api/v1/drafts -H 'Content-Type: application/json' \
  -d '{"title": "Notes", "documents": [{"path": "README.md", "content": "# Notes"}, {"path": "src/main.py", "content": "print(1)"}]}'
{ "id": "01J…", "url": "https://marke.st/p/01J…", "claim_url": "https://marke.st/app/claim/…", "expires_at": "2026-09-30T12:00:00+00:00", "visibility": "unlisted", "draft": true, "documents": [ … ], "message": "…" }

The artifact can be read at url straight away. It is unlisted, and it is removed 24 hours after it was published unless someone claims it: whoever opens claim_url while signed in to Markest with a confirmed email address moves it into their account, once, under their plan. Give claim_url only to the person who should own the artifact.

Without an account
Documents At most 10
Size At most 256 KB in all
Links At most 50
Content Markdown, text and code, named .md, .txt or for their language; no HTML, no images
Read as Its page only: no raw text, download, ZIP or embed until it is claimed
Per address As many a day as the site allows; GET /api/v1/drafts says, and whether it is switched on

These fields are refused with field_not_allowed: visibility, expires_in, burn_after_reading, password, folder, tags, track_versions, proxy_images, keep_line_breaks. A refusal is {"error": "…", "code": "…"}, with one of these codes: drafts_disabled (403), cross_site (403), nothing_sent (400), invalid_json (400), unsupported_type (415), too_large (413), too_many_documents, html_not_allowed, name_not_allowed, not_text, too_many_links, invalid (422), address_limit, site_limit (429, Retry-After), busy (503). A request sent from another site's page in a browser is refused as cross_site.


Paste API

Create a paste

POST /api/v1/pastes

Requires create_paste. Accepts JSON or multipart/form-data.

Single document (JSON) — the shorthand form:

curl -X POST https://marke.st/api/v1/pastes \
  -H "Authorization: Bearer $MARKEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Release notes",
    "content": "# 1.4.0\n\nFixed the thing.",
    "filename": "README.md",
    "visibility": "unlisted",
    "expires_in": 604800
  }'

Several documents (JSON):

curl -X POST https://marke.st/api/v1/pastes \
  -H "Authorization: Bearer $MARKEST_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "title": "Widget API",
    "default_path": "README.md",
    "documents": [
      { "path": "README.md",     "content": "# Widget API\n\nSee [setup](docs/setup.md)." },
      { "path": "docs/setup.md", "content": "# Setup\n\n1. Get a key." }
    ]
  }'

Files (multipart):

curl -X POST https://marke.st/api/v1/pastes \
  -H "Authorization: Bearer $MARKEST_KEY" \
  -F "title=Docs bundle" \
  -F "visibility=unlisted" \
  -F "files[]=@README.md" \
  -F "files[]=@CHANGELOG.md"

Each file's original name becomes its document path.

Fields

Field Type Default Notes
content string — Single-document shorthand. Provide this or documents.
filename string README.md Path for the content shorthand.
documents array — [{ path, content, title?, content_type? }]. Order is preserved.
title string null Paste title.
visibility string your default, unlisted unless changed public, unlisted or private, subject to your plan.
expires_in integer 0 Seconds until expiry. 0 means never, if your plan allows it.
default_path string first document Which document the paste opens on.
burn_after_reading boolean false Destroy the paste after its first view. Not allowed with HTML.
track_versions boolean your account setting Keep a version history from the first save. See Version history below.
proxy_images boolean your account setting Keep copies of the documents' external images on Markest, so readers never contact the sites they come from. false leaves them where they are, and readers are asked before any loads.
reading_layout string standard How its markdown documents open: standard, or page - a sheet of paper that prints as shown. Readers may still choose their own.
folder string key's default Folder in your paste list. Organisational only; not part of the URL. At most 100 characters.
content_type string detected Per document: markdown, html or code. Omit and Markest decides from the path and body; a name such as main.py or Makefile is code, highlighted in the language it names.

Paths are normalised and validated: no .., no backslashes, no leading /, at most 200 characters, and none of < > : " | ? * # or control characters (they would break the viewer URL or the ZIP download).

Response — 201 Created

{
  "id": "01K3XY4Z8QW2V7N0M5P9RT6ABC",
  "title": "Widget API",
  "visibility": "unlisted",
  "folder": null,
  "track_versions": false,
  "proxy_images": true,
  "reading_layout": "standard",
  "url": "https://marke.st/p/01K3XY4Z8QW2V7N0M5P9RT6ABC",
  "api_url": "https://marke.st/api/v1/pastes/01K3XY4Z8QW2V7N0M5P9RT6ABC",
  "documents": [
    {
      "path": "README.md",
      "title": "Readme",
      "content_type": "markdown",
      "raw_url": "https://marke.st/r/01K3XY4Z8QW2V7N0M5P9RT6ABC/README.md"
    }
  ],
  "created_at": "2026-09-04T10:30:00+00:00"
}

When visibility is public and the account's publishing confirmation is on — it is by default — the paste is created unlisted instead and the response is 202 Accepted: the same body, with status set to approval_required and an approval_url for the account holder to open. Nothing is public until they approve it; see Visibility below.


List your pastes

GET /api/v1/pastes

Requires list_own. Returns every paste you own; document contents are not included.

curl https://marke.st/api/v1/pastes -H "Authorization: Bearer $MARKEST_KEY"
{
  "pastes": [
    {
      "id": "01K3XY4Z8QW2V7N0M5P9RT6ABC",
      "title": "Widget API",
      "visibility": "unlisted",
      "folder": null,
      "document_count": 2,
      "created_at": "2026-09-04T10:30:00+00:00",
      "updated_at": "2026-09-04T10:30:00+00:00",
      "expires_at": null,
      "url": "https://marke.st/p/01K3XY4Z8QW2V7N0M5P9RT6ABC"
    }
  ],
  "total": 1
}

Workspaces

A key reaches the pastes of a workspace its account is in only once you let that agent into the workspace - for 10, 30 or 60 minutes, or until you revoke it - and GET /api/v1/pastes then lists them beside your own. Until then it reads them as someone with no account would, and a paste your account could reach answers 403 with the link that asks:

{
  "error": "This is in the workspace \"Design team\", which this agent has not been allowed to reach. …",
  "status": "workspace_access_required",
  "approval_url": "https://marke.st/app/agent-access/9f1c…",
  "already_pending": false,
  "workspace": { "id": "01JA…", "name": "Design team" }
}

Open the link in Markest, signed in, to choose how long; Account → API keys lists what each agent may reach, with Revoke. Once let in, a key acts with the account's role there. Every member's key reads them, private ones included; an editor's also changes their documents, images and title; only an owner's sets a password or expiry, turns on burning after reading, or deletes one. Anything else is refused with 403, or 404 where the paste is not the key's to change. An owner of a workspace can also make keys for the workspace itself, on its page, and they need no approval for it. Such a key reaches that workspace's pastes alone, as an editor or a reader - never those of the account that pays for the workspace - creates unlisted or private pastes in it, and never publishes, deletes or changes who may read a paste; it stays when the person who made it leaves.

Read one paste

GET /api/v1/pastes/{id}

Requires read_own. Returns the paste with the decrypted content of every document. Only pastes you own are visible; anything else returns 404.

{
  "id": "01K3XY4Z8QW2V7N0M5P9RT6ABC",
  "title": "Widget API",
  "visibility": "unlisted",
  "folder": null,
  "reading_layout": "standard",
  "documents": [
    {
      "path": "README.md",
      "title": "Readme",
      "content": "# Widget API\n\nSee [setup](docs/setup.md).",
      "content_type": "markdown",
      "sort_order": 0
    }
  ],
  "created_at": "2026-09-04T10:30:00+00:00",
  "updated_at": "2026-09-04T10:30:00+00:00",
  "expires_at": null
}

Add documents to a paste

POST /api/v1/pastes/{id}/documents · requires create_paste

Appends to a paste you already own, leaving its other documents untouched. This is how a multi-document paste gets built up over several calls rather than submitted whole.

curl -X POST https://marke.st/api/v1/pastes/01J.../documents \
  -H "Authorization: Bearer $MARKEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "path": "docs/setup.md", "content": "# Setup\n\nInstall it." }'

Several at once:

{
  "documents": [
    { "path": "a.md", "content": "# A" },
    { "path": "b/c.md", "content": "# C" }
  ]
}
Field Type Notes
path string Required. Folders are allowed; the path is normalised.
content string The document body.
title string Optional display title.
content_type string Optional; otherwise detected from the path and body.
documents array Use instead of path/content to add several at once.
overwrite bool Replace a document already at that path. Defaults to false. A replaced document keeps its title unless the request gives one.

A path already in use returns 409 rather than silently replacing anything:

{ "error": "This paste already has a document at \"a.md\". Pass \"overwrite\": true to replace it, or PATCH to update it." }

The resulting document set is validated as a whole, so the same size, path and plan rules that guard creation also guard an addition.

Returns 201 with the paste's full document list.

Update a document

PATCH /api/v1/pastes/{id}/documents · requires create_paste

Changes one document in place. content replaces the body entirely — read it first if you need to keep any of it.

curl -X PATCH https://marke.st/api/v1/pastes/01J.../documents \
  -H "Authorization: Bearer $MARKEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "path": "docs/setup.md", "content": "# Setup\n\nRevised." }'
Field Type Notes
path string Required. Names the document to change.
content string New body.
new_path string Move or rename the document.
title string New display title.
content_type string Override the detected type.
sort_order int Position within the paste.

404 if the paste has no document at that path.

Delete a document

DELETE /api/v1/pastes/{id}/documents?path=docs/setup.md · requires delete_own

The path may also be given in the body. A paste must keep at least one document; removing the last one returns 422. Delete the paste itself instead.

Each of these returns the paste's documents after the change:

{
  "id": "01J...",
  "url": "https://marke.st/p/01J...",
  "document_count": 2,
  "documents": [
    { "path": "README.md", "title": "README", "content_type": "markdown", "sort_order": 0,
      "raw_url": "https://marke.st/api/p/01J.../raw/README.md" }
  ]
}

Delete a paste

DELETE /api/v1/pastes/{id}

Requires delete_own. Permanent, and it takes every document with it.

{ "deleted": true }

Upload an image

POST /api/v1/pastes/{pasteId}/images — needs create_paste.

Stores an image with a paste you own, so its documents can show it without relying on another site. Send the image itself as the request body, with nothing to encode:

curl https://marke.st/api/v1/pastes/01J.../images?name=chart.png \
  -H "Authorization: Bearer $MARKEST_KEY" \
  -H "Content-Type: image/png" \
  --data-binary @chart.png

name is the image's name, and without its extension its alt text; a Content-Disposition filename does the same, and with neither the image is called image. PUT is taken as well as POST, so curl -T chart.png works too. A multipart image file is taken as well, and so is JSON carrying the image in base64:

{ "name": "chart.png", "data": "iVBORw0KGgo…" }

data is base64; a data: URL is accepted too. PNG, JPEG, GIF, WebP, AVIF, BMP and SVG are kept, under the plan's image limits. The response, 201 Created, gives the address to use:

{ "id": "01J…", "source": "upload", "name": "chart.png", "original_url": null, "content_type": "image/png", "size": 48213, "sha256": "9f86d081…", "path": "/img/01J…/01J…", "url": "https://marke.st/img/01J…/01J…", "markdown": "![chart](/img/01J…/01J…)", "created_at": "2026-09-15T12:00:00+00:00" }

sha256 is the SHA-256 of the image's bytes, so a script can tell whether it has already sent an image.

Whoever can read the paste can see the image. A file that is no image is 422, one that is too large 413. An agent connected over MCP can instead add image files from its disk, a whole folder at a time, with markest_add_images; see the MCP guide.

List, download and delete images

Request Needs Does
GET /api/v1/pastes/{pasteId}/images read_own Lists the paste's images: uploads, and the copies Markest made of images its documents show from other sites
GET /api/v1/pastes/{pasteId}/images/{imageId} read_own The image itself, private pastes included
DELETE /api/v1/pastes/{pasteId}/images/{imageId} delete_own Deletes an upload: 204

Each image is described as an upload's response describes it:

{ "paste_id": "01J…", "images": [ { "id": "01J…", "source": "upload", "name": "chart.png", "original_url": null, "content_type": "image/png", "size": 48213, "sha256": "9f86d081…", "path": "/img/01J…/01J…", "url": "https://marke.st/img/01J…/01J…", "markdown": "![chart](/img/01J…/01J…)", "created_at": "2026-09-15T12:00:00+00:00" } ] }

A copy ("source": "proxy") cannot be deleted — 409 — because it goes when no document shows it any more.

Read API

These endpoints need no key. They serve any paste you could open in a browser: public and unlisted pastes, and private ones when you pass the signature parameters from a signed link. A paste that is expired, password-locked, or private without a valid signature answers 404 — the same answer as a paste that does not exist, so the endpoint never reveals which.

Document list

GET /api/p/{id}/manifest
{
  "id": "01K3XY4Z8QW2V7N0M5P9RT6ABC",
  "title": "Widget API",
  "defaultPath": "README.md",
  "visibility": "unlisted",
  "updatedAt": "2026-09-04T10:30:00+00:00",
  "documents": [
    {
      "path": "README.md",
      "title": "Readme",
      "contentType": "markdown",
      "sortOrder": 0,
      "sha256": "9f86d081884c7d65…",
      "updatedAt": "2026-09-04T10:30:00+00:00"
    }
  ]
}

Document source

GET /api/p/{id}/doc?path=README.md

Returns the raw source, not JSON: text/markdown for a markdown document and text/plain for an HTML one — HTML is never served as text/html here. The response carries an ETag of the document's SHA-256, so conditional requests work.

Rendered HTML

GET /api/p/{id}/rendered?path=README.md

Returns the document rendered to HTML as JSON. HTML documents have no rendered form on this origin and answer 409 instead:

{
  "error": "This document is HTML and is only shown sandboxed inside the Markest viewer.",
  "contentType": "html",
  "viewUrl": "https://marke.st/p/01K3XY4Z8QW2V7N0M5P9RT6ABC/report.html"
}

Download

GET /api/p/{id}/download.zip        # the whole paste
GET /r/{id}/{path}                  # one document, inline
GET /r/{id}/{path}?download=1       # one document, as a file

/r/ serves markdown as text/markdown and HTML as inert text/plain. With ?download=1 it becomes an attachment under the document's own name, and an HTML document is sent as application/octet-stream so nothing can render it.

Private pastes

A signed link carries two query parameters:

?exp=<unix-timestamp>&sig=<64 hex characters>

Pass both to any read-API endpoint to reach a private paste:

curl "https://marke.st/api/p/$ID/manifest?exp=1788000000&sig=$SIG"

The signature covers the paste id and the expiry, and is checked in constant time. It fails closed on a missing key, a malformed digest, a lapsed deadline, or a signature minted for a different paste. A signed link opens the whole paste — every document in it and the ZIP download — not a single file. Rotating the paste's signing key invalidates every link already issued.


Reading a paste as a script or agent

You do not need the API for this. Request a paste's normal URL as a non-browser client and Markest serves clean markdown instead of the HTML page, with an index of the paste's documents at the top:

curl https://marke.st/p/01K3XY4Z8QW2V7N0M5P9RT6ABC
curl -H "Accept: text/markdown" https://marke.st/p/$ID/docs/setup.md
curl "https://marke.st/p/$ID?raw=1"

A client is treated as non-browser when it sends ?raw=1, an Accept header asking for text/markdown or text/plain without text/html, a known AI-agent user agent, or no Sec-Fetch-Mode header at all. Password-protected and unread burn-after-reading pastes refuse this path.


HTML documents

A document is either markdown or html. HTML documents are never rendered on the marke.st origin. They are served only from /h/{id}/{path}, under a Content-Security-Policy: sandbox without allow-same-origin, and only to the viewer's own iframe — a direct navigation is redirected to the viewer and any other context gets 403. Their scripts and forms work; none of it can read a cookie or touch the surrounding page.

For API purposes this means an HTML document is readable as source (/api/p/{id}/doc, /r/{id}/{path}) but has no rendered form you can fetch.


Limits and errors

Paste size limits are enforced for every write path, the API included:

Limit Value
Markdown document 128 KB
HTML document 1 MB
Code document 1 MB
Paste total 512 KB, or 2 MB when it contains HTML or code
Documents per paste 50, or fewer on your plan

Your plan additionally caps the number of pastes, documents per paste, storage, and which visibilities and features you may use. Exceeding any of them returns 403 with a message naming the limit.

Status Meaning
200 / 201 / 202 Success — 202 when a new public paste awaits confirmation
400 Malformed request — invalid JSON, or neither content nor documents
401 Missing or invalid API key
403 Key lacks the permission, key unusable, or a plan quota was exceeded
404 Paste or document not found, or not yours
409 The document is HTML and has no rendered form
422 Validation failed — bad path, oversized document, or an unusable combination
429 Rate limit exceeded — see Rate limits

Errors are always shaped the same way:

{ "error": "Document \"notes.md\" exceeds 128KB limit." }

Rate limits

Requests are limited per minute. API traffic is counted against your API key, so one key's usage never affects another's.

Bucket Applies to Default
api /api/v1/* and /mcp 30/min, or your plan's allowance if higher
paste_create Creating a paste 10/min
auth Sign-in and account recovery 5/min
global Everything else 60/min

Paid plans raise the API allowance — currently 120/min on Pro and 300/min on Business. The higher of the site default and your plan's allowance applies.

Every limited response carries your standing:

X-RateLimit-Limit: 120
X-RateLimit-Remaining: 117

Exceeding a limit returns 429 with a Retry-After header giving the seconds to wait:

{ "error": "Rate limit exceeded: 120 requests per minute. Retry in 34 seconds." }

Limits are a sliding window, so the allowance refills continuously rather than resetting on the minute. OAuth discovery documents are never limited, so a client can always find its way to the authorization server.

From the command line

The markest command uses this API for you: it publishes a folder as one artifact with its images, reads, pulls and manages your artifacts, and encrypts artifacts end to end on your machine. It is open source, and signs in in your browser or uses an API key.

npm install -g github:techdyn/markest-cli
markest publish ./docs

Agents and MCP

Markest has a Model Context Protocol endpoint, so an AI agent can work with your pastes as tools rather than by calling this API directly. It is the way to connect ChatGPT, Claude, Codex or Cursor.

POST https://marke.st/mcp

Everything about it — the tool list, connecting each client, the OAuth flow and the publishing confirmation — is on the MCP guide.

Two things worth knowing from here:

  • The MCP endpoint shares this API's rate-limit bucket.
  • Agent access is a plan feature, separate from API access.

Visibility

POST /api/v1/pastes/visibility · requires create_paste

Set the visibility of one paste or many. The two directions behave differently on purpose.

curl -X POST https://marke.st/api/v1/pastes/visibility \
  -H "Authorization: Bearer $MARKEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "paste_ids": ["01J...", "01K..."], "visibility": "private" }'
Field Type Notes
paste_ids array The pastes to change. Up to 100 per call.
paste_id string Shorthand for a single paste.
visibility string public, unlisted or private.

Restricting applies immediately

Setting pastes to unlisted or private takes effect at once, in batches, subject only to your plan's allowed visibilities. Returns 200:

{
  "status": "applied",
  "changed": [ { "id": "01J...", "title": "Notes", "visibility": "private", "url": "https://marke.st/p/01J..." } ],
  "unchanged": []
}

Publishing may need your confirmation

Making a paste public is the one visibility change that cannot be undone — once something has been read, copied or indexed, setting it back to private does not retract it. So by default an API caller cannot publish directly. The request returns 202 with a link for the account holder to open:

{
  "status": "approval_required",
  "approval_url": "https://marke.st/app/approvals/31635a45fb0e5b8a67bd011cea321c04",
  "visibility": "public",
  "pending": [ { "id": "01J...", "title": "Notes", "visibility": "private", "url": "https://marke.st/p/01J..." } ],
  "message": "Making these 2 pastes public needs your confirmation. Open the link to review and approve."
}

Nothing is public until that link is opened and approved. The confirmation screen covers every paste in the request at once, with a checkbox each, so a batch takes one visit and a subset can be approved without the rest. The link works once and expires after 24 hours.

202 means understood and stored, not done. Do not poll it and do not retry — hand the URL to the person whose account it is.

Turn the gate off under Account → Publishing if you would rather publish directly from the API. Restricting a paste is never gated either way.

Status Meaning
200 Applied
202 Stored, awaiting the account holder's confirmation
403 Key lacks create_paste, or the plan does not include API access
422 Unknown visibility, a paste that is not yours, plan disallows it, or too many at once

Over MCP

The markest_set_visibility tool takes the same arguments and follows the same rules. When approval is required it returns the URL and says plainly that nothing is published until the account holder confirms — an agent should pass the link on rather than retry. Send every paste in one call so the person confirms one screen instead of thirty.

Update a paste

PATCH /api/v1/pastes/{id} · requires create_paste

Change a paste's settings without touching its documents.

curl -X PATCH https://marke.st/api/v1/pastes/01J... \
  -H "Authorization: Bearer $MARKEST_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "title": "Release notes", "folder": "Docs", "expires_in": 604800 }'
Field Type Notes
title string null clears it.
folder string null or "" unfiles the paste. At most 100 characters.
tags string Comma-separated; replaces the current tags.
default_path string Which document opens first. Must already exist in the paste.
expires_in int Seconds from now. 0 means never, if your plan allows it.
password string Protects the paste. null or "" removes the password.
burn_after_reading bool Deletes the paste once read.
track_versions bool true records every later save; false stops recording and keeps the versions already recorded.
proxy_images bool true copies external images to Markest; false removes the copies already made and leaves images on their own sites, loaded only once the reader agrees.
reading_layout string standard or page: how its markdown documents open.

Send only the fields you want changed; anything omitted is left alone. An empty body returns 400 rather than silently doing nothing.

Visibility is not settable here. It has one entry point — the visibility endpoint above — because that is where the publishing confirmation lives. Sending visibility to this endpoint returns 422 with a pointer, rather than quietly giving you a second way to publish.

Password protection, burn-after-reading and no-expiry are plan features and return 403 when your plan does not include them.

Returns the paste's settings after the change:

{
  "id": "01J...",
  "title": "Release notes",
  "visibility": "unlisted",
  "folder": "Docs",
  "default_path": "README.md",
  "burn_after_reading": false,
  "password_protected": false,
  "expires_at": "2026-09-11T08:00:00+00:00",
  "document_count": 3,
  "track_versions": false,
  "proxy_images": true,
  "reading_layout": "standard",
  "url": "https://marke.st/p/01J...",
  "updated_at": "2026-09-04T08:00:00+00:00"
}

Over MCP the same fields are available as markest_update_paste.

Version history

A paste can keep a version history: each save is recorded with what it changed and the text of every document as it was. It is off unless you turn it on, per paste with track_versions or for every new paste in your account settings. Turning it off stops recording and keeps the versions already recorded.

History belongs to the paste's owner alone. These endpoints answer 404 for a paste you do not own, even a public one. Restoring a version is done in the editor.

List versions

GET /api/v1/pastes/{id}/versions · requires read_own

curl https://marke.st/api/v1/pastes/01J.../versions \
  -H "Authorization: Bearer $MARKEST_API_KEY"

Versions come newest first. source says where each save came from: created, editor, api, mcp, restore, enabled (the paste as it stood when history was turned on) or system. changes compares a version with the one before it.

{
  "paste_id": "01J...",
  "track_versions": true,
  "versions": [
    {
      "number": 2,
      "source": "api",
      "created_at": "2026-09-13T08:00:00+00:00",
      "title": "Release notes",
      "document_count": 2,
      "changes": {
        "title": null,
        "default_document": null,
        "added": ["CHANGELOG.md"],
        "removed": [],
        "modified": ["README.md"],
        "renamed": [],
        "retitled": [],
        "reordered": false
      }
    },
    {
      "number": 1,
      "source": "created",
      "created_at": "2026-09-12T17:30:00+00:00",
      "title": "Release notes",
      "document_count": 1,
      "changes": {
        "title": [null, "Release notes"],
        "default_document": null,
        "added": ["README.md"],
        "removed": [],
        "modified": [],
        "renamed": [],
        "retitled": [],
        "reordered": false
      }
    }
  ]
}

Read a version

GET /api/v1/pastes/{id}/versions/{number} · requires read_own

Returns the version and the documents it held. Add path to include that document's text as the version kept it.

curl "https://marke.st/api/v1/pastes/01J.../versions/2?path=README.md" \
  -H "Authorization: Bearer $MARKEST_API_KEY"
{
  "paste_id": "01J...",
  "number": 2,
  "source": "api",
  "created_at": "2026-09-13T08:00:00+00:00",
  "title": "Release notes",
  "document_count": 2,
  "changes": {
    "title": null,
    "default_document": null,
    "added": ["CHANGELOG.md"],
    "removed": [],
    "modified": ["README.md"],
    "renamed": [],
    "retitled": [],
    "reordered": false
  },
  "default_path": "README.md",
  "documents": [
    { "path": "README.md", "title": null, "content_type": "markdown" },
    { "path": "CHANGELOG.md", "title": null, "content_type": "markdown" }
  ],
  "document": {
    "path": "README.md",
    "title": null,
    "content_type": "markdown",
    "content": "# Release notes\n\nFixed the export."
  }
}

A version or path the paste does not have returns 404. Over MCP the same history is read with markest_list_versions and markest_get_version.