API reference

A free JSON API over the Postgres mailing lists — searchable, threaded, structured. Free means no cost; you still need an account and an API key.

Base URL

All endpoints live under /api/v1 and return application/json.

https://horton.kehvyn.dev/api/v1

Authentication

Every request needs a key. There is no anonymous access. Mint keys at /users/api-keys (you'll need to register first). The raw key is shown exactly once; only a hash is stored. Keys carry an optional expiry and can be revoked.

Send it as a bearer token (checked first) or the X-API-Key header. Keys are prefixed hml_.

curl -H "Authorization: Bearer hml_…" https://horton.kehvyn.dev/api/v1/lists
# or
curl -H "X-API-Key: hml_…" https://horton.kehvyn.dev/api/v1/lists

A missing, malformed, or expired key returns 401 with { "error": { "code": "unauthorized" } }.

Endpoints

Method Path Key params
GET /api/v1/lists
GET /api/v1/messages list, from, to, dir, limit, after, before
GET /api/v1/messages/:b64id
GET /api/v1/messages/:b64id/thread
GET /api/v1/messages/:b64id/commits
GET /api/v1/messages/:b64id/refs
GET /api/v1/search q (required), sort, from, to, sender, committed, path, major, limit, after
GET /api/v1/senders q, sort, dir, limit, after, before
GET /api/v1/senders/:id
GET /api/v1/attachments limit, after, before
GET /api/v1/attachments/:id
GET /api/v1/attachments/:id/patch
GET /api/v1/source/activity path (required), major, limit, after, before
GET /api/v1/commits path, author, major, from, to, limit, after, before
GET /api/v1/commits/:sha
GET /api/v1/commits/:sha/thread
GET /api/v1/versions
GET /api/v1/versions/:major/gucs changed_since
GET /api/v1/imports list

:b64id is the base64url-encoded Message-ID header; :id is a numeric id. Attachment endpoints return metadata only — the API never returns file bytes.

Pagination

Collections use keyset (seek) pagination, not offsets. Pass limit (default 25, max 100) and follow the opaque next_cursor / prev_cursor from each response via after / before. Either cursor may be null at an edge. Setting both after and before is a 422; an undecodable cursor is a 400.

Search

GET /api/v1/search is the reason this exists. It runs Postgres full-text search over the whole corpus with a domain-tuned 'pg' text-search config the stock archive can't match.

  • q is required; a blank query is a 400.
  • Default order is relevance (ts_rank_cd). Relevance results are capped at 200 — narrow with filters, or pass sort=sent_at for the full, uncapped set in date order.
  • Filter with from / to (ISO-8601 bounds on sent_at) and sender (substring over email or display name).
  • Filter by linkage to the source tree: committed=true|false (the thread landed a commit via its Discussion: trailer), path= (the thread touched a source-path prefix — a landed commit changed a file under it, or a thread message carries a patch touching it), and major= (the thread landed a commit that first shipped in that major, e.g. major=17). All three are extraction-tier filters — no fuzzy matching, so pre-2016 threads (before Discussion: trailers) rarely match.

What the 'pg' config does

  • Synonyms. Product spellings collapse to one term — postgres, postgresql, and pgsql all match. Common abbreviations map to their canonical term: autovacautovacuum, txntransaction, idxindex.
  • Loanword plurals. Latin/Greek plurals fold to their singular stem — indices matches index, matricesmatrix, schemataschema.
  • Noise filtering. Emails, URLs, and bare numbers are dropped from the index, so a search for vacuum isn't drowned by quoted headers and log lines.

Response & error shapes

Every success is wrapped in a data envelope — an array for collections (with cursors alongside), an object for single resources.

{ "data": [ … ], "next_cursor": "…", "prev_cursor": null }

Errors carry a stable code slug (decoupled from the HTTP status and never renamed), a human message, and — only when relevant — details.

{ "error": { "code": "invalid_params", "message": "…", "details": { "param": "limit" } } }
Code HTTP When
invalid_message_id 400 The message-id in the URL is not valid.
invalid_cursor 400 The pagination cursor is not valid.
invalid_query 400 A blank or missing search query (q).
invalid_params 422 A bad limit, sort, dir, from/to, or after+before together.
unauthorized 401 Missing, malformed, or expired API key.
not_found 404 No resource with that identifier.
internal_error 500 An unexpected server error.

Examples

Full-text search, relevance order, filtered by sender and date:

curl -H "Authorization: Bearer $KEY" \
  "https://horton.kehvyn.dev/api/v1/search?q=replication+lag&sender=tom&from=2024-01-01&limit=25"

Messages on one list over a date range, oldest first:

curl -H "X-API-Key: $KEY" \
  "https://horton.kehvyn.dev/api/v1/messages?list=pgsql-hackers&from=2023-06-01&to=2023-06-30&dir=asc&limit=50"

Senders ranked by post count:

curl -H "Authorization: Bearer $KEY" \
  "https://horton.kehvyn.dev/api/v1/senders?sort=messages&dir=desc&limit=20"