bril.beer
@bril.beer
API

API docs

Everything the website does goes through this API. There are no keys and no accounts. Machine-readable description: OpenAPI 3.1.

Overview Quickstart Waiting for mail Create an address Inbox status List messages Wait for a message Event stream Latest message Get a message Raw, HTML, attachments Delete Service info Message object Errors Limits What to trust Use it from Claude

Overview

Quickstart

The usual job: get a fresh address, use it to sign up somewhere, and collect the verification code.

# 1. Get an address nobody can guess
ADDR=$(curl -sX POST https://bril.beer/api/v1/inboxes | jq -r .address)
echo "$ADDR"

# 2. Use it. Replace this with your own sign-up step
./sign-up.sh "$ADDR"

# 3. Hold the request open until mail arrives, then print the code.
#    after=0 means "any mail in this inbox", so it can't be missed.
curl -s "https://bril.beer/api/v1/inboxes/$ADDR/messages/wait?after=0&timeout=120" \
  | jq -r '.codes[0]'

In Python, using requests:

import requests

API = "https://bril.beer/api/v1"
inbox = requests.post(f"{API}/inboxes", timeout=10).json()

sign_up(inbox["address"])  # your own code

response = requests.get(
    inbox["wait_url"],
    params={"after": inbox["cursor"], "timeout": 120, "from": "example.com"},
    timeout=130,
)
if response.status_code == 204:
    raise TimeoutError("no mail arrived")

message = response.json()
print(message["subject"], message["codes"], message["links"])

In JavaScript, in Node or a browser:

const API = "https://bril.beer/api/v1";
const inbox = await (await fetch(`${API}/inboxes`, { method: "POST" })).json();

await signUp(inbox.address); // your own code

const url = `${inbox.wait_url}?after=${inbox.cursor}&timeout=120`;
const response = await fetch(url);
if (response.status === 204) throw new Error("no mail arrived");

const message = await response.json();
console.log(message.codes[0], message.links);

Waiting for mail

There are three ways, from most to least recommended:

  1. Long polling suits scripts and tests. One request stays open until a match arrives.
  2. Server-sent events suit long-running watchers. One connection delivers every new message.
  3. Plain polling works too. Send If-None-Match with the last ETag and unchanged results return an empty 304.

Avoid the race. If mail might arrive before your wait request starts, pass a starting point. Read cursor from inbox status before you trigger the email, then send after=<cursor>. Each message has a seq that works the same way, so repeating after=<seq> walks through an inbox one message at a time. For a new random address, after=0 is enough.

Create an address

POST /inboxes

Returns a random address with 50 bits of randomness. Nothing is stored; it only saves you inventing a name. The JSON body is optional.

Body fieldDescription
prefixOptional readable start of the name, up to 40 characters. {"prefix":"ci"} gives ci-k3v9qzt2ma@bril.beer.
{
  "address": "mossy-condor-k3v9qzt2ma@bril.beer",
  "localpart": "mossy-condor-k3v9qzt2ma",
  "message_count": 0,
  "cursor": 0,
  "messages_url": "https://bril.beer/api/v1/inboxes/mossy-condor-k3v9qzt2ma@bril.beer/messages",
  "wait_url": "https://bril.beer/api/v1/inboxes/mossy-condor-k3v9qzt2ma@bril.beer/messages/wait",
  "events_url": "https://bril.beer/api/v1/inboxes/mossy-condor-k3v9qzt2ma@bril.beer/events",
  "web_url": "https://bril.beer/inbox/mossy-condor-k3v9qzt2ma"
}

Inbox status

GET /inboxes/{address}

Returns the same fields as above with the current message_count and cursor. The cursor is the seq of the newest message, or 0 for an empty inbox.

List messages

GET /inboxes/{address}/messages

Returns message summaries, newest first. Supports ETag and If-None-Match.

QueryDescription
limit1 to 200, default 50.
orderdesc (default) or asc.
afterOnly messages with a greater seq.
beforeOnly messages with a smaller seq.
sinceOnly messages received at or after this time: RFC 3339 or Unix seconds.
fromSender name or address contains this text, ignoring ASCII case.
subjectSubject contains this text, ignoring ASCII case.
{
  "address": "demo@bril.beer",
  "count": 1,
  "cursor": 4182,
  "next_before": null,
  "next_after": null,
  "messages": [
    {
      "id": "b3k2q7xv9d4m6n8p2r5t7w3y4a",
      "seq": 4182,
      "address": "demo@bril.beer",
      "from": { "name": "Example", "address": "hello@example.com" },
      "to": [ { "name": "", "address": "demo@bril.beer" } ],
      "cc": [],
      "reply_to": "",
      "envelope_from": "bounces@example.com",
      "subject": "Your verification code",
      "snippet": "Your verification code is 483920. It expires in 10 minutes.",
      "size": 4210,
      "sent_at": "2026-11-06T10:15:02Z",
      "received_at": "2026-11-06T10:15:04.219Z",
      "expires_at": "2026-11-09T10:15:04.219Z",
      "has_html": true,
      "attachment_count": 0,
      "codes": [ "483920" ],
      "url": "https://bril.beer/api/v1/inboxes/demo@bril.beer/messages/b3k2q7xv9d4m6n8p2r5t7w3y4a"
    }
  ]
}

To page through a long inbox, pass next_before as before. With order=asc, pass next_after as after. A null value means you have reached the end.

Wait for a message

GET /inboxes/{address}/messages/wait

Holds the request open until a matching message exists, then returns the oldest match as a full message. If nothing arrives in time, the response is 204 No Content.

QueryDescription
timeoutSeconds to wait, 0 to 120, default 30.
afterOnly messages with a greater seq.
sinceOnly messages received at or after this time.
from, subjectThe same text filters as the list endpoint.

Without after or since, only mail arriving after the request starts counts. Give your HTTP client a timeout a little longer than timeout.

Event stream

GET /inboxes/{address}/events

A server-sent events stream. It first sends a ready event with the current cursor, then one message event with a message summary per new mail. The event id is the seq, so reconnecting clients resume through Last-Event-ID and miss nothing. Accepts after, since, from and subject. Comment lines arrive every 25 seconds as a heartbeat, and the server closes the stream after an hour, so reconnect automatically.

curl -N https://bril.beer/api/v1/inboxes/demo/events

retry: 3000
event: ready
data: {"address":"demo@bril.beer","cursor":4182}

id: 4183
event: message
data: {"id":"...","seq":4183,"subject":"Hello","codes":[], ...}

Latest message

GET /inboxes/{address}/messages/latest

Returns the newest matching message in full, or a 404 with code no_messages. Accepts after, since, from and subject.

Get a message

GET /inboxes/{address}/messages/{id}

Returns the full message object.

Raw, HTML, attachments

GET /inboxes/{address}/messages/{id}/raw

The original message as received (message/rfc822), byte for byte. Supports range requests.

GET /inboxes/{address}/messages/{id}/html

A sanitized, sandboxed HTML document for displaying in a frame. Remote images stay blocked unless you add images=1. For the unmodified HTML, use the html field of the message.

GET /inboxes/{address}/messages/{id}/attachments/{index}

Downloads one attachment. The URL is in attachments[].url. Files are always served as application/octet-stream, with the original type in attachments[].content_type.

Delete

DELETE /inboxes/{address}/messages/{id}

Deletes one message and returns 204.

DELETE /inboxes/{address}/messages

Deletes everything in the inbox and returns {"deleted": 3}. Anyone who knows the address can do this.

Service info

GET /meta

Returns the domain, retention, size limits, rate limits and server_time. Compare against server_time if you use since from a machine whose clock may be off.

GET /openapi.json

The OpenAPI 3.1 description of this API, for generating clients.

Message object

A full message has all the summary fields plus these:

FieldDescription
textPlain text body. If the mail only has HTML, this is derived from it.
htmlThe original HTML body, unsanitized. Never put it straight into a web page.
truncatedtrue if text or html was cut at 2 MiB. The raw download always has everything.
codesLikely verification codes, best guess first. Also present on summaries.
linksUp to 100 http(s) links in document order.
headersAll headers in their original order, as {"name", "value"}.
attachments{"index", "filename", "content_type", "size", "inline", "content_id", "url"}.
has_remote_imagesWhether the HTML appears to load remote images.
raw_url, html_url, web_urlLinks to the raw message, the sanitized HTML and the page on the website.

Errors

Every error has the same shape, and code is stable enough to branch on:

{ "error": { "code": "invalid_address", "message": "That is not a valid address" } }
StatusCodeMeaning
400invalid_addressNot a valid local part.
400wrong_domainThe address is on another domain.
400invalid_parameterA query or body field is invalid. The message names it.
400invalid_bodyThe body could not be read as JSON.
403reserved_addressA reserved address.
404message_not_foundDoesn't exist in this inbox, was deleted, or expired.
404no_messagesNothing matched latest.
404no_htmlThe message has no HTML part.
404attachment_not_foundNo attachment at that index.
404not_foundNo such endpoint.
405method_not_allowedWrong HTTP method. See the Allow header.
429rate_limitedSlow down. See Retry-After.
429too_many_streamsToo many open wait or event requests.
500internal_errorOur fault. Retry later.
503busyToo many downloads at once. See Retry-After.

Limits

Use it from Claude

There's a skill that teaches Claude how to use this API, and to treat everything in a message as untrusted. To install it for Claude Code:

mkdir -p ~/.claude/skills/bril-beer
curl -fsSL https://bril.beer/skill/SKILL.md -o ~/.claude/skills/bril-beer/SKILL.md

What to trust