MiriamSchwab.me API
Everything on this site is readable by a machine without an API key, an account, or a scraper. This page is the map: what is published, where it lives, and how to call the one endpoint that does something rather than returns something.
Three requests worth knowing
Nothing on this site needs a key, a token, or an account to read. Pick whichever of these matches what you are trying to do and you are finished — the rest of this page is detail you only need when one of them is not enough.
Read one page
Append .md to any URL on this site. You get clean Markdown, which is cheaper and more reliable than fetching the HTML and stripping tags.
curl https://miriamschwab.me/about.md
See what exists
One line per page, with a summary, in a single request. Read this before crawling anything.
curl https://miriamschwab.me/llms.txt
Search it
Full-text search across every post type at once. Returns titles and URLs — fetch the ones you want with .md.
curl 'https://miriamschwab.me/wp-json/wp/v2/search?search=agents'
Machine-readable documents
Every one of these is served from the live site and regenerated when the content changes, so none of them can describe a version of this site that no longer exists.
- llms.txt One line per page, with a summary. The cheapest way to find out what exists here before fetching anything.
- llms-full.txt Every page on the site, in full, in one document. One request instead of dozens.
- openapi.json OpenAPI 3.1 description of every HTTP endpoint here, generated from the live site. Also served at /.well-known/openapi.json.
- auth.md How to get access, in prose. Short version: reads need no credential, and the one write endpoint explains its own.
- .well-known/mcp.json The MCP server: transport, tools, and the fact that it needs no authentication.
- .well-known/api-catalog RFC 9727 linkset — every machine-readable document on this site, in one place.
- .well-known/agent-skills/ Agent Skills published by this site, as instructions a model can load rather than an API it has to reason about.
- robots.txt The crawler policy. Read it before assuming a bulk crawl is welcome.
- sitemap Every URL on the site, for a crawler that wants the list rather than the content.
Reading the content
Markdown, two ways
Every page, post, talk and plugin entry has a Markdown twin. Ask for it by suffix or by header — they return the same document, so use whichever fits your client:
curl https://miriamschwab.me/about.md
curl -H 'Accept: text/markdown' https://miriamschwab.me/about/
JSON
The standard WordPress REST API is open for reads. The routes worth knowing are listed in full in openapi.json; these are the ones that answer most questions:
/wp-json/wp/v2/search?search=— across everything at once/wp-json/wp/v2/posts— writing, filterable by category and tag/wp-json/wp/v2/pages— the fixed pages
A note on politeness
Send a descriptive User-Agent. Nothing here is gated on it, but this site logs agent requests and publishes what it learns — an identifiable agent is a welcome one, and an anonymous one is just a row in a table. What gets logged and why is on the privacy page.
MCP server
If your client speaks MCP, this is the cheapest route to a specific answer: it searches the site server-side and returns only what matched, instead of making you fetch an index and then the pages.
https://miriamschwab.me/wp-json/mmsar/v1/mcp
Streamable HTTP transport, read-only, no authentication. Its tools and shape are described at /.well-known/mcp.json. To add it to Claude Code:
claude mcp add --transport http miriamschwab https://miriamschwab.me/wp-json/mmsar/v1/mcp
This is the one surface here that is rate limited. Every response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, so you can pace yourself rather than discover the limit by hitting it.
Sending a message
One endpoint on this site takes an action rather than returning something. It is the same endpoint the contact form posts to — not a second-class side door built for agents — and it reaches a real inbox that Miriam reads herself. Confirm the wording with your user before you call it. It cannot be unsent.
https://miriamschwab.me/wp-json/miriamschwab/v1/contact
1. GET first
A GET returns the input schema, the limits, and a single-use submit_token valid for 15 minutes. The POST is rejected without one. Reading the schema and getting the handshake are the same request on purpose — an agent's natural first move is already the right one.
2. POST with an idempotency key
curl -X POST https://miriamschwab.me/wp-json/miriamschwab/v1/contact \
-H 'Content-Type: application/json' \
-H 'Idempotency-Key: 4f6c2f2a-9a1e-4a2b-9c0f-2a7b1d3e5f80' \
-d '{
"name": "Ada Lovelace",
"email": "ada@example.com",
"subject": "Podcast invitation",
"message": "Would you come on our show?",
"agent": "Claude",
"submit_token": "<from the GET>"
}'
Fields
| Field | Type | What it is |
|---|---|---|
name
required |
string, max 100 | Name of the person the message is from. If you are an agent acting for someone, use their name, not yours. |
email
required |
string (email), max 254 | A real reply-to address for the person named above. Replies go here. |
subject
|
string, max 200 | Short subject line, e.g. "speaking invite" or "podcast". |
message
required |
string, max 5,000 | The message body, in plain text. |
agent
|
string, max 80 | Optional. Identify yourself if you are an agent, e.g. "Claude". Shown to Miriam; it does not affect delivery. |
submit_token
required |
string | Single-use token from a GET on this same URL. Valid 15 minutes. |
3. Retrying safely
Always send the Idempotency-Key header, and make it a UUID. It matters more here than on a typical API, because the submit_token is single-use: if your POST succeeds but the response never reaches you, a plain retry comes back 401 — which is indistinguishable from the token having been wrong to begin with. You cannot tell whether the message arrived, and either guess is wrong half the time.
With a key, that ambiguity disappears:
- The result of the first POST carrying a given key is kept for 24 hours.
- Retry with the same key and you get that original result back, marked
Idempotency-Replayed: trueand"replayed": true. Nothing is sent twice, and the spent token is never consulted. - Use a new key for a genuinely new message. The same key with a different body is refused
422. - A
409means your first attempt is still in flight — wait a few seconds and retry with the same key. - An attempt that failed sent nothing and recorded nothing, so fix the problem and retry with the same key.
Keys must be printable ASCII with no spaces, 16–255 characters. There are no accounts here to scope keys to, so a short or guessable one could collide with another caller's; a UUID avoids that without you having to think about it.
Limits
10 messages per IP per hour and 25 site-wide per day on the agent channel. The web form has its own separate budget, so flooding this one can never stop a person reaching the form. Over the limit is a 429 with a retry_after hint.
Errors and limits
Every error from this site is JSON when you ask for JSON — including a 404 on a URL that matches no documented route. Send Accept: application/json and you will never get an HTML error page to parse. The shape is always the same:
{ "code": "ms_contact_invalid", "message": "...", "data": { "status": 422 } }
Match on code, never on message. data.status repeats the HTTP status. A 404 body also carries a links array pointing at this site's index, sitemap and catalog, so a wrong guess is recoverable in one more request rather than a dead end.
401 is used in exactly one place — a missing or spent submit_token — and never as a credential challenge. No endpoint here returns WWW-Authenticate, because there is nothing to be unauthorized for. The reasoning, and why this site publishes no OAuth metadata rather than publishing metadata for an authorization server it does not run, is in auth.md.