Mark
Mark API & MCP reference
One call stamps any content with a permanent, publicly verifiable record of who made it and when. You hash the content locally, so the bytes never leave your machine. Verification is free, forever, with no account.
API base: https://makemark.io ·
MCP: https://makemark.io/mcp
Quickstart
Hash your content, send the fingerprint, get a permanent verify URL back.
# 1. hash the content locally (only the fingerprint is sent)
HASH=$(shasum -a 256 report.pdf | cut -d' ' -f1)
# 2. stamp it
curl -sS https://makemark.io/proof \
-H "authorization: Bearer pk_your_key" \
-H "content-type: application/json" \
-d "{\"contentHash\":\"$HASH\",\"title\":\"Q3 report\",\"visibility\":\"public\"}"
# → { "proofId": "proof_…", "verifyUrl": "https://makemark.io/p/proof_…", "status": "SIGNED", … }Getting a key
API keys are portable credentials for agents, scripts, and other apps. Humans can
sign in at makemark.io/account, choose a file, and use
their lifetime sample without creating or handling a key. An agent with no inbox
calls POST /keys or the
create_account MCP tool directly and gets a usable key back in the
same response, no human step at all:
curl -sS -X POST https://makemark.io/keys \
-H "content-type: application/json" \
-d '{"label":"my-agent"}'
# → { "id": "key_…", "secret": "pk_…", "creatorId": "key_…", "plan": "free" }
# secret is shown exactly once, store it now
Self-serve keys are rate-limited (3 per network per day) and start on the free
plan under a fresh, unattached identity. Send the secret as
authorization: Bearer pk_… on every later call. Agents that would
rather not hold a key at all can pay per stamp with x402 instead.
Pricing
Verification is always free. Every durable account gets one lifetime sample stamp. Prepaid packs never expire and come from the same published offer manifest used by checkout and the pricing page.
| Stamps | USD | Offer id |
|---|---|---|
| 5 | $1.00 | bundle_5 |
| 10 | $1.50 | bundle_10 |
| 50 | $5.00 | bundle_50 |
| 100 | $8.00 | bundle_100 |
| 500 | $35.00 | bundle_500 |
| 1,000 | $65.00 | bundle_1000 |
| 2,000 | $120.00 | bundle_2000 |
REST API
| Method & path | Auth | What it does |
|---|---|---|
POST /keys | none (rate-limited) | Self-serve key: mint a free-plan key with no human step. 3 per network per day. |
POST /proof | key or x402 | Create a stamp for a content hash. Returns a signed receipt + verify URL. |
POST /verify | none | Given a content hash, returns every stamp for it, earliest-anchored first. |
GET /search?q= | none | Search public stamps by title (or exact id / hash) across creators. Paginated via nextCursor; private stamps never appear. |
GET /proof/:id | none | Public projection of one proof (status, times, anchors). |
GET /proof/:id/bundle | none | Self-contained JSON to verify the proof offline. |
GET /proof/:id/c2pa | none | C2PA 2.4 manifest (media content types). |
GET /proof/:id/badge.svg | none | Embeddable status badge. |
GET /proof/:id/qr.svg | none | Scannable QR that opens the proof record. |
GET /proofs | key | List the caller's own stamps, newest first. ?cursor= pages past the first page; ?q= searches your whole history. Returns { proofs, nextCursor }. |
POST /proof/:id/revoke | key (owner) | Forward-only revoke. |
POST /identities | key | Start verifying a domain or handle ("prove it's you"). Returns publish instructions. |
POST /identities/:id/verify | key | Check the published token; on success your stamps carry the verified identity. |
GET /identities | none | The transparent public registry of every verified publisher. |
GET /identities/:value/proofs | none | A verified publisher's public stamps (their "everything I publish" feed). |
Verification endpoints are unmetered and need no account.
curl -sS https://makemark.io/verify \
-H "content-type: application/json" \
-d '{"contentHash":"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"}'
# → { "exact": true, "matchCount": 1, "conflict": null,
# "proofs": [ { "publisher": { "type": "domain", "value": "example.com",
# "tier": "verified" }, … } ] }
Reading a verify result: exact means these exact bytes are stamped;
publisher is the verified identity (never trust a name that isn't in it:
null means an unverified account); conflict means two or
more different accounts stamped the same bytes, earliest anchored first. A no-match
on a re-encoded or screenshotted copy is expected, not evidence of fakery.
MCP (for agents)
The proof service speaks the Model Context Protocol over Streamable HTTP, so any MCP-capable agent (Claude Code, Claude Desktop, Cursor, Windsurf) can stamp and verify content directly.
claude mcp add --transport http mark https://makemark.io/mcp{
"mcpServers": {
"mark": { "url": "https://makemark.io/mcp" }
}
}Tools exposed: create_account, create_proof, verify_proof, list_proofs, revoke_proof. Then tell the agent: "Stamp everything you finish."
ChatGPT Actions
A ready-to-import OpenAPI 3.1 schema covers POST /keys,
POST /proof, POST /verify, and GET /proof/:id.
In a Custom GPT's configuration, under Actions, choose Import from URL
and paste:
https://makemark.io/.well-known/gpt-actions.json
Set the action's auth to API Key, Bearer, and paste a key from
Getting a key above (or leave it unauthenticated to only expose
POST /keys and POST /verify, which need no key).
Agents: stamp autonomously, end to end
The whole loop needs no human once a standing instruction is set:
0 account | No key yet? create_account (MCP) or POST /keys. One call, usable immediately, no signup page. |
1 hash | SHA-256 the finished bytes locally. Content never leaves the machine. |
2 stamp | create_proof (MCP) or POST /proof. On 402, sign the advertised x402 payment and retry. |
3 attach | Drop the receipt's verifyUrl, byline, badgeEmbed, or qrUrl into the published artifact. |
4 identity | Once, ever: POST /identities for your human's domain. If you manage their DNS (e.g. the Cloudflare API), create the returned TXT record yourself, then POST /identities/:id/verify. Every future stamp carries the verified name. |
5 recover | Every error body carries next + docs. Follow next; no support ticket. |
Paying with x402
Agents with no account will be able to pay $0.10 per stamp ($0.05 with an API key)
in USDC on Base using the x402 protocol (EIP-3009, gasless). Call
POST /proof with no key; a 402 comes back describing how
to pay, your x402 client signs the authorization, and the retried call returns a
receipt. Payment is final on-chain, so there are no chargebacks and no stored card.
Until this lane is switched on, the API returns 503 payment_unavailable
pointing you to a free key instead; see pricing for status.
Identity without an account
A keyless x402 stamp still has a stable identity: your paying wallet is your
identity. The stamp is recorded under your Base address (the one that signed
the payment), so re-stamping the same bytes is idempotent and version chains
(previousProofId) work with zero setup, exactly like a keyed account. You
don't have to send anything extra.
Two optional fields let you add a human "who" without signing up:
displayName | A human label (e.g. "Acme Newsroom bot") shown on public stamps as self-asserted, unverified. Never rendered as a verified identity, so it can't impersonate one. |
claimEmail | The human's email. They get a one-time link to sign in (their free Mark account is created automatically, no password) and these wallet stamps move into that account, which they keep and reuse. Nothing is created until they click; the email is never shown publicly. |
You may also pass your own creatorId (a did:web / did:key
you control) to chain stamps across rotating wallets. Reserved prefixes
(payer:, account:, tatastu:, key:) are rejected.
A self-chosen creatorId can't be claimed by email (it proves no control), so
claimEmail only applies to stamps recorded under your paying wallet. For a truly
verified name, verify a domain or handle via the identity
flow (needs a key).
Error responses that tell you the next step
Every auth or payment failure returns a small JSON body an agent can act on:
{
"error": "payment_unavailable",
"message": "Create a durable Mark account to claim one lifetime sample: POST /keys with no auth, use the create_account MCP tool, or sign in on the account page.",
"next": "https://makemark.io/account",
"pricing": "https://makemark.io/pricing",
"docs": "https://makemark.io/docs"
}| Status | error | Meaning |
|---|---|---|
| 400 | invalid_content_hash | contentHash must be 64 hex chars (sha256). |
| 401 | unauthorized | Missing or revoked API key. Sign in for a free one. |
| 402 | quota_exhausted | Monthly included stamps used up and no credits. See pricing; resets on the 1st (UTC). |
| 402 | payment_required | x402 payment needed; the body describes how to pay. |
| 429 | too_many_requests | Rate limited. Retry after the given delay. |
| 429 | rate_limited | POST /keys only: 3 self-serve accounts per network per day. Wait, or sign in for a human account. |
| 503 | payment_unavailable | Pay-per-stamp lane is off; use an API key. |
Offline verification
Fetch the bundle and check the Ed25519 signature and the RFC 6962 Merkle inclusion
path with zero network calls. A small standalone verifier ships in the MIT-licensed
@tatastu/mark
npm package as @tatastu/mark/verify/offline, so those checks can run
without Mark. Keep the bundle and original file.
Offline checks do not validate ledger state or anchor publication time;
independently check the public anchor references for that. Arweave is the
configured witness; Base is optional.
curl -sS https://makemark.io/proof/proof_xxx/bundle > bundle.json
# contains: receipt, signature, signing pubkey, merkle path + root, anchorsWhat a stamp proves (and what it does not)
A stamp asserts provenance from a signer at a time, not authorship truth. Two timestamps are always shown: the claimed time (when the service signed) and the proven time (when a daily batch root is independently anchored). Daily batches use Arweave, with Base available as an optional additional witness. It covers the exact bytes you hashed, not a live web page. That honesty is the point: it is evidence anyone can check, forever, without trusting Tatastu.