Skip to content

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

NameTypeRequiredDescription
addressstringyesFull address, e.g. pw-99@mailfade.dev. URL-encode if necessary.

Query params

NameTypeDefaultDescription
limitint25Max items to return (capped at 50).
sinceintUnix ms — only return messages received after this timestamp.

Response

{
  "address": "pw-99@mailfade.dev",
  "count": 1,
  "emails": [
    {
      "id": "01HF5K9X2E4...",
      "sender": "verify@acme.com",
      "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

NameDefaultDescription
html1Set to 0 to skip the HTML fetch (saves one R2 round trip).

Response

{
  "id": "01HF5K9X2E4...",
  "address": "pw-99@mailfade.dev",
  "sender": "verify@acme.com",
  "to": ["pw-99@mailfade.dev"],
  "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

CodeMeaning
200OK
400Bad request — invalid_address, invalid_plan, invalid_json
401missing_or_invalid_key, key_required
402key_inactive (revoked or expired)
404not_found
429free_tier_quota_exhausted or quota_exhausted
503stripe_unavailable / lightning_unavailable — rail is not configured
500internal_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