Catalog.beer API
Catalog.beer is an open database of 6,700+ breweries and 60,000+ beers with a
curated, versioned style taxonomy. The API is plain REST + JSON.
- Base URL: (HTTPS required — plain HTTP fails)
- Send on every request
- Send
Content-Type: application/json
on POST/PUT/PATCH
- Full API docs: https://catalog.beer/api-docs
This skill is always served current at
https://catalog.beer/skills/catalog-beer/SKILL.md. Copies installed from a zip
don't update themselves — if the
date above is months old, or the API
rejects a request in a way these instructions don't explain, fetch that URL and
follow it instead of this copy.
Authentication
HTTP Basic auth. The API key is the username; the password is blank.
bash
curl https://api.catalog.beer/beer/search?q=pliny \
-u "$CATALOG_BEER_API_KEY:" \
-H "Accept: application/json"
Getting a key (there is no signup API — accounts are created on the website):
- The user creates a free account at https://catalog.beer/signup
- They verify their email address (the key is until verified)
- The key is shown at https://catalog.beer/account
Ask the user for their key and read it from the
environment variable. Never hardcode or commit it. Keys include 1,000 free
requests/month; past that, usage bills at $1 per 1,000 requests
only if
the user has added a payment method at
https://catalog.beer/billing —
otherwise a
ends the month's access.
and
report status without counting against the limit. See
→ "Usage limits & billing" — and never call the
billing endpoints (checkout, spend cap, disable) unless the user explicitly
asks; they spend the user's money.
The contribution rules (non-negotiable)
Catalog.beer's value is that its data is verifiable. Contributions must
follow these rules:
- No source, no write. Only submit facts you have actually verified —
fetched from the brewery's own website, or read by the user off the can,
label, or menu in front of them. Never fill in ABV, IBU, style, or any
other fact from your own knowledge or memory, no matter how confident
you are. If a required fact (like ABV) can't be found, ask the user —
don't guess.
- Search before you create. Always call and
before POSTing. Names vary ("Russian River Brewing
Company" vs "Russian River") — search by the distinctive part of the name
and check the results before concluding something is missing. If the
entity exists, update it (PATCH) instead of creating a duplicate.
- Prefer PATCH for edits. is a full replacement: any optional
field you omit is cleared to null. Only use PUT when you intend to
replace the whole record.
- Never DELETE without the user explicitly asking for that specific
deletion. Deleting a brewer cascades to its beers and locations.
- When unsure about style, be less specific. Filing a beer as family
is correct; guessing when the brewery just says
"IPA" is wrong. See "Classifying styles" below.
- Read the reference before your first write to an entity type. Before
your first write to a brewer, beer, or location, read that entity's file
in (plus once per session). The
examples in this file show request shape, not the full contract — field
limits, clearing semantics, and edge cases live in the references.
Workflow: add a beer the user is drinking
The most common task. Order of operations:
1. GET /brewer/search?q={brewery name} → does the brewery exist?
2. If not: fetch the brewery's website, then POST /brewer
3. GET /brewer/{brewer_id}/beer → does the beer exist?
4. If not: find the beer on the brewery's site, then POST /beer
5. (Optional) POST /location + POST /address/{location_id} for taprooms
Step 2 — create the brewery (verify name/URL/description from their site).
is a subtitle shown in search results and is
limited to
160 characters — exceeding it returns a 400.
is
fetched live by the
API and rejects far more than bad syntax — see "The URL field bites" below:
bash
curl -X POST https://api.catalog.beer/brewer \
-u "$CATALOG_BEER_API_KEY:" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"name": "Alibi Brewing",
"url": "https://alibi.beer",
"short_description": "Neighborhood brewery in ..."
}'
Step 4 — create the beer (
,
,
, and a style are
required;
is a float
–
stored rounded to one decimal place,
an optional whole number
–
). Send the brewery's exact figure and
let the API round it — and when a label publishes a bound instead of a number
("Less than 0.5% ABV"), record the bound (
). Omit
when the brewery
doesn't publish one:
means the beer has no measurable bitterness, not that
you don't know. See
→ "Recording ABV" and "Recording IBU":
bash
curl -X POST https://api.catalog.beer/beer \
-u "$CATALOG_BEER_API_KEY:" \
-H "Content-Type: application/json" -H "Accept: application/json" \
-d '{
"brewer_id": "c1a5b1f0-...",
"name": "Kölsch",
"style": "Kölsch",
"abv": 4.8,
"description": "..."
}'
Successful creates return the full object including its
. Report the new
catalog.beer URL to the user:
https://catalog.beer/beer/{id}
.
Classifying styles
The taxonomy has three tiers —
class (
/
) →
family (26,
e.g.
,
) →
style (~200, e.g.
). A beer may be
filed at any tier; the API derives the broader tiers automatically.
-
Default: pass the brewery's own label as . It's matched against
canonical names + aliases — "NEIPA", "New England IPA", and "Juicy IPA" all
resolve to the same style — and stored verbatim alongside the match.
-
A label that doesn't resolve is rejected, not stored. There is no
"keep the label with a null
" fallback: the API returns
and writes
nothing. Marketing names ("Cali Pilsner",
"Margarita-inspired Gose") hit this constantly.
-
Recovery: resend the same verbatim label plus an explicit tier, in one
request. The explicit field classifies the beer; the label is stored
exactly as you sent it, so the brewery's own words survive:
json
{"style": "Margarita-inspired Gose", "style_id": "contemporary-gose"}
The 400 tells you what to send. It carries a
object —
, each with
,
,
,
,
,
and
— ranked best-first. Retry with the
best candidate that fits; no second lookup needed.
Check before you trust the order. It is the difference between
an answer and a guess:
| What to do |
|---|
| Your label is this style's name or alias. Take it. |
| Every word of your label is in its name or aliases. Usually right. |
| Only some words matched. Never take on faith. Read the list; prefer / or a catch-all. |
| Matched only our prose about the style. Weakest signal we return. |
Within a
level ties break on how many beers we hold in each style,
so a populous style can outrank a better-fitting rare one (for "Cali
Pilsner" every candidate is
and the closest is last of six). An
all-
list means the API did not recognise your label — that is a
cue to file one tier up, not to pick the first row.
and ride along when your label names a family
or super-class but no style matched outright — "Crisp American Lager"
returns the
class. Send
/
back the same way you
would a
.
appears when we could not match your
whole label and fell back to its last two words; the candidates describe
that shorter phrase, not what you sent.
GET /style/search?q={label}
and
are there if you want
to look further afield — and dropping marketing words from the query
("Crisp American Lager" → "American Lager") is usually what finds it.
Send
(style slug),
(family slug), or
(
/
) — the most specific field you send wins. When no real
style fits, use the nearest
catch-all style (
—
,
,
, …). It keeps the beer
in the right family and filed at style tier, which a bare
doesn't.
-
Always send the label with the tier, never the tier alone. With
and no
, the API substitutes the canonical style name —
"Margarita-inspired Gose" is stored as "Contemporary-Style Gose" and the
brewery's wording is lost.
-
File at the tier the evidence supports. Brewery says "IPA" → send
, not a guessed sub-style. Picking the tier is a mapping
judgment, not a fact you're inventing — but the label you send must be the
brewery's, verbatim.
-
If you had to guess, say so. flags a classification
for review. Omit it normally — the API derives it. Send
or
when your mapping is shakier than the request looks (style
inferred from the beer's
name, ambiguous brewery page). You can only
claim
less certainty this way, never more: an unmatched label sent as
is silently reduced. See
.
-
(beer/cider/perry/mead) is derived — never send it.
Style specs are also the API's best read feature:
returns curated ABV/IBU/SRM/OG/FG ranges sourced from BA/BJCP guidelines —
use it instead of recalling specs from memory.
The URL field bites
(on brewers and locations) is not just syntax-checked —
the API
fetches it live before accepting the write: a
request, 10s timeout,
up to 10 redirects, strict TLS verification, sent with the user agent
. Anything other than a final 2xx/3xx is treated as a
bad URL.
That produces false rejections on URLs that are perfectly correct:
- Bot protection / WAF (Cloudflare et al.) answering to a
non-browser user agent — common for breweries on hosted platforms
- Servers that answer to but serve fine
- Sites slower than 10s, expired/self-signed certs, geo-blocked hosts
Two consequences worth knowing before you start:
- A refused URL fails the entire request — with an
unreachable creates no brewer at all, not a brewer without a URL.
- A wrong URL already in the catalog can't be corrected if the correct
one is bot-protected: the PATCH 400s and the wrong URL stays.
When a write fails with
set (the message says "something
seems to be wrong with your URL" regardless of cause — it is not evidence
the URL is wrong):
- Retry once with the exact URL a browser lands on — ,
correct or bare host, no tracking params.
- If it fails again, resend without so the rest of the record is
still created or updated. Never let the URL sink the write. (If the
original was a PUT, retry as a PATCH — an omitted on PUT clears the
URL already on the record.)
- Tell the user plainly: the URL is correct, the API's reachability check
refused it, and the field was left unset. Don't record a substitute URL
(a Facebook page, an old domain) just to fill the field — a wrong URL is
worse than none.
Endpoint quick reference
| Task | Endpoint |
|---|
| Search beers / brewers / styles | · · (max 100) |
| Get one | · · · |
| A brewery's beers / locations | · |
| Breweries near me | GET /location/nearby?latitude=&longitude=
· · /location/city?city=&state=
|
| Create | · · , then POST /address/{location_id}
|
| Edit | etc. (partial) · (full replace — clears omitted fields) |
| Styles | (all, with ) · (families) · |
| My usage / billing status | · (never blocked by the usage limit, not counted) |
All entity IDs are 36-char UUIDs; style IDs are slugs. List endpoints use
cursor pagination: pass
back as
;
is only
present when
is true.
Common mistakes
- Guessing ABV/IBU because the brewery's site doesn't list them. Don't —
omit (optional) and ask the user for (required).
- Sending to mean "not listed". is a real, storable value meaning
no measurable bitterness; (or omitting the field) means unknown.
Getting this backwards writes a false fact about the beer.
- Reading an rounding as stale data. is stored to one decimal
place, so a record holding where the brewery says is
already right. Compare site figures to stored ones at one decimal before
deciding a field needs a PATCH, or a reconcile run fills up with writes that
change nothing.
- Skipping a non-alcoholic beer because the label says "Less than 0.5% ABV"
rather than a number. Record the published bound — → ,
"Under 4%" → — and say in your report that the stored figure is an
upper bound. Never pick an interior value like ; that one is invented.
- Writing a brewer longer than 160 characters →
400. Compose it as a one-line subtitle; put anything longer in
.
- Formatting a with Markdown — , ,
. The API stores exactly the bytes you send and catalog.beer
renders them as plain text, so the syntax appears literally on a public page
and a human has to go and strip it. Write prose. Newlines are the one thing
that survives — use blank lines for paragraphs.
- Treating a 400 as "the URL is wrong" and abandoning the
write, or swapping in a different URL. The API fetched the site and
something answered non-2xx — usually bot protection. Retry once, then
send the record without .
- Assuming an unmatched label is stored verbatim with a null
. It isn't — it's a and nothing is written. Resend the
label plus // together. This applies to PATCH
as well, where the whole patch is discarded: a and an sent
alongside an unresolvable are not saved either, even though
marks them (which means "passed validation", not
"was written"). Fix the field and resend the entire body.
- Sending without , which overwrites the brewery's label
with the canonical style name.
- Verifying a write against a list endpoint. List and nested rows are
compact — e.g. rows omit — so a
missing field there is not a failed write. Verify with the single-object
endpoint ().
- Creating a duplicate because search used the full legal name. Search the
distinctive word ("russian river", not "Russian River Brewing Company").
- Using PUT to change one field — it nulls every optional field you omitted.
- Sending or verification flags (,
) — these are server-controlled and cannot be set.
- Forgetting the address is a second request: creates
the location (needs + , ISO 3166-1 alpha-2);
POST /address/{location_id}
adds the street address (US only; needs
= street, plus either + or ).
- Giving a location a that's just the city, or leaving null when
a brewer runs several venues in one city. is for what the address
doesn't already say: a venue with its own name uses it ("The Barrel House"),
siblings in one city use the neighborhood ("South Park", "Bay Park"), and
a brewer's only location in a city needs no . Read the neighborhood off
the brewery's page — never supply one from your own knowledge of the city.
- Calling billing endpoints (
POST /billing/checkout-session
,
, ) without the user explicitly asking.
They spend the user's money. On a free-tier , report the options —
wait for the monthly reset, or add a payment method at
https://catalog.beer/billing — and let the user decide.
- Error responses use , , and per-field
/ objects — read to see exactly which
field failed and why.
Detailed references
Read the matching file before your first write to each entity type — the
examples above are shape, these are the contract. Read the rest as needed:
- references/beers.md — full beer endpoints, style resolution details
- references/brewers.md — full brewer endpoints
- references/locations.md — locations, addresses, nearby search
- references/styles.md — style taxonomy, objects, endpoints
- references/api-basics.md — auth, errors, pagination, usage limits, method semantics