📡 API Documentation

Integrate Phantom Mail into your apps. Generate disposable emails, stream your inbox in real time, send mail, and claim addresses programmatically.

Contents

Getting Started Authentication Plans & Limits Endpoints — POST /api/v1/generate — GET /api/v1/emails — GET /api/v1/emails/stream (SSE) — POST /api/v1/send (Premium) — GET /api/v1/status — POST /api/v1/claim Error Codes Fair Use Policy
Getting Started
Every API key ties to a Phantom Mail account. Sign in on the main site, open the Dashboard → API Key tab, and generate your key. Free keys start with pm_free_ and can receive only. Premium keys start with pm_pro_ and can receive and send.
Base URL https://mail.unknowns.app/api/v1

All API endpoints are served over HTTPS. Requests must include your API key in the X-API-Key header. Generated addresses use the @unkn0wn.qzz.io and @phant0m.qzz.io mail domains.

Authentication

Include your API key in every request using the X-API-Key HTTP header.

HTTP Header
X-API-Key: pm_free_your32characterapikey...
# or a premium key
X-API-Key: pm_pro_your32characterapikey...
SSE exception: the streaming endpoint (GET /api/v1/emails/stream) also accepts the key as a ?key= query parameter, because the browser EventSource API cannot set custom headers.
⚠️ Keep your API key secret. Never expose it in client-side code or public repositories. If compromised, regenerate it from your dashboard immediately.
Plans & Limits

API limits are applied per key, per day (UTC reset at midnight).

Capability
Free (pm_free_)
Premium (pm_pro_)
API receive
10 / day
500 / day
API send
Blocked
50 / day
Custom handle / domain choice
Inbox retention
1 hour
15 days

API send is blocked for free keys. Calling POST /api/v1/send with a pm_free_ key returns 403. When you exceed a daily limit you'll receive a 429 response.

Endpoints
POST /api/v1/generate Generate a disposable email address
🔑 Requires API Key

Generates a new disposable address with a server-side, human-looking username. No address history is stored — the returned keyId is the only way to re-claim the address later (see /api/v1/claim). Premium keys may request a specific domain and a custom handle.

Request Body (JSON, optional)

FieldTypeRequiredDescription
domainstringOptionalMail domain: unkn0wn.qzz.io or phant0m.qzz.io. Premium only; defaults to a random domain.
handlestringOptionalCustom handle (3–30 chars, a-z 0-9 . _ -). Premium only.

Responses

200 OK — Address generated

JSON
{
  "success": true,
  "address": "silentfox482@unkn0wn.qzz.io",
  "keyId": "k_9f3a...b21c",
  "expiresIn": 3600,
  "usage": { "today": 1, "limit": 10 }
}

keyId maps to the Ed25519 key held at generation. Store it to re-claim the address later. expiresIn is 3600 (1 h) for free keys; premium addresses can be saved for 15-day retention.

403 Forbidden — Custom handle/domain on a free key

JSON
{ "error": "Custom handle requires Premium" }
GET /api/v1/emails Retrieve emails for an address
🔑 Requires API Key

Returns received emails for a given address. Counts against your daily receive limit (10/day free, 500/day premium). Supports ETag / If-None-Match for cheap polling — a 304 response does not count against your limit.

Query Parameters

ParameterTypeRequiredDescription
addressstringRequiredThe full email address to check (e.g. silentfox482@unkn0wn.qzz.io)

Responses

200 OK

JSON
{
  "success": true,
  "address": "silentfox482@unkn0wn.qzz.io",
  "count": 1,
  "emails": [
    {
      "id": "email_abc123",
      "from": "noreply@example.com",
      "subject": "Your verification code",
      "body": "Your code is 123456",
      "hasHtml": true,
      "timestamp": 1710000000000
    }
  ]
}

404 Not Found — Address not found or expired

JSON
{ "error": "Address not found or expired" }
GET /api/v1/emails/stream Real-time inbox stream (SSE)
🔑 Requires API Key

Opens a Server-Sent Events stream that pushes inbox changes in real time — no polling required. The connection stays open, sends a keep-alive comment every 25 seconds, and browser EventSource clients auto-reconnect on disconnect. Because EventSource can't set headers, the key may be passed as ?key=.

Query Parameters

ParameterTypeRequiredDescription
addressstringRequiredThe address to stream (e.g. silentfox482@unkn0wn.qzz.io)
keystringOptionalYour API key, if you cannot send the X-API-Key header (browser EventSource).

Event Types

init— snapshot of current emails, sent immediately on connect
new_email— a new message arrived (payload = the email object)
deleted— a message was deleted (payload = { id })
error— stream error (e.g. address expired); stream then closes
bye— server is closing the stream gracefully

Wire Format

text/event-stream
event: init
data: {"address":"silentfox482@unkn0wn.qzz.io","emails":[]}

: keep-alive

event: new_email
data: {"id":"email_abc123","from":"noreply@example.com","subject":"Hi","timestamp":1710000000000}

event: deleted
data: {"id":"email_abc123"}

event: bye
data: {"reason":"server_shutdown"}

Consume the stream (JavaScript / EventSource)

JavaScript
const address = 'silentfox482@unkn0wn.qzz.io';
const key = 'pm_free_your_api_key_here';
const url = `https://mail.unknowns.app/api/v1/emails/stream`
  + `?address=${encodeURIComponent(address)}&key=${key}`;

const es = new EventSource(url); // auto-reconnects

es.addEventListener('init', e => {
  const snap = JSON.parse(e.data);
  console.log('Snapshot:', snap.emails.length, 'emails');
});
es.addEventListener('new_email', e => {
  const mail = JSON.parse(e.data);
  console.log('New:', mail.from, '—', mail.subject);
});
es.addEventListener('deleted', e => console.log('Deleted', JSON.parse(e.data).id));
es.addEventListener('bye', () => es.close());
es.addEventListener('error', e => console.warn('Stream error', e));

Consume the stream (Python / sseclient)

Python (requests + sseclient)
import json, requests, sseclient  # pip install sseclient-py

ADDRESS = "silentfox482@unkn0wn.qzz.io"
KEY = "pm_free_your_api_key_here"
URL = "https://mail.unknowns.app/api/v1/emails/stream"

resp = requests.get(
    URL,
    params={"address": ADDRESS},
    headers={"X-API-Key": KEY, "Accept": "text/event-stream"},
    stream=True,
)
client = sseclient.SSEClient(resp)
for event in client.events():
    if event.event == "new_email":
        mail = json.loads(event.data)
        print(f"New: {mail['from']} — {mail['subject']}")
    elif event.event == "bye":
        break

On the web app itself, real-time delivery is powered by Pusher WebSocket push (cluster ap2) with ETag polling as a fallback; the SSE endpoint above is the programmatic equivalent for API clients.

POST /api/v1/send Send an email (Premium only)
⭐ Premium API Key Required

Sends an email from one of your Phantom Mail addresses. Requires a Premium (pm_pro_) key — free keys receive 403. Anonymous sending is disabled; every send is tied to your account. Limited to 50 sends/day.

Request Body (JSON)

FieldTypeRequiredDescription
fromstringRequiredOne of your addresses (e.g. me@phant0m.qzz.io)
tostringRequiredRecipient email address
subjectstringRequiredMessage subject
textstringOptional*Plain-text body
htmlstringOptional*HTML body. *Provide at least one of text or html.

Example Request

cURL
curl -X POST https://mail.unknowns.app/api/v1/send \
  -H "X-API-Key: pm_pro_your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{
    "from": "me@phant0m.qzz.io",
    "to": "friend@example.com",
    "subject": "Hello from Phantom Mail",
    "text": "Sent via the Phantom Mail API."
  }'

200 OK

JSON
{ "success": true, "id": "msg_7f21ac" }

403 Forbidden — Free key attempted to send

JSON
{ "error": "Sending requires a Premium API key" }
GET /api/v1/status Key info and remaining quota
🔑 Requires API Key

Returns your plan tier and today's usage across receive and send quotas. Useful for checking remaining calls before a batch job.

200 OK

JSON
{
  "success": true,
  "plan": "premium",
  "usage": {
    "receive": { "today": 42, "limit": 500 },
    "send":    { "today": 3,  "limit": 50 }
  }
}
POST /api/v1/claim Re-claim an address (Ed25519)
🔑 Requires API Key

Phantom Mail stores no address history. Ownership is proven cryptographically: at generation you hold an Ed25519 key pair. To re-claim a previously-generated address, sign a challenge with that key. Only the holder of the original key can re-claim the address.

Challenge format: phantom-claim:{address}:{timestamp} — where timestamp is the current Unix time in milliseconds. Sign the exact UTF-8 bytes of this string with your Ed25519 private key. Send the publicKey and signature as base64.

Request Body (JSON)

FieldTypeRequiredDescription
addressstringRequiredThe address to re-claim
timestampnumberRequiredUnix time (ms) used to build the challenge
publicKeystringRequiredEd25519 public key, base64
signaturestringRequiredEd25519 signature of the challenge, base64

Build & sign the challenge (JavaScript / Web Crypto)

JavaScript (Web Crypto, Ed25519)
// keyPair was generated & saved when you first created the address:
// crypto.subtle.generateKey({ name: 'Ed25519' }, true, ['sign','verify'])
const address = 'silentfox482@unkn0wn.qzz.io';
const timestamp = Date.now();
const challenge = `phantom-claim:${address}:${timestamp}`;
const msg = new TextEncoder().encode(challenge);

const sigBuf = await crypto.subtle.sign(
  { name: 'Ed25519' }, keyPair.privateKey, msg
);
const pubRaw = await crypto.subtle.exportKey('raw', keyPair.publicKey);

const b64 = buf => btoa(String.fromCharCode(...new Uint8Array(buf)));

const res = await fetch('https://mail.unknowns.app/api/v1/claim', {
  method: 'POST',
  headers: {
    'X-API-Key': 'pm_free_your_api_key_here',
    'Content-Type': 'application/json'
  },
  body: JSON.stringify({
    address,
    timestamp,
    publicKey: b64(pubRaw),
    signature: b64(sigBuf)
  })
});
console.log(await res.json()); // { success: true, address }

200 OK

JSON
{ "success": true, "address": "silentfox482@unkn0wn.qzz.io" }

401 Unauthorized — Signature does not match

JSON
{ "error": "Invalid signature" }
Error Codes
StatusCodeDescription
400Bad RequestMissing or invalid parameters
401UnauthorizedAPI key missing/invalid, or claim signature mismatch
403ForbiddenFeature requires Premium (e.g. send, custom handle/domain)
404Not FoundAddress does not exist or has expired
429Too Many RequestsDaily receive/send limit exceeded
500Server ErrorInternal server error — try again
Fair Use Policy
Fair use & abuse protection. Beyond the published per-key daily quotas, Phantom Mail applies additional fair-use and abuse-protection limits — including inbound-rate ceilings and per-IP send limits — to keep the service healthy for everyone. These thresholds are intentionally undisclosed and may adjust dynamically. Legitimate integrations will never notice them; scraping, bulk abuse, or automated flooding may be throttled, temporarily blocked, or have the offending key revoked. See the Acceptable Use Policy for details.
← Inbox