API reference
Base URL: https://api.mailfade.dev
All endpoints return JSON unless noted otherwise. Errors use a stable shape:
{ "error": "machine_readable_code", "hint": "human readable context (optional)" }
Authentication
Most endpoints work without a key (free tier; rate-limited per IP per day). Paid endpoints — attachments, raw RFC822, HTML body, and any inbox bound to a custom domain — require:
Authorization: Bearer mf_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
Keys are returned exactly once at checkout. Store them in your CI secrets.
GET /inbox/:address
List the most recent messages in an inbox.
Path params
| Name | Type | Required | Description |
|---|---|---|---|
address | string | yes | Full address, e.g. [email protected]. URL-encode if necessary. |
Query params
| Name | Type | Default | Description |
|---|---|---|---|
limit | int | 25 | Max items to return (capped at 50). |
since | int | — | Unix ms — only return messages received after this timestamp. |
Response
{
"address": "[email protected]",
"count": 1,
"emails": [
{
"id": "01HF5K9X2E4...",
"sender": "[email protected]",
"subject": "Confirm your account",
"has_attachments": false,
"size_bytes": 4821,
"received_at": 1716729384213,
"expires_at": 1716733000000
}
]
}
GET /message/:id
Get a single message. Without a key, only the plain-text body is included. With a key, the rendered HTML body and full headers come back too.
Query params
| Name | Default | Description |
|---|---|---|
html | 1 | Set to 0 to skip the HTML fetch (saves one R2 round trip). |
Response
{
"id": "01HF5K9X2E4...",
"address": "[email protected]",
"sender": "[email protected]",
"to": ["[email protected]"],
"subject": "Confirm your account",
"text": "Click here to confirm: https://...",
"html": "<html>...</html>",
"attachments": [],
"size_bytes": 4821,
"received_at": 1716729384213,
"expires_at": 1716733000000
}
GET /message/:id/raw
Stream the raw RFC822 message as message/rfc822. Requires a key.
GET /attachment/:id
Stream a single attachment. Requires a key. Content-Type and Content-Disposition are set from the original message.
POST /keys
Create a checkout session for a Dev or Team plan.
Body
{ "plan": "dev", "rail": "stripe" }
rail accepts "stripe" (card subscription) or "lightning" (one-time
BTCPay invoice). Returns:
{
"invoice_id": "01HF5...",
"rail": "stripe",
"checkout_url": "https://checkout.stripe.com/..."
}
Redirect the user (or open it in a browser); the key is returned once on success, and you can also poll status with the next endpoint.
GET /keys/:invoiceId
Poll the status of a checkout session. Returns "status": "pending" until
the webhook fires; then returns "status": "paid" along with key_id.
Status codes
| Code | Meaning |
|---|---|
| 200 | OK |
| 400 | Bad request — invalid_address, invalid_plan, invalid_json |
| 401 | missing_or_invalid_key, key_required |
| 402 | key_inactive (revoked or expired) |
| 404 | not_found |
| 429 | free_tier_quota_exhausted or quota_exhausted |
| 503 | stripe_unavailable / lightning_unavailable — rail is not configured |
| 500 | internal_error |
Rate limits
Free tier: 100 requests per IP per UTC day across /inbox and /message.
Paid keys: monthly request budget by plan; resets at period_resets_at.
Both buckets return helpful headers:
X-RateLimit-Limit: 100
X-RateLimit-Remaining: 87