Integrate Phantom Mail into your apps. Generate disposable emails, stream your inbox in real time, send mail, and claim addresses programmatically.
pm_free_ and can receive only. Premium keys start with pm_pro_ and can receive and send.
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.
Include your API key in every request using the X-API-Key HTTP header.
X-API-Key: pm_free_your32characterapikey... # or a premium key X-API-Key: pm_pro_your32characterapikey...
GET /api/v1/emails/stream) also accepts the key as a ?key= query parameter, because the browser EventSource API cannot set custom headers.
API limits are applied per key, per day (UTC reset at midnight).
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.
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)
| Field | Type | Required | Description |
|---|---|---|---|
| domain | string | Optional | Mail domain: unkn0wn.qzz.io or phant0m.qzz.io. Premium only; defaults to a random domain. |
| handle | string | Optional | Custom handle (3–30 chars, a-z 0-9 . _ -). Premium only. |
Responses
200 OK — Address generated
{
"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
{ "error": "Custom handle requires Premium" }
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
| Parameter | Type | Required | Description |
|---|---|---|---|
| address | string | Required | The full email address to check (e.g. silentfox482@unkn0wn.qzz.io) |
Responses
200 OK
{
"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
{ "error": "Address not found or expired" }
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
| Parameter | Type | Required | Description |
|---|---|---|---|
| address | string | Required | The address to stream (e.g. silentfox482@unkn0wn.qzz.io) |
| key | string | Optional | Your API key, if you cannot send the X-API-Key header (browser EventSource). |
Event Types
Wire Format
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)
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)
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.
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)
| Field | Type | Required | Description |
|---|---|---|---|
| from | string | Required | One of your addresses (e.g. me@phant0m.qzz.io) |
| to | string | Required | Recipient email address |
| subject | string | Required | Message subject |
| text | string | Optional* | Plain-text body |
| html | string | Optional* | HTML body. *Provide at least one of text or html. |
Example Request
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
{ "success": true, "id": "msg_7f21ac" }
403 Forbidden — Free key attempted to send
{ "error": "Sending requires a Premium 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
{
"success": true,
"plan": "premium",
"usage": {
"receive": { "today": 42, "limit": 500 },
"send": { "today": 3, "limit": 50 }
}
}
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.
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)
| Field | Type | Required | Description |
|---|---|---|---|
| address | string | Required | The address to re-claim |
| timestamp | number | Required | Unix time (ms) used to build the challenge |
| publicKey | string | Required | Ed25519 public key, base64 |
| signature | string | Required | Ed25519 signature of the challenge, base64 |
Build & sign the challenge (JavaScript / Web Crypto)
// 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
{ "success": true, "address": "silentfox482@unkn0wn.qzz.io" }
401 Unauthorized — Signature does not match
{ "error": "Invalid signature" }
| Status | Code | Description |
|---|---|---|
| 400 | Bad Request | Missing or invalid parameters |
| 401 | Unauthorized | API key missing/invalid, or claim signature mismatch |
| 403 | Forbidden | Feature requires Premium (e.g. send, custom handle/domain) |
| 404 | Not Found | Address does not exist or has expired |
| 429 | Too Many Requests | Daily receive/send limit exceeded |
| 500 | Server Error | Internal server error — try again |