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_TOKEN | Your bearer token. Shown once, when your subscription starts. |
RAVENHOOK_DOMAIN | Your 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
| Endpoint | What it does |
|---|---|
GET /messages/latest | Returns the newest message for one address, holding the connection open until mail arrives. This is the endpoint your tests use. |
GET /messages | Summaries 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 /namespace | The namespace allocated to your account and the domain built from it. You need this once, during setup. |
GET /health | Whether the service can actually serve requests. No token required — point your uptime monitor here. |
Limits
| Retention | Messages are deleted automatically 24 hours after they arrive |
| Per namespace | The newest 500 messages are kept; older ones drop off as new mail arrives |
| Per address | The newest 100 messages |
| Message size | Messages over 1 MB are rejected at the SMTP level and never stored |
| Addresses | Unlimited, and nothing is created in advance |
| Inbound rate | 300 messages a minute per namespace. There is no monthly quota |
| API requests | 3000 a minute per namespace |
| Concurrent waits | 200 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
| Code | Meaning |
|---|---|
200 | Fine — including "no message yet" |
204 | Deleted |
400 | Malformed address |
401 | Token missing, malformed, or unknown |
403 | Account has no namespace allocated |
404 | Message absent, expired, or outside your namespace |
422 | wait above the cap, or limit out of range |
429 | Too many requests, or too many waiting at once |
503 | Storage 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=0never 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.