ravenhook

Documentation

Ravenhook receives email sent to any address under your namespace and hands it back over an authenticated HTTP API. There is no SDK to install and nothing to create in advance — onboarding is two strings.

What you need

RAVENHOOK_TOKENYour bearer token. Shown once, when your subscription starts.
RAVENHOOK_DOMAINYour namespace domain, like ex4mple0ns.inbox.ravenhook.dev. Read it from GET /namespace.

That is the whole setup. There is no dashboard to configure and no inbox to register.

Addresses are not created

Every address under your namespace already works. Invent one in your test and mail sent to it arrives:

mfa@ex4mple0ns.inbox.ravenhook.dev
run-8f3a@ex4mple0ns.inbox.ravenhook.dev
literally-anything@ex4mple0ns.inbox.ravenhook.dev
  • any name works
  • your namespace
  • fixed for everyone

This is what makes parallel test runs possible. Ten tests sharing one address cannot tell whose code is whose — the messages are genuinely indistinguishable, so no filtering fixes it. Giving each run its own address is the only answer, and because addresses cost nothing, it is string concatenation.

Your first call

Drive your signup, then read the code back. The request holds open until mail lands, so there is no polling loop to write:

import os, re, uuid, requests

TOKEN  = os.environ["RAVENHOOK_TOKEN"]
DOMAIN = os.environ["RAVENHOOK_DOMAIN"]

inbox = f"signup-{uuid.uuid4()}@{DOMAIN}"

reply = requests.get(
    "https://api.ravenhook.dev/messages/latest",
    params={"address": inbox, "wait": 25},
    headers={"Authorization": f"Bearer {TOKEN}"},
    timeout=30,
)

body = reply.json()["message"]["text_body"]
code = re.search(r"\d{6}", body).group()
const TOKEN = process.env.RAVENHOOK_TOKEN;
const DOMAIN = process.env.RAVENHOOK_DOMAIN;

const inbox = `signup-${crypto.randomUUID()}@${DOMAIN}`;

const params = new URLSearchParams({ address: inbox, wait: "25" });
const res = await fetch(`https://api.ravenhook.dev/messages/latest?${params}`, {
  headers: { Authorization: `Bearer ${TOKEN}` },
});

const body = (await res.json()).message.text_body;
const code = body.match(/\d{6}/)[0];
curl -s "https://api.ravenhook.dev/messages/latest?address=mfa@ex4mple0ns.inbox.ravenhook.dev&wait=25" \
  -H "Authorization: Bearer $RAVENHOOK_TOKEN"

If nothing arrives before wait elapses you get 200 with "message": null — not an error. A timeout is a normal answer, and treating it as a failure would be indistinguishable from the service being down.

Endpoints

EndpointWhat it does
GET /messages/latestReturns the newest message for one address, holding the connection open until mail arrives. This is the endpoint your tests use.
GET /messagesSummaries of every message in your namespace, newest first. Useful for debugging what actually arrived.
GET /messages/{id}The full message: both bodies, every header, and attachment metadata.
DELETE /messages/{id}Remove a message before it expires on its own.
GET /namespaceThe namespace allocated to your account and the domain built from it. You need this once, during setup.
GET /healthWhether the service can actually serve requests. No token required — point your uptime monitor here.

Limits

RetentionMessages are deleted automatically 24 hours after they arrive
Per namespaceThe newest 500 messages are kept; older ones drop off as new mail arrives
Per addressThe newest 100 messages
Message sizeMessages over 1 MB are rejected at the SMTP level and never stored
AddressesUnlimited, and nothing is created in advance
Inbound rate300 messages a minute per namespace. There is no monthly quota
API requests3000 a minute per namespace
Concurrent waits200 open waiting requests at once. Calls with wait=0 do not count

A message that worked a moment ago can legitimately become 404. That is retention doing its job, not a bug.

Status codes

CodeMeaning
200Fine — including "no message yet"
204Deleted
400Malformed address
401Token missing, malformed, or unknown
403Account has no namespace allocated
404Message absent, expired, or outside your namespace
422wait above the cap, or limit out of range
429Too many requests, or too many waiting at once
503Storage unreachable

About 429

Two separate limits, both per account, both carrying a Retry-After header.

  • 3000 requests a minute. Long-polling means a hundred parallel tests make only a few hundred requests a minute, so this is hard to reach by accident.
  • 200 concurrent waiting requests. This is the one that bounds parallel runs. A call with wait=0 never counts against it, so it keeps working when you are at the limit.

Back off rather than tightening your retry loop. A 429 on the waiting endpoint means too many simultaneous waits, so retrying harder makes it worse.