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.

None authentication required, anywhere
9 machine-readable documents
1 endpoint that takes an action

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.

POST 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.

GET POST 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

FieldTypeWhat 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: true and "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 409 means 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.