fake.bg
REST · SSE · v1

API reference

Anonymous endpoints are free and need no key; they are rate-limited per IP to keep abuse out. API keys (Developer tier) unlock programmatic inbox creation, webhooks and per-key limits. Base URL:

https://fake.bg

In the examples, <domain> is one of the domains returned by GET /api/v1/domains/. Domains currently served: loading…

POST/api/v1/inboxes/generate

Generate an inbox (anonymous, free tier)

Request body
{
  "domain": "<domain>",      // optional; first served domain if omitted
  "local_part": "myalias"    // optional; random if omitted
}
Response
{
  "id": "0416d8e4-...",
  "address": "myalias@<domain>",
  "message_count": 0,
  "unread_count": 0,
  "inbox_expires_at": "2026-10-17T11:30:00Z"
}
POST/api/v1/inboxes

Programmatic inbox (Developer tier — auth required)

Request body
{
  "domain": "<domain>",
  "local_part": "checkout-tests",     // optional
  "label": "CI checkout regression",
  "ttl_seconds": 86400,                // null = persistent (paid)
  "webhook_url": "https://your.app/email-hook"
}
Response
{
  "id": "...",
  "address": "checkout-tests@<domain>",
  "label": "CI checkout regression",
  "webhook_url": "https://your.app/email-hook",
  "webhook_secret": "<48 hex chars — shown ONCE, save it>"
}
Requires Authorization: Bearer <jwt> OR X-API-Key. Webhook payloads are HMAC-SHA256 signed using the secret returned here (header X-VaultMail-Signature).
GET/api/v1/domains/

List domains

Response
{
  "domains": [
    { "name": "<domain>", "is_public": true, "tier_required": 0 },
    ...
  ]
}
GET/api/v1/inboxes/{address}

Get inbox + stats

Response
{ "id": "...", "address": "...", "message_count": 4, "unread_count": 1, ... }
Use /api/v1/inboxes/{address}/stats for aggregate counters (unread, OTPs, total bytes, attachments).
GET/api/v1/inboxes/{address}/messages?limit=50

List messages

Response
[
  {
    "id": "ecbfa70e-...",
    "from_address": "noreply@example.com",
    "subject": "Confirm your account (code 482193)",
    "received_at": "...",
    "is_read": false,
    "has_otp": true,
    "raw_size": 533,
    "link_count": 1,
    "attachment_count": 0
  }
]
GET/api/v1/messages/{id}

Read a message

Response
{
  "id": "...",
  "from_address": "noreply@example.com",
  "subject": "...",
  "body_text": "...",
  "body_html": "...",
  "links": [{ "index": 0, "url": "https://..." }],
  "attachments": [{ "filename": "...", "size": ... }],
  "otp_detected": "482193",
  "headers": { "From": "...", "DKIM-Signature": "..." }
}
Hitting this endpoint marks the message read.
GET/api/v1/messages/{id}/raw

Download raw .eml

Response
(application/rfc822 bytes; Content-Disposition: attachment)
GET/api/v1/messages/{id}/otp

Extract OTP

Response
{
  "message_id": "...",
  "otp": "482193",
  "found": true,
  "subject": "Confirm your account (code 482193)"
}
If the message wasn't tagged at delivery time, this re-runs the extractor. Returns found=false when nothing matches.
GET/api/v1/messages/{id}/links

List links

Response
{
  "message_id": "...",
  "count": 2,
  "links": [
    { "index": 0, "url": "https://example.com/verify?token=abc" },
    { "index": 1, "url": "https://example.com/help" }
  ]
}
POST/api/v1/api-keys

Create API key

Request body
{ "label": "CI", "expires_in_days": 90 }
Response
{
  "id": "...",
  "key_prefix": "abc123def456",
  "label": "CI",
  "plaintext": "vlt_abc123def456_<48 hex secret>"
}
plaintext is shown ONCE. Use as X-API-Key header. Tier 3 (Developer) or higher required.
GET/api/v1/api-keys

List my API keys

Response
[
  { "id": "...", "key_prefix": "abc...", "label": "CI",
    "is_active": true, "last_used_at": "...", "created_at": "..." }
]
DELETE/api/v1/api-keys/{id}

Revoke API key

Response
204 No Content
GET/api/v1/sse/inbox/{address}

Real-time stream (SSE)

Response
event: connected
data: {"address":"..."}

event: message
data: {"type":"new_message","id":"...","from":"...","subject":"...","has_otp":true}
Long-lived connection. Server emits 'connected' on subscribe, 'message' on every delivered email (type: new_message) and when expired messages are deleted (type: messages_deleted, with an ids list), plus periodic 'ping' keepalives. Use EventSource in the browser, or a streaming HTTP client elsewhere.
POST/api/v1/inboxes/{address}/mark-all-read

Mark all as read

Response
204 No Content
POST/api/v1/inboxes/{address}/clear

Clear inbox

Response
204 No Content
Deletes every message. Address remains active.
DELETE/api/v1/messages/{id}

Delete a message

Response
204 No Content

The temporary-phone service still runs in simulation mode — the endpoints respond, but no real numbers are purchased.

GET/api/v1/phones/services?q=goog

Temporary phone — list services + prices

Response
[
  {
    "service_name": "google",
    "display_name": "Google",
    "capability": "sms",
    "number_type": "mobile",
    "price_usd": "0.85",
    "is_test": false
  }
]
Served from our cached catalog. Only priced, enabled services appear.
POST/api/v1/phones/verifications

Temporary phone — rent a number

Request body
{
  "service_name": "google",
  "label": "signup test",          // optional
  "area_codes": ["205"]            // optional preference
}
Response
{
  "id": "9f1c...",
  "number": "+15551230000",
  "service_name": "google",
  "status": "pending",
  "price_usd": "0.85",
  "otp_code": null,
  "ends_at": "2026-10-17T12:20:00Z"
}
Debits your phone-credit wallet at the listed price. Numbers are single-use and expire on their own. If no SMS ever arrives — you cancel, or it times out — the full price is refunded automatically. 402 means insufficient credit (nothing was bought).
GET/api/v1/phones/verifications/{id}/otp?wait=180

Temporary phone — wait for the code

Response
{
  "verification_id": "9f1c...",
  "number": "+15551230000",
  "code": "482913",
  "status": "completed",
  "waited_seconds": 21.4
}
Long-polls until the SMS lands or `wait` seconds elapse (clamped server-side). Returns code:null on timeout — call again to keep waiting.
POST/api/v1/phones/verifications/{id}/cancel

Temporary phone — release + refund

Response
{
  "id": "9f1c...",
  "status": "canceled",
  "price_usd": "0.85",
  "refunded_usd": "0.85"
}
Full refund when no SMS was ever received. Once a code has arrived the sale is final and refunded_usd stays 0.00.
GET/api/v1/phones/wallet

Temporary phone — credit balance

Response
{
  "balance_usd": "12.60",
  "lifetime_topup_usd": "20.00",
  "lifetime_spend_usd": "7.40",
  "currency": "USD",
  "entries": [
    {"kind": "purchase", "amount_usd": "-0.85", "balance_after_usd": "12.60", ...}
  ]
}
GET/api/v1/sse/phone/{verification_id}

Temporary phone — live SMS stream (SSE)

Response
event: connected
data: {"verification_id":"9f1c...","number":"+15551230000"}

event: sms
data: {"type":"sms","from":"Google","content":"G-482913 is your code","code":"482913"}
Owner-only. Same connection model as the inbox stream.

End-to-end example

# 1. Generate an inbox
ADDR=$(curl -s -X POST https://fake.bg/api/v1/inboxes/generate \
  -H 'Content-Type: application/json' -d '{}' | jq -r .address)
echo "Inbox: $ADDR"

# 2. Watch for messages in another terminal
curl -N https://fake.bg/api/v1/sse/inbox/$ADDR

# 3. Send a test email to that address (from any provider)

# 4. List messages
curl https://fake.bg/api/v1/inboxes/$ADDR/messages

# 5. Read OTP
curl https://fake.bg/api/v1/messages/<id>/otp | jq

Webhook delivery

When a message arrives at an inbox with webhook_url set, we POST a JSON payload to that URL. Up to 4 attempts (initial + 3 retries), backoff 5s → 25s → 125s. Verify X-VaultMail-Signature before trusting the payload.

Headers

X-VaultMail-Signature: sha256=<hex hmac of raw body, key = webhook_secret>
X-VaultMail-Event: message.received
X-VaultMail-Attempt: 1
Content-Type: application/json

Verify (Python)

import hmac, hashlib

def verify(secret: str, raw_body: bytes, signature_header: str) -> bool:
    expected = "sha256=" + hmac.new(
        secret.encode(), raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature_header)

Verify (Node)

import { createHmac, timingSafeEqual } from "node:crypto";

export function verify(secret: string, rawBody: Buffer, header: string) {
  const expected = "sha256=" + createHmac("sha256", secret)
    .update(rawBody).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(header));
}