# MailerDot Developer Documentation

> The MailerDot API, MCP server, and agent-readable resources — every U.S. city we publish, its daily editions, weather, and census facts. Read-only, no key, no sign-up, CORS open.

- Canonical (HTML): https://mailerdot.com/developers
- OpenAPI 3.1 spec: https://mailerdot.com/openapi.json
- MCP server: https://mailerdot.com/mcp
- Base URL: https://mailerdot.com

## Authentication

None. No key, no token, no account, no sign-up — including for the endpoints that sell things:
a checkout is created anonymously and the buyer identifies themselves on the payment page.

Most endpoints are read-only. The four that write are `POST /api/subscribe` (free) and the three
purchase endpoints under "Buying things" below, none of which can charge anyone. There is no
sandbox because there is nothing to break: the read endpoints cannot mutate anything and the
purchase endpoints stop at a payment link, so you can call them against production now.

## Quickstart

```bash
# 1. Find a city
curl -s 'https://mailerdot.com/api/cities'

# 2. Read its latest edition as markdown
curl -s 'https://mailerdot.com/austin-texas.md'

# 3. Search every archive
curl -s 'https://mailerdot.com/api/search?q=farmers+market&limit=3'
```

## City slugs

Every endpoint is keyed by a slug of the form `{city}-{state-name}`, lowercased and hyphenated — `austin-texas`, `san-francisco-california`, `new-york-new-york`. The state is the full name, not the postal abbreviation. Fetch https://mailerdot.com/api/cities for the authoritative list.

## Endpoints

| Method | Path | What it returns |
|---|---|---|
| `GET` | `/api/cities` | Every city with a published edition, grouped by state. |
| `GET` | `/api/cities-census` | Census-tracked cities, filterable by population and state. |
| `GET` | `/api/city/{slug}` | One city: subscriber count, archive index, census snapshot, events, directory. |
| `GET` | `/api/archive/{slug}/{date}` | One past edition. Append `.md` to the date for markdown instead of JSON. |
| `GET` | `/api/search` | Full-text search across every archived edition, with highlighted snippets. |
| `GET` | `/api/weather/{slug}` | Today's forecast — the stored snapshot the edition used, or a live fetch. |
| `GET` | `/api/facts/{slug}` | Schema.org Dataset facts for a city: newsletter metadata plus ACS demographics. |
| `GET` | `/api/territories` | Whether a town can be leased and what it costs. No query returns the taken list. |
| `GET` | `/api/oembed` | oEmbed provider for city and edition pages (rich iframe embeds). |
| `GET` | `/api/listing-quote` | What an event or business listing costs in a town, and whether that town is selling. |
| `GET` | `/api/listing-order` | Whether a listing order has been paid and whether the listing is live yet. |
| `POST` | `/api/subscribe` | Subscribe an address to a city. Free, and the only write that is not a purchase. |
| `POST` | `/api/listing-checkout` | Submit an event to a town's calendar. Returns a payment link; charges nothing. |
| `POST` | `/api/business-checkout` | Submit a business to a town's directory. Returns a payment link; charges nothing. |
| `POST` | `/api/operator-actions` | With `action: "guest-checkout"`, returns a payment link to lease a town as its operator. |

Worked examples:

- `GET https://mailerdot.com/api/cities`
- `GET https://mailerdot.com/api/cities-census?state=Texas&minPopulation=50000`
- `GET https://mailerdot.com/api/city/austin-texas`
- `GET https://mailerdot.com/api/archive/austin-texas/2026-03-22`
- `GET https://mailerdot.com/api/search?q=farmers%20market&limit=5`
- `GET https://mailerdot.com/api/weather/austin-texas`
- `GET https://mailerdot.com/api/facts/austin-texas`
- `GET https://mailerdot.com/api/territories?city=Austin&state=Texas`
- `GET https://mailerdot.com/api/oembed?url=https%3A%2F%2Fmailerdot.com%2Faustin-texas`
- `GET https://mailerdot.com/api/listing-quote?slug=austin-texas`
- `GET https://mailerdot.com/api/listing-order?id=lo_00000000-0000-0000-0000-000000000000`
- `POST https://mailerdot.com/api/subscribe`
- `POST https://mailerdot.com/api/listing-checkout`
- `POST https://mailerdot.com/api/business-checkout`
- `POST https://mailerdot.com/api/operator-actions`

## Buying things

No endpoint here can charge anyone. Every purchase call ends at a hosted checkout URL on our merchant of record's domain, and a person has to open it and enter card details before money moves. Nothing publishes until payment clears (paid tiers) or a person approves it (free tier).

Both products work the same way:

1. **Price it** — a `GET` that reserves nothing.
2. **Create the checkout** — a `POST` that screens the request and returns a hosted checkout URL.
3. **A person pays** — on our merchant of record's page. Nothing before this step moves money.
4. **Follow it** — `GET /api/listing-order` for a listing; a town lease activates on the payment webhook.

### Town lease (operator)

Exclusive commercial rights to one U.S. town's daily newsletter. The edition writes and sends itself — an operator never writes a word — and the operator takes half the net on every listing sold in their town.

- **Price:** $10/month for the first 1,000 subscribers, then +$5/month per additional 1,000.
- **Quote:** `GET /api/territories?city=Austin&state=Texas`
- **Buy:** `POST /api/operator-actions — body: {"action":"guest-checkout","city":"Austin","state":"Texas"}`
- **Before anything publishes:** The town is held for a short reservation window and released if checkout is not completed. The email entered at checkout becomes the operator account.

### Event listing

An event on a town's public calendar, and on the featured tier, inside the daily email.

- **Price:** Free (reviewed by a person), $10 flat, or $25 featured. One-off, not a subscription.
- **Quote:** `GET /api/listing-quote?slug=austin-texas`
- **Buy:** `POST /api/listing-checkout`
- **Before anything publishes:** The compliance screen runs inside the request, so a rejected submission is answered with a reason and never gets a payment link. A paid listing publishes only after payment clears; a free one only after a person approves it.

### Business directory listing

A business in a town's directory for 30 days, and on the featured tier, a weekly mention in the daily email.

- **Price:** Free (reviewed by a person), $10 flat, or $25 featured. One-off, not a subscription.
- **Quote:** `GET /api/listing-quote?slug=austin-texas`
- **Buy:** `POST /api/business-checkout`
- **Before anything publishes:** Same screen-then-pay contract as an event listing. The 30-day run starts the day payment clears.

A listing checkout answers `200` for every screened outcome — exactly one of `checkoutUrl`,
`rejected`, `held` or `queued` is present. Branch on which field is there, not on the status:
"the answer is no" is a successful request.

```bash
# What does a listing cost in this town?
curl -s 'https://mailerdot.com/api/listing-quote?slug=austin-texas'

# Screen it and get a payment link
curl -s 'https://mailerdot.com/api/listing-checkout' \
  -H 'content-type: application/json' -d '{
    "slug": "austin-texas", "tier": "listing",
    "title": "Riverside Night Market", "description": "Forty stalls along the water, live music from 7pm.",
    "url": "https://example.com/night-market", "startsAt": "2026-09-12",
    "buyerEmail": "you@example.com"
  }'
```

## Errors

Every failure is `application/problem+json` (RFC 9457) — never an HTML page, on any path
under `/api/`, including ones that do not exist. Branch on `code`; it is stable.

```json
{
  "type": "https://mailerdot.com/developers#error-city_not_found",
  "title": "No MailerDot city for that slug",
  "status": 404,
  "detail": "No MailerDot city matches the slug \"atlantis-texas\".",
  "instance": "/api/city/atlantis-texas",
  "code": "city_not_found",
  "resolution": "Fetch https://mailerdot.com/api/cities for every city with a published edition. A U.S. city with no MailerDot edition yet has no data to return; subscribing to it creates one.",
  "documentation": "https://mailerdot.com/developers",
  "error": "No MailerDot city matches the slug \"atlantis-texas\"."
}
```

`error` duplicates `detail` for older clients. `issues[]` lists per-field failures on a
validation error. `resolution` says what to do next — it is written to be acted on.

| Code | HTTP | Meaning | What to do |
|---|---|---|---|
| `city_not_found` | 404 | No MailerDot city for that slug | Fetch https://mailerdot.com/api/cities for every city with a published edition. A U.S. city with no MailerDot edition yet has no data to return; subscribing to it creates one. |
| `conflict` | 409 | Request conflicts with current state | Re-read the resource to see its current state, then decide whether to retry. |
| `edition_not_found` | 404 | No edition published for that date | Fetch https://mailerdot.com/api/city/{slug} for the dates that exist. Editions publish each morning in the city's own timezone, so today's may not exist yet. |
| `endpoint_not_found` | 404 | No such API endpoint | Every public endpoint is listed in https://mailerdot.com/openapi.json and https://mailerdot.com/.well-known/api-catalog. Human-readable docs: https://mailerdot.com/developers. |
| `forbidden` | 403 | Not permitted | The credential is valid but does not grant this action. Nothing to retry. |
| `internal_error` | 500 | Unexpected server error | Retry once; this is usually transient. If it persists, report it at https://mailerdot.com/contact with the request path and time. |
| `invalid_json` | 400 | Request body is not valid JSON | Send a JSON body with `Content-Type: application/json`, then retry. |
| `invalid_slug` | 400 | City slug is not well-formed | Slugs are `{city}-{state-name}`, lowercased and hyphenated — `austin-texas`, `san-francisco-california`. List valid slugs at https://mailerdot.com/api/cities. |
| `method_not_allowed` | 405 | HTTP method not allowed | Use one of the methods in the `Allow` response header. Per-endpoint methods are in https://mailerdot.com/openapi.json. |
| `missing_slug` | 400 | City slug is missing | Put a city slug in the path, e.g. `/api/city/austin-texas`. List valid slugs at https://mailerdot.com/api/cities. |
| `not_found` | 404 | Resource not found | Check the identifier and retry. The available endpoints are listed at https://mailerdot.com/openapi.json. |
| `payload_too_large` | 413 | Request body is too large | Send a smaller body. The per-endpoint limit is in the `detail` field. |
| `rate_limited` | 429 | Rate limit exceeded | Back off and retry after the `Retry-After` header's number of seconds. |
| `service_misconfigured` | 503 | Service is misconfigured | The deployment is missing required configuration and cannot serve the API. This is our problem, not the caller's — retry later or report it at https://mailerdot.com/contact. |
| `unauthorized` | 401 | Authentication required | This endpoint is not part of the public API. Sign in at https://mailerdot.com/operators — the public, unauthenticated surface is documented at https://mailerdot.com/developers. |
| `upstream_error` | 502 | An upstream service failed | A third-party dependency failed or timed out. Retry with backoff. |
| `validation_error` | 400 | Request validation failed | Read `issues[]` for the per-field failures, fix the request, and retry. Request shapes are documented at https://mailerdot.com/openapi.json. |
| `weather_unavailable` | 404 | No weather available for that city | The city resolved but neither a stored snapshot nor a live forecast was available. Retry later; weather is refreshed with each morning's edition. |

## Markdown twins

Every page has one. Append `.md` to the path, or send `Accept: text/markdown`:

- `https://mailerdot.com/{slug}.md` — A city page: latest edition, archive index, census table.
- `https://mailerdot.com/{slug}/{YYYY-MM-DD}.md` — One dated edition, in full.
- `https://mailerdot.com/cities.md` — The whole city directory.
- `https://mailerdot.com/states/{state}.md` — Every tracked city in one state, by population.
- `https://mailerdot.com/feed.md` — The 30 most recent editions across all cities.
- `https://mailerdot.com/developers.md` — This page.

## MCP server

`https://mailerdot.com/mcp` speaks the Model Context Protocol over streamable HTTP.
No authentication. Point any MCP client at it:

```json
{
  "mcpServers": {
    "mailerdot": {
      "url": "https://mailerdot.com/mcp"
    }
  }
}
```

Tools:

- `list_cities` — Every city with an active newsletter, grouped by state.
- `get_city` — City overview: subscribers, census demographics, recent editions.
- `get_archive` — Read a past edition as markdown.
- `search_archives` — Full-text search across every edition.
- `get_weather` — Today's weather for a city.
- `get_town_quote` — Whether a town can be leased, and the monthly price.
- `start_town_checkout` *(writes)* — Payment link to lease a town. Charges nothing.
- `get_listing_prices` — What an event or business listing costs in a town.
- `create_event_listing` *(writes)* — Submit an event; returns a payment link. Charges nothing.
- `create_business_listing` *(writes)* — Submit a business; returns a payment link. Charges nothing.
- `get_listing_order` — Whether an order is paid and the listing is live.

The tools marked *(writes)* are the commerce ones. They are annotated `readOnlyHint: false`
so a client can gate them, and they return a payment link rather than making a purchase.

It also serves `resources/list` (site overview, OpenAPI spec, live city directory,
recent editions, these docs), `resources/templates/list` (per-city and per-edition
URI templates), and `prompts/list`.

## Discovery

- [OpenAPI 3.1 specification](https://mailerdot.com/openapi.json) (`application/json`) — The full contract: every endpoint, schema, and error code. Generate a client from it.
- [MCP server](https://mailerdot.com/mcp) (`application/json`) — Model Context Protocol over streamable HTTP. Tools, resources, and prompts.
- [API catalog (RFC 9727)](https://mailerdot.com/.well-known/api-catalog) (`application/linkset+json`) — Linkset pointing at everything on this list. The standard discovery entry point.
- [Agent Skills index](https://mailerdot.com/.well-known/agent-skills/index.json) (`application/json`) — A SKILL.md an agent can load to learn the URL shapes without crawling for them.
- [llms.txt](https://mailerdot.com/llms.txt) (`text/plain`) — What MailerDot is, in one page, written for a language model.
- [llms-full.txt](https://mailerdot.com/llms-full.txt) (`text/plain`) — The long form: product, editorial policy, operator terms, and API notes.
- [RSS feed](https://mailerdot.com/feed.xml) (`application/rss+xml`) — The most recent editions across every city.
- [Sitemap index](https://mailerdot.com/sitemap.xml) (`application/xml`) — Every indexable URL: cities, states, and dated editions.

## Limits and conventions

- No API key, no account, no rate limit on the read endpoints. Be reasonable and cache what you fetch — responses carry real `Cache-Control` headers, so honouring them costs you nothing.
- Every read endpoint sends `Access-Control-Allow-Origin: *`, so browser-side agents can call them directly.
- Markdown responses carry an `X-Markdown-Tokens` header with a rough token estimate, so you can decide whether to fetch a full edition or stop at the summary.
- Editions publish each morning in the city's own timezone. Asking for today's edition before it publishes returns a 404 with the `edition_not_found` code, not the previous day's content.
- Coverage is the United States only.

## Support

- Questions and bug reports: https://mailerdot.com/contact (we reply within 3 business days)
- Security reports: https://mailerdot.com/.well-known/security.txt
- Terms: https://mailerdot.com/terms · Privacy: https://mailerdot.com/privacy
