QuickInbox Business API
A JSON API for receiving disposable test email programmatically — signup flows, OTP/verification codes, and QA pipelines. This page documents the Business API (the paid-tier platform at business.quickinbox.net). Looking for the free, no-signup temporary email API instead? See the classic API docs.
Base URL: https://business.quickinbox.net
Quick start
- Sign up with an email and password, then enter the 6-digit code we email you to verify the address.
- Verifying your email automatically creates your first namespace and your default team.
- Open your dashboard and create an API key — the raw key is shown once, so copy it immediately.
- Call the API with that key in the
X-API-Keyheader. No separate "activate" step.
A ready-to-run example for this step is next to POST /api/business/namespaces below — pick your language from the switcher above and every example on this page follows.
Authentication
Every Business API endpoint under /api/business/* (except signup/verify/login) accepts either:
- An API key — send it as
X-API-Key: qk_live_<...>. Keys always start withqk_live_. This is the normal way to call the API from a script, test suite, or CI pipeline. - A signed-in session cookie — set automatically when you sign in through the dashboard in a browser. Useful for calling the API straight from browser dev tools, not meant for server-to-server use.
A request with neither gets 401 Unauthorized: { "error": "Not signed in." }.
Create and manage keys from the dashboard, or programmatically via POST /api/business/keys once you have at least one valid key or an active session — so the very first key has to be created from the dashboard, after which you can mint more from the API itself.
Namespaces & tags
A namespace is your own slice of the mail system — think of it as a mailbox collection, not a single address. Every namespace accepts mail at any address of the shape {namespace}.{tag}@quickinbox.net, where {tag} can be absolutely anything you want: change it per test run, per user, or per request, and you've created a brand-new address on the fly with nothing to pre-register.
Example: namespace acme7x receives mail sent to acme7x.signup-test@quickinbox.net and acme7x.user42@quickinbox.net alike — both land in the same namespace, tagged differently, so you can tell them apart.
Every account gets one namespace automatically on verification, and can create more at any time — there's no cap on the free plan.
Teams
Namespaces and API keys belong to a team, not directly to your login. Every account has a default team created automatically the first time it's needed, and you can create additional teams and switch between them from the team selector in the dashboard header. This is for organizing projects, not for billing — a team by itself doesn't change your plan.
Retention
Each namespace has its own retention window — how long a message stays before it's deleted — settable from 1 to 3,650 days (about 10 years), on any plan including Free. Retention is a sliding window: every new message received resets the countdown for the whole namespace, so an actively used namespace never silently expires mid-test-run.
Webhooks
Instead of polling, set a webhook URL on a namespace and QuickInbox will POST to it the instant a message arrives — included free on every plan, including Free. Payload:
{
"event": "mail.received",
"namespace": "acme7x",
"message": {
"id": "b2c3d4e5-...",
"to": "acme7x.signup-test",
"from": "sender@example.com",
"subject": "Confirm your signup",
"text": "...",
"html": "...",
"spam": { "score": 100, "spf": "pass", "dkim": "pass", "dmarc": "pass" },
"receivedAt": 1893455460
}
}
Set or clear it with PATCH /api/business/namespaces/:id/webhook. We don't currently retry a failed delivery — treat the webhook as a convenience, and poll GET /api/business/mail if you need a guarantee.
Spam Testing Guide
How it works
Every message that arrives on a namespace is automatically given a deliverability/spam score from 0-100 (100 = clean) — no separate call needed, it's already attached to the message as a spam field everywhere a message appears: REST, GraphQL, and the webhook payload above.
"spam": {
"score": 54,
"spf": "softfail",
"dkim": "pass",
"dmarc": "none",
"contentFlags": ["BAYES_50", "HTML_IMAGE_ONLY_08", "MIME_HTML_ONLY"],
"engine": "spamassassin-live"
}
| Field | Meaning |
|---|---|
score | 0-100. 100 is clean; lower means more spam-like. |
spf / dkim / dmarc | The sending domain's authentication verdict for this specific message, read straight from the headers already stamped on it: pass, softfail, fail, neutral, none, or an error code. |
contentFlags | The specific signals that moved the score — rule names when engine is spamassassin-live (the same rule-name format SpamAssassin always reports), or short plain-English reasons when it's heuristic. Empty array when nothing was flagged. |
engine | spamassassin-live or heuristic — which engine actually scored this message. See below. |
score and contentFlags come straight from a real, unmodified SpamAssassin instance we run (engine: "spamassassin-live") — not a port or a reimplementation. If that add-on is ever unreachable or not configured, scoring falls back automatically, in-Worker, to our own lighter rule set (common promotional phrasing, shouting subjects, excessive punctuation, link-stuffed bodies — engine: "heuristic"), so a message is never left unscored and mail delivery is never blocked waiting on it. spf/dkim/dmarc are read directly from the message's own authentication headers (RFC 8601) either way. For checking a domain's published policy ahead of time (not a specific message), see the free SPF, DKIM, DMARC and blacklist checkers.TL;DR
- Send your email to any QuickInbox namespace address like you normally would — scoring happens automatically, nothing extra to configure.
- Read the
spamfield on the message (REST, GraphQL, or the webhook payload) —scorebelow ~70 usually means something in the content or auth setup needs fixing. - Check
spf/dkim/dmarcfirst if the score is low and you weren't expecting it — a failed auth check is the single most common cause. - Low on content but auth all
pass? Look atcontentFlagsfor the specific rule(s) that fired. - Want to check your domain's auth setup before you ever send a test message? Use the free SPF, DKIM and DMARC checkers.
REST API — Namespaces
POST /api/business/namespaces
Creates a namespace on your active team. Body: { "prefix": "mychoice" } — omit prefix for a random one.
// Response
{ "prefix": "acme7x", "domain": "quickinbox.net", "address": "acme7x.<anything>@quickinbox.net" }
curl -X POST \
https://business.quickinbox.net/api/business/namespaces \
-H "X-API-Key: $QI_KEY" \
-H "Content-Type: application/json" \
-d '{"prefix":"acme7x"}'const res = await fetch(
"https://business.quickinbox.net/api/business/namespaces",
{
method: "POST",
headers: {
"X-API-Key": process.env.QI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ prefix: "acme7x" })
}
);
const ns = await res.json();import requests
r = requests.post(
"https://business.quickinbox.net/api/business/namespaces",
headers={"X-API-Key": QI_KEY},
json={"prefix": "acme7x"},
)
ns = r.json()<?php
$ch = curl_init("https://business.quickinbox.net/api/business/namespaces");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . getenv("QI_KEY"),
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["prefix" => "acme7x"]));
$ns = json_decode(curl_exec($ch), true);
curl_close($ch);require "net/http"
require "json"
require "uri"
uri = URI("https://business.quickinbox.net/api/business/namespaces")
req = Net::HTTP::Post.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
req["Content-Type"] = "application/json"
req.body = { prefix: "acme7x" }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
ns = JSON.parse(res.body)body, _ := json.Marshal(map[string]string{"prefix": "acme7x"})
req, _ := http.NewRequest("POST",
"https://business.quickinbox.net/api/business/namespaces", bytes.NewBuffer(body))
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req)
defer resp.Body.Close()
var ns map[string]interface{}
json.NewDecoder(resp.Body).Decode(&ns)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var content = new StringContent(
JsonSerializer.Serialize(new { prefix = "acme7x" }),
Encoding.UTF8, "application/json");
var res = await client.PostAsync(
"https://business.quickinbox.net/api/business/namespaces", content);
var ns = await res.Content.ReadFromJsonAsync<JsonElement>();HttpClient client = HttpClient.newHttpClient();
String json = "{\"prefix\":\"acme7x\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/namespaces"))
.header("X-API-Key", System.getenv("QI_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());GET /api/business/namespaces
Lists every namespace on your active team.
// Response
{
"teamId": 12,
"namespaces": [
{ "id": 1, "prefix": "acme7x", "domain": "quickinbox.net", "retentionDays": 7, "webhookUrl": null, "createdAt": 1893450000 }
]
}
curl "https://business.quickinbox.net/api/business/namespaces" \
-H "X-API-Key: $QI_KEY"const res = await fetch(
"https://business.quickinbox.net/api/business/namespaces",
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { namespaces } = await res.json();r = requests.get(
"https://business.quickinbox.net/api/business/namespaces",
headers={"X-API-Key": QI_KEY},
)
namespaces = r.json()["namespaces"]<?php
$ch = curl_init("https://business.quickinbox.net/api/business/namespaces");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
$namespaces = $data["namespaces"];uri = URI("https://business.quickinbox.net/api/business/namespaces")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
namespaces = JSON.parse(res.body)["namespaces"]req, _ := http.NewRequest("GET",
"https://business.quickinbox.net/api/business/namespaces", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct {
Namespaces []map[string]interface{} `json:"namespaces"`
}
json.NewDecoder(resp.Body).Decode(&data)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var res = await client.GetAsync(
"https://business.quickinbox.net/api/business/namespaces");
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var namespaces = data.GetProperty("namespaces");HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/namespaces"))
.header("X-API-Key", System.getenv("QI_KEY"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());PATCH /api/business/namespaces/:id/retention
Body: { "retentionDays": 14 } — any whole number from 1 to 3650. Returns the updated namespace.
curl -X PATCH \
https://business.quickinbox.net/api/business/namespaces/1/retention \
-H "X-API-Key: $QI_KEY" \
-H "Content-Type: application/json" \
-d '{"retentionDays":14}'const res = await fetch(
"https://business.quickinbox.net/api/business/namespaces/1/retention",
{
method: "PATCH",
headers: {
"X-API-Key": process.env.QI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ retentionDays: 14 })
}
);
const ns = await res.json();r = requests.patch(
"https://business.quickinbox.net/api/business/namespaces/1/retention",
headers={"X-API-Key": QI_KEY},
json={"retentionDays": 14},
)
ns = r.json()<?php
$ch = curl_init("https://business.quickinbox.net/api/business/namespaces/1/retention");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . getenv("QI_KEY"),
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["retentionDays" => 14]));
$ns = json_decode(curl_exec($ch), true);
curl_close($ch);uri = URI("https://business.quickinbox.net/api/business/namespaces/1/retention")
req = Net::HTTP::Patch.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
req["Content-Type"] = "application/json"
req.body = { retentionDays: 14 }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
ns = JSON.parse(res.body)body, _ := json.Marshal(map[string]int{"retentionDays": 14})
req, _ := http.NewRequest("PATCH",
"https://business.quickinbox.net/api/business/namespaces/1/retention", bytes.NewBuffer(body))
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var content = new StringContent(
JsonSerializer.Serialize(new { retentionDays = 14 }),
Encoding.UTF8, "application/json");
var req = new HttpRequestMessage(new HttpMethod("PATCH"),
"https://business.quickinbox.net/api/business/namespaces/1/retention") { Content = content };
var res = await client.SendAsync(req);HttpClient client = HttpClient.newHttpClient();
String json = "{\"retentionDays\":14}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/namespaces/1/retention"))
.header("X-API-Key", System.getenv("QI_KEY"))
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());PATCH /api/business/namespaces/:id/webhook
Body: { "webhookUrl": "https://yourapp.example.com/hooks/mail" }. Pass null or an empty string to remove it. Must be a valid http:// or https:// URL, 500 characters or fewer.
curl -X PATCH \
https://business.quickinbox.net/api/business/namespaces/1/webhook \
-H "X-API-Key: $QI_KEY" \
-H "Content-Type: application/json" \
-d '{"webhookUrl":"https://yourapp.example.com/hooks/mail"}'const res = await fetch(
"https://business.quickinbox.net/api/business/namespaces/1/webhook",
{
method: "PATCH",
headers: {
"X-API-Key": process.env.QI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ webhookUrl: "https://yourapp.example.com/hooks/mail" })
}
);r = requests.patch(
"https://business.quickinbox.net/api/business/namespaces/1/webhook",
headers={"X-API-Key": QI_KEY},
json={"webhookUrl": "https://yourapp.example.com/hooks/mail"},
)<?php
$ch = curl_init("https://business.quickinbox.net/api/business/namespaces/1/webhook");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "PATCH");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . getenv("QI_KEY"),
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["webhookUrl" => "https://yourapp.example.com/hooks/mail"]));
$res = curl_exec($ch);
curl_close($ch);uri = URI("https://business.quickinbox.net/api/business/namespaces/1/webhook")
req = Net::HTTP::Patch.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
req["Content-Type"] = "application/json"
req.body = { webhookUrl: "https://yourapp.example.com/hooks/mail" }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }body, _ := json.Marshal(map[string]string{"webhookUrl": "https://yourapp.example.com/hooks/mail"})
req, _ := http.NewRequest("PATCH",
"https://business.quickinbox.net/api/business/namespaces/1/webhook", bytes.NewBuffer(body))
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var content = new StringContent(
JsonSerializer.Serialize(new { webhookUrl = "https://yourapp.example.com/hooks/mail" }),
Encoding.UTF8, "application/json");
var req = new HttpRequestMessage(new HttpMethod("PATCH"),
"https://business.quickinbox.net/api/business/namespaces/1/webhook") { Content = content };
var res = await client.SendAsync(req);HttpClient client = HttpClient.newHttpClient();
String json = "{\"webhookUrl\":\"https://yourapp.example.com/hooks/mail\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/namespaces/1/webhook"))
.header("X-API-Key", System.getenv("QI_KEY"))
.header("Content-Type", "application/json")
.method("PATCH", HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());REST API — API keys
POST /api/business/keys
Creates a new key on your active team. Body (both optional): { "namespaceId": 1, "label": "CI pipeline" } — scope a key to one namespace, or leave namespaceId out for a key valid across the whole team.
// Response — the raw key is returned exactly once, here, and never again
{ "id": 9, "key": "qk_live_abc123...", "display": "qk_live_abc1…9z8y", "namespaceId": 1, "label": "CI pipeline" }
curl -X POST https://business.quickinbox.net/api/business/keys \
-H "X-API-Key: $QI_KEY" \
-H "Content-Type: application/json" \
-d '{"label":"CI pipeline"}'const res = await fetch(
"https://business.quickinbox.net/api/business/keys",
{
method: "POST",
headers: {
"X-API-Key": process.env.QI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ label: "CI pipeline" })
}
);
const key = await res.json();
// key.key is the raw secret — shown once, store it nowr = requests.post(
"https://business.quickinbox.net/api/business/keys",
headers={"X-API-Key": QI_KEY},
json={"label": "CI pipeline"},
)
key = r.json() # key["key"] is the raw secret, shown once<?php
$ch = curl_init("https://business.quickinbox.net/api/business/keys");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . getenv("QI_KEY"),
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["label" => "CI pipeline"]));
$key = json_decode(curl_exec($ch), true);
curl_close($ch);uri = URI("https://business.quickinbox.net/api/business/keys")
req = Net::HTTP::Post.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
req["Content-Type"] = "application/json"
req.body = { label: "CI pipeline" }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
key = JSON.parse(res.body)body, _ := json.Marshal(map[string]string{"label": "CI pipeline"})
req, _ := http.NewRequest("POST",
"https://business.quickinbox.net/api/business/keys", bytes.NewBuffer(body))
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var key map[string]interface{}
json.NewDecoder(resp.Body).Decode(&key)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var content = new StringContent(
JsonSerializer.Serialize(new { label = "CI pipeline" }),
Encoding.UTF8, "application/json");
var res = await client.PostAsync(
"https://business.quickinbox.net/api/business/keys", content);
var key = await res.Content.ReadFromJsonAsync<JsonElement>();HttpClient client = HttpClient.newHttpClient();
String json = "{\"label\":\"CI pipeline\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/keys"))
.header("X-API-Key", System.getenv("QI_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());GET /api/business/keys
Lists keys on your active team. Only the masked key_display is ever returned again — store the raw key yourself when it's first created.
curl "https://business.quickinbox.net/api/business/keys" \
-H "X-API-Key: $QI_KEY"const res = await fetch(
"https://business.quickinbox.net/api/business/keys",
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { keys } = await res.json();r = requests.get(
"https://business.quickinbox.net/api/business/keys",
headers={"X-API-Key": QI_KEY},
)
keys = r.json()["keys"]<?php
$ch = curl_init("https://business.quickinbox.net/api/business/keys");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
$keys = $data["keys"];uri = URI("https://business.quickinbox.net/api/business/keys")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
keys = JSON.parse(res.body)["keys"]req, _ := http.NewRequest("GET",
"https://business.quickinbox.net/api/business/keys", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct {
Keys []map[string]interface{} `json:"keys"`
}
json.NewDecoder(resp.Body).Decode(&data)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var res = await client.GetAsync("https://business.quickinbox.net/api/business/keys");
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var keys = data.GetProperty("keys");HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/keys"))
.header("X-API-Key", System.getenv("QI_KEY"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());POST /api/business/keys/:id/revoke
Immediately invalidates a key. No body required.
curl -X POST \
https://business.quickinbox.net/api/business/keys/9/revoke \
-H "X-API-Key: $QI_KEY"const res = await fetch(
"https://business.quickinbox.net/api/business/keys/9/revoke",
{ method: "POST", headers: { "X-API-Key": process.env.QI_KEY } }
);requests.post(
"https://business.quickinbox.net/api/business/keys/9/revoke",
headers={"X-API-Key": QI_KEY},
)<?php
$ch = curl_init("https://business.quickinbox.net/api/business/keys/9/revoke");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
curl_exec($ch);
curl_close($ch);uri = URI("https://business.quickinbox.net/api/business/keys/9/revoke")
req = Net::HTTP::Post.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }req, _ := http.NewRequest("POST",
"https://business.quickinbox.net/api/business/keys/9/revoke", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
http.DefaultClient.Do(req)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
await client.PostAsync(
"https://business.quickinbox.net/api/business/keys/9/revoke", null);HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/keys/9/revoke"))
.header("X-API-Key", System.getenv("QI_KEY"))
.POST(HttpRequest.BodyPublishers.noBody())
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());REST API — Mail
GET /api/business/mail?namespace=acme7x
Returns messages stored for that namespace, newest first. Requires a signed-in session (not just an API key) since this is also what powers the dashboard's visual mail viewer.
| Param | Meaning |
|---|---|
namespace | required — which namespace to read |
since / until | optional — unix-seconds bounds on receivedAt (inclusive); omit either side to leave it open |
limit | optional — max messages to return |
offset | optional — skip this many (newest-first) before applying limit, for paging through a large inbox |
// Response
{
"namespace": "acme7x",
"retentionDays": 7,
"total": 1,
"messages": [
{
"id": "b2c3d4e5-...", "to": "acme7x.signup-test", "from": "sender@example.com",
"subject": "Confirm your signup", "text": "...", "html": "...",
"spam": { "score": 100, "spf": "pass", "dkim": "pass", "dmarc": "pass" },
"receivedAt": 1893455460
}
]
}
total is the count after since/until filtering but before limit/offset — so you can tell how many messages exist even when you only asked for one page of them.
curl \
"https://business.quickinbox.net/api/business/mail?namespace=acme7x" \
-H "X-API-Key: $QI_KEY"const res = await fetch(
"https://business.quickinbox.net/api/business/mail?namespace=acme7x",
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { messages } = await res.json();
console.log(messages[0].spam.score);r = requests.get(
"https://business.quickinbox.net/api/business/mail",
params={"namespace": "acme7x"},
headers={"X-API-Key": QI_KEY},
)
messages = r.json()["messages"]
print(messages[0]["spam"]["score"])<?php
$ch = curl_init("https://business.quickinbox.net/api/business/mail?namespace=acme7x");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
echo $data["messages"][0]["spam"]["score"];uri = URI("https://business.quickinbox.net/api/business/mail?namespace=acme7x")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
messages = JSON.parse(res.body)["messages"]
puts messages[0]["spam"]["score"]req, _ := http.NewRequest("GET",
"https://business.quickinbox.net/api/business/mail?namespace=acme7x", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct {
Messages []struct {
Spam struct{ Score int `json:"score"` } `json:"spam"`
} `json:"messages"`
}
json.NewDecoder(resp.Body).Decode(&data)
fmt.Println(data.Messages[0].Spam.Score)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var res = await client.GetAsync(
"https://business.quickinbox.net/api/business/mail?namespace=acme7x");
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var score = data.GetProperty("messages")[0]
.GetProperty("spam").GetProperty("score").GetInt32();HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/mail?namespace=acme7x"))
.header("X-API-Key", System.getenv("QI_KEY"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());
// parse response.body() and read messages[0].spam.scoreGET /api/business/mail/stream?namespace=acme7x
Server-Sent Events stream — stays open and pushes each new message the moment it arrives, instead of polling. Requires a signed-in session, same as above.
curl -N "https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x" \
--cookie "session=..."const res = await fetch(
"https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x",
{ headers: { Cookie: `session=${sessionCookie}` } }
);
const reader = res.body.getReader();
// read the stream chunk by chunk; each chunk is one "data: {...}" SSE eventimport sseclient # pip install sseclient-py
r = requests.get(
"https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x",
headers={"Cookie": f"session={session_cookie}"},
stream=True,
)
for event in sseclient.SSEClient(r):
message = json.loads(event.data)<?php
$ch = curl_init("https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["Cookie: session=" . $sessionCookie]);
curl_setopt($ch, CURLOPT_WRITEFUNCTION, function ($ch, $chunk) {
// each $chunk is part of the "data: {...}" SSE stream
echo $chunk;
return strlen($chunk);
});
curl_exec($ch);uri = URI("https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x")
Net::HTTP.start(uri.host, uri.port, use_ssl: true) do |http|
req = Net::HTTP::Get.new(uri)
req["Cookie"] = "session=#{session_cookie}"
http.request(req) do |res|
res.read_body { |chunk| puts chunk } # parse "data: {...}" lines as they arrive
end
endreq, _ := http.NewRequest("GET",
"https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x", nil)
req.Header.Set("Cookie", "session="+sessionCookie)
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
scanner := bufio.NewScanner(resp.Body)
for scanner.Scan() {
line := scanner.Text() // parse "data: {...}" lines as they arrive
}using var client = new HttpClient();
client.DefaultRequestHeaders.Add("Cookie", $"session={sessionCookie}");
using var stream = await client.GetStreamAsync(
"https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x");
using var reader = new StreamReader(stream);
while (!reader.EndOfStream) {
var line = await reader.ReadLineAsync(); // parse "data: {...}" lines
}HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/mail/stream?namespace=acme7x"))
.header("Cookie", "session=" + sessionCookie)
.GET()
.build();
HttpResponse<Stream<String>> response = client.send(request, HttpResponse.BodyHandlers.ofLines());
response.body().forEach(line -> { /* parse "data: {...}" lines as they arrive */ });DELETE /api/business/mail/:id?namespace=acme7x
Deletes one message by its id. Deleting an id that's already gone (already expired, or deleted a second time) is not an error — the response just says deleted: false instead of true, so retries and double-clicks are always safe.
// Response
{ "ok": true, "deleted": true }
curl -X DELETE \
"https://business.quickinbox.net/api/business/mail/b2c3d4e5-...?namespace=acme7x" \
-H "X-API-Key: $QI_KEY"await fetch(
"https://business.quickinbox.net/api/business/mail/b2c3d4e5-...?namespace=acme7x",
{ method: "DELETE", headers: { "X-API-Key": process.env.QI_KEY } }
);requests.delete(
"https://business.quickinbox.net/api/business/mail/b2c3d4e5-...",
params={"namespace": "acme7x"},
headers={"X-API-Key": QI_KEY},
)<?php
$ch = curl_init("https://business.quickinbox.net/api/business/mail/b2c3d4e5-...?namespace=acme7x");
curl_setopt($ch, CURLOPT_CUSTOMREQUEST, "DELETE");
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
curl_exec($ch);
curl_close($ch);uri = URI("https://business.quickinbox.net/api/business/mail/b2c3d4e5-...?namespace=acme7x")
req = Net::HTTP::Delete.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }req, _ := http.NewRequest("DELETE",
"https://business.quickinbox.net/api/business/mail/b2c3d4e5-...?namespace=acme7x", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
http.DefaultClient.Do(req)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
await client.DeleteAsync(
"https://business.quickinbox.net/api/business/mail/b2c3d4e5-...?namespace=acme7x");HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/mail/b2c3d4e5-...?namespace=acme7x"))
.header("X-API-Key", System.getenv("QI_KEY"))
.DELETE()
.build();
client.send(request, HttpResponse.BodyHandlers.ofString());REST API — SMS add-on
A second signal besides email for OTP/verification testing — provision a real phone number and receive inbound SMS programmatically. Priced per package; see SMS Add-on for current pricing.
GET /api/business/sms/packages
Lists the available SMS packages (no auth required — this is just the price list).
// Response
{ "packages": [ { "code": "sms100", "label": "100 messages/mo", "monthlyQuota": 100, "priceUsd": 3 }, ... ] }
curl "https://business.quickinbox.net/api/business/sms/packages"const res = await fetch("https://business.quickinbox.net/api/business/sms/packages");
const { packages } = await res.json();r = requests.get("https://business.quickinbox.net/api/business/sms/packages")
packages = r.json()["packages"]<?php
$data = json_decode(file_get_contents("https://business.quickinbox.net/api/business/sms/packages"), true);
$packages = $data["packages"];require "net/http"
res = Net::HTTP.get(URI("https://business.quickinbox.net/api/business/sms/packages"))
packages = JSON.parse(res)["packages"]resp, _ := http.Get("https://business.quickinbox.net/api/business/sms/packages")
defer resp.Body.Close()
var data struct {
Packages []map[string]interface{} `json:"packages"`
}
json.NewDecoder(resp.Body).Decode(&data)using var client = new HttpClient();
var res = await client.GetAsync("https://business.quickinbox.net/api/business/sms/packages");
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var packages = data.GetProperty("packages");HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/sms/packages"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());POST /api/business/sms/numbers
Provisions a new US phone number on the given package. Body: { "packageCode": "sms100" }.
// Response
{ "phoneNumber": "+15551234567", "package": { "code": "sms100", "label": "100 messages/mo", "monthlyQuota": 100, "priceUsd": 3 } }
curl -X POST \
https://business.quickinbox.net/api/business/sms/numbers \
-H "X-API-Key: $QI_KEY" \
-H "Content-Type: application/json" \
-d '{"packageCode":"sms100"}'const res = await fetch(
"https://business.quickinbox.net/api/business/sms/numbers",
{
method: "POST",
headers: {
"X-API-Key": process.env.QI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ packageCode: "sms100" })
}
);
const { phoneNumber } = await res.json();r = requests.post(
"https://business.quickinbox.net/api/business/sms/numbers",
headers={"X-API-Key": QI_KEY},
json={"packageCode": "sms100"},
)
phone_number = r.json()["phoneNumber"]<?php
$ch = curl_init("https://business.quickinbox.net/api/business/sms/numbers");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . getenv("QI_KEY"),
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["packageCode" => "sms100"]));
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
$phoneNumber = $data["phoneNumber"];uri = URI("https://business.quickinbox.net/api/business/sms/numbers")
req = Net::HTTP::Post.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
req["Content-Type"] = "application/json"
req.body = { packageCode: "sms100" }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
phone_number = JSON.parse(res.body)["phoneNumber"]body, _ := json.Marshal(map[string]string{"packageCode": "sms100"})
req, _ := http.NewRequest("POST",
"https://business.quickinbox.net/api/business/sms/numbers", bytes.NewBuffer(body))
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct{ PhoneNumber string `json:"phoneNumber"` }
json.NewDecoder(resp.Body).Decode(&data)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var content = new StringContent(
JsonSerializer.Serialize(new { packageCode = "sms100" }),
Encoding.UTF8, "application/json");
var res = await client.PostAsync(
"https://business.quickinbox.net/api/business/sms/numbers", content);
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var phoneNumber = data.GetProperty("phoneNumber").GetString();HttpClient client = HttpClient.newHttpClient();
String json = "{\"packageCode\":\"sms100\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/sms/numbers"))
.header("X-API-Key", System.getenv("QI_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());GET /api/business/sms/numbers
Lists the numbers on your account, with their package, quota and usage this period.
curl "https://business.quickinbox.net/api/business/sms/numbers" \
-H "X-API-Key: $QI_KEY"const res = await fetch(
"https://business.quickinbox.net/api/business/sms/numbers",
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { numbers } = await res.json();r = requests.get(
"https://business.quickinbox.net/api/business/sms/numbers",
headers={"X-API-Key": QI_KEY},
)
numbers = r.json()["numbers"]<?php
$ch = curl_init("https://business.quickinbox.net/api/business/sms/numbers");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
$numbers = $data["numbers"];uri = URI("https://business.quickinbox.net/api/business/sms/numbers")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
numbers = JSON.parse(res.body)["numbers"]req, _ := http.NewRequest("GET",
"https://business.quickinbox.net/api/business/sms/numbers", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct {
Numbers []map[string]interface{} `json:"numbers"`
}
json.NewDecoder(resp.Body).Decode(&data)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var res = await client.GetAsync("https://business.quickinbox.net/api/business/sms/numbers");
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var numbers = data.GetProperty("numbers");HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/sms/numbers"))
.header("X-API-Key", System.getenv("QI_KEY"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());REST API — Custom domains
Receive mail on your own domain (mail.yourcompany.com) instead of a quickinbox.net address. First domain is included free on every plan; additional domains are a flat add-on.
POST /api/business/domains
Starts verification for a domain you own. Body: { "domain": "mail.yourcompany.com" }.
// Response
{
"domainId": 4, "domain": "mail.yourcompany.com", "billing": "included free with your plan",
"dnsRecordsNeeded": [
{ "type": "TXT", "name": "_quickinbox-verify.mail.yourcompany.com", "value": "...", "purpose": "ownership verification" },
{ "type": "MX", "name": "mail.yourcompany.com", "value": "mxa.mailgun.org", "priority": 10, "purpose": "mail delivery" },
{ "type": "MX", "name": "mail.yourcompany.com", "value": "mxb.mailgun.org", "priority": 10, "purpose": "mail delivery (backup)" }
]
}
Add those DNS records at your registrar/DNS host; verification completes automatically once they propagate.
curl -X POST https://business.quickinbox.net/api/business/domains \
-H "X-API-Key: $QI_KEY" \
-H "Content-Type: application/json" \
-d '{"domain":"mail.yourcompany.com"}'const res = await fetch(
"https://business.quickinbox.net/api/business/domains",
{
method: "POST",
headers: {
"X-API-Key": process.env.QI_KEY,
"Content-Type": "application/json"
},
body: JSON.stringify({ domain: "mail.yourcompany.com" })
}
);
const { dnsRecordsNeeded } = await res.json();r = requests.post(
"https://business.quickinbox.net/api/business/domains",
headers={"X-API-Key": QI_KEY},
json={"domain": "mail.yourcompany.com"},
)
dns_records_needed = r.json()["dnsRecordsNeeded"]<?php
$ch = curl_init("https://business.quickinbox.net/api/business/domains");
curl_setopt($ch, CURLOPT_POST, true);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-API-Key: " . getenv("QI_KEY"),
"Content-Type: application/json"
]);
curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(["domain" => "mail.yourcompany.com"]));
$data = json_decode(curl_exec($ch), true);
curl_close($ch);uri = URI("https://business.quickinbox.net/api/business/domains")
req = Net::HTTP::Post.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
req["Content-Type"] = "application/json"
req.body = { domain: "mail.yourcompany.com" }.to_json
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
data = JSON.parse(res.body)body, _ := json.Marshal(map[string]string{"domain": "mail.yourcompany.com"})
req, _ := http.NewRequest("POST",
"https://business.quickinbox.net/api/business/domains", bytes.NewBuffer(body))
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var content = new StringContent(
JsonSerializer.Serialize(new { domain = "mail.yourcompany.com" }),
Encoding.UTF8, "application/json");
var res = await client.PostAsync(
"https://business.quickinbox.net/api/business/domains", content);
var data = await res.Content.ReadFromJsonAsync<JsonElement>();HttpClient client = HttpClient.newHttpClient();
String json = "{\"domain\":\"mail.yourcompany.com\"}";
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/domains"))
.header("X-API-Key", System.getenv("QI_KEY"))
.header("Content-Type", "application/json")
.POST(HttpRequest.BodyPublishers.ofString(json))
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());GET /api/business/domains
Lists your domains with verification status and billing.
curl "https://business.quickinbox.net/api/business/domains" \
-H "X-API-Key: $QI_KEY"const res = await fetch(
"https://business.quickinbox.net/api/business/domains",
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { domains } = await res.json();r = requests.get(
"https://business.quickinbox.net/api/business/domains",
headers={"X-API-Key": QI_KEY},
)
domains = r.json()["domains"]<?php
$ch = curl_init("https://business.quickinbox.net/api/business/domains");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
$domains = $data["domains"];uri = URI("https://business.quickinbox.net/api/business/domains")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
domains = JSON.parse(res.body)["domains"]req, _ := http.NewRequest("GET",
"https://business.quickinbox.net/api/business/domains", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct {
Domains []map[string]interface{} `json:"domains"`
}
json.NewDecoder(resp.Body).Decode(&data)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var res = await client.GetAsync("https://business.quickinbox.net/api/business/domains");
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var domains = data.GetProperty("domains");HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/domains"))
.header("X-API-Key", System.getenv("QI_KEY"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());GET /api/business/mail/domain?address=you@mail.yourcompany.com
Returns messages received at one address on one of your verified custom domains.
curl "https://business.quickinbox.net/api/business/mail/domain?address=you@mail.yourcompany.com" \
-H "X-API-Key: $QI_KEY"const res = await fetch(
"https://business.quickinbox.net/api/business/mail/domain?address=you@mail.yourcompany.com",
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { messages } = await res.json();r = requests.get(
"https://business.quickinbox.net/api/business/mail/domain",
params={"address": "you@mail.yourcompany.com"},
headers={"X-API-Key": QI_KEY},
)
messages = r.json()["messages"]<?php
$ch = curl_init("https://business.quickinbox.net/api/business/mail/domain?address=you@mail.yourcompany.com");
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
curl_setopt($ch, CURLOPT_HTTPHEADER, ["X-API-Key: " . getenv("QI_KEY")]);
$data = json_decode(curl_exec($ch), true);
curl_close($ch);
$messages = $data["messages"];uri = URI("https://business.quickinbox.net/api/business/mail/domain?address=you@mail.yourcompany.com")
req = Net::HTTP::Get.new(uri)
req["X-API-Key"] = ENV["QI_KEY"]
res = Net::HTTP.start(uri.host, uri.port, use_ssl: true) { |http| http.request(req) }
messages = JSON.parse(res.body)["messages"]req, _ := http.NewRequest("GET",
"https://business.quickinbox.net/api/business/mail/domain?address=you@mail.yourcompany.com", nil)
req.Header.Set("X-API-Key", os.Getenv("QI_KEY"))
resp, _ := http.DefaultClient.Do(req)
defer resp.Body.Close()
var data struct {
Messages []map[string]interface{} `json:"messages"`
}
json.NewDecoder(resp.Body).Decode(&data)using var client = new HttpClient();
client.DefaultRequestHeaders.Add("X-API-Key", Environment.GetEnvironmentVariable("QI_KEY"));
var res = await client.GetAsync(
"https://business.quickinbox.net/api/business/mail/domain?address=you@mail.yourcompany.com");
var data = await res.Content.ReadFromJsonAsync<JsonElement>();
var messages = data.GetProperty("messages");HttpClient client = HttpClient.newHttpClient();
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://business.quickinbox.net/api/business/mail/domain?address=you@mail.yourcompany.com"))
.header("X-API-Key", System.getenv("QI_KEY"))
.GET()
.build();
HttpResponse<String> response = client.send(request, HttpResponse.BodyHandlers.ofString());GraphQL API: Setup and auth
The namespace, key and mail operations documented above as REST are also available as GraphQL, at a single endpoint, using the exact same auth as everything else on this page — an X-API-Key header or a signed-in session. There's no separate GraphQL-specific key or token.
POST /api/business/graphql
Content-Type: application/json
X-API-Key: qk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
{ "query": "{ viewer { email planTier namespaces { prefix } } }" }
Errors follow the standard GraphQL shape — a data field, an errors field, or both. Point any standard GraphQL client (Apollo Client, urql, graphql-request, Postman/Insomnia's GraphQL mode) at that one URL and it works with no special-casing — there's nothing QuickInbox-specific about the transport.
GraphQL API: Querying the inbox
The mail query takes a namespace and returns that namespace's messages, newest first — ask for only the fields you actually need:
query {
mail(namespace: "acme7x") {
total
messages { id from subject spam { score spf dkim dmarc } }
}
}
Create a namespace, read its own fresh key, and mutations in general all work the same way — a Mutation instead of a Query, same request shape:
mutation {
createNamespace(prefix: "acme7x") { id prefix retentionDays }
}
GraphQL API: "Live" queries
GraphQL itself has no subscription support on this endpoint — re-running the mail query is a normal request/response round trip, same cost as calling it the first time. For push-style "tell me the instant something arrives" delivery, QuickInbox gives you two options outside GraphQL itself, both already documented above and both work no matter whether the rest of your code talks REST or GraphQL:
- A Server-Sent Events stream you keep open — the same stream a REST-only caller would use.
- A webhook QuickInbox
POSTs to the instant a message arrives, so you don't have to keep a connection open or re-query at all.
The webhook option has no equivalent on most competing test-inbox APIs — it's often the simplest way to get "live" behavior without writing any polling or streaming code yourself.
GraphQL API: Sorting and filtering
Messages are always returned newest first. The mail query takes four optional arguments for narrowing that list down:
| Argument | Meaning |
|---|---|
since / until | unix-seconds bounds on receivedAt (inclusive); omit either side to leave it open |
limit | max messages to return |
offset | skip this many (newest-first) before applying limit — for paging through a large inbox |
query {
mail(namespace: "acme7x", since: 1893450000, limit: 10) {
total
messages { id subject receivedAt }
}
}
GraphQL API: Reference
| Query | Returns |
|---|---|
viewer | Your account's email, plan, namespaces and keys — same data as GET /api/business/me. |
mail(namespace: String!, since: Int, until: Int, limit: Int, offset: Int) | Messages for one of your namespaces, each including its spam report, plus a total count — same as GET /api/business/mail. |
| Mutation | Does |
|---|---|
createNamespace(prefix: String) | Same as POST /api/business/namespaces. |
setNamespaceRetention(id: ID!, retentionDays: Int!) | Same as the retention PATCH endpoint. |
setNamespaceWebhook(id: ID!, webhookUrl: String) | Same as the webhook PATCH endpoint. |
createApiKey(namespaceId: ID, label: String) | Same as POST /api/business/keys — raw key returned once. |
revokeApiKey(id: ID!) | Same as the revoke endpoint. |
deleteMessage(namespace: String!, id: ID!) | Same as the DELETE mail endpoint — returns false rather than erroring if that id was already gone. |
Examples — REST, by use case
Automated signup test (Cypress-style)
The pattern for any browser-automation test suite: create (or reuse) a namespace before the test, trigger your app's signup email, then poll for the message and pull the verification link or code out of it.
// Inside a Cypress test, as a plain HTTP call
cy.request({
method: "GET",
url: "https://business.quickinbox.net/api/business/mail?namespace=acme7x",
headers: { "X-API-Key": Cypress.env("QI_KEY") }
}).then((res) => {
const msg = res.body.messages.find((m) => m.subject.includes("Confirm your signup"));
const code = msg.text.match(/\b\d{6}\b/)[0];
cy.get("#otp-input").type(code);
});
Automated signup test (Playwright-style)
const res = await request.get(
"https://business.quickinbox.net/api/business/mail?namespace=acme7x",
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { messages } = await res.json();
const code = messages[0].text.match(/\b\d{6}\b/)[0];
await page.fill("#otp-input", code);
CI pipeline (curl + jq)
KEY="qk_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx"
curl -X POST https://business.quickinbox.net/api/business/namespaces \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" -d '{"prefix":"ci-run-42"}'
curl -s "https://business.quickinbox.net/api/business/mail?namespace=ci-run-42" \
-H "X-API-Key: $KEY" | jq '.messages[0].spam.score'
Examples — GraphQL
Basic query
curl -X POST https://business.quickinbox.net/api/business/graphql \
-H "X-API-Key: $KEY" -H "Content-Type: application/json" \
-d '{"query":"query($ns:String!){ mail(namespace:$ns){ messages { id subject spam { score } } } }","variables":{"ns":"acme7x"}}'
Paginated, filtered query
The same since/limit/offset arguments REST uses, passed as GraphQL variables — handy for a "load more" button or a cron job that only wants mail since its last run:
const query = `
query($ns: String!, $since: Int!, $offset: Int!) {
mail(namespace: $ns, since: $since, limit: 20, offset: $offset) {
total
messages { id subject receivedAt spam { score } }
}
}
`;
const res = await fetch("https://business.quickinbox.net/api/business/graphql", {
method: "POST",
headers: { "X-API-Key": process.env.QI_KEY, "Content-Type": "application/json" },
body: JSON.stringify({ query, variables: { ns: "acme7x", since: lastRunAt, offset: 0 } })
});
const { data } = await res.json();
Mutation chain: new namespace, new key, delete a message
Mutations compose in one request just like queries do — useful for a one-shot test-environment setup script:
mutation {
ns: createNamespace(prefix: "ci-run-42") { id prefix }
key: createApiKey(label: "ci-run-42") { key }
}
mutation {
deleteMessage(namespace: "acme7x", id: "b2c3d4e5-f6a7-4b8c-9d0e-1f2a3b4c5d6e")
}
Examples — OTP / verification testing
Unique tag per test run
The most common use case: generate a unique tag per test run (so parallel test runs never see each other's mail), send to it, and extract the code.
import requests, re
tag = f"run-{run_id}"
address = f"acme7x.{tag}@quickinbox.net"
# ... trigger your app to email `address` ...
r = requests.get(
"https://business.quickinbox.net/api/business/mail",
params={"namespace": "acme7x"},
headers={"X-API-Key": API_KEY},
)
msg = next(m for m in r.json()["messages"] if m["to"] == f"acme7x.{tag}")
code = re.search(r"\b\d{6}\b", msg["text"]).group()
Poll with a timeout
Mail delivery isn't instant — a test that reads the inbox once, right after triggering the send, will sometimes race the email and fail flakily. Poll for a few seconds instead of reading once:
async function waitForOtp(namespace, to, { timeoutMs = 15000, intervalMs = 1000 } = {}) {
const deadline = Date.now() + timeoutMs;
while (Date.now() < deadline) {
const res = await fetch(
`https://business.quickinbox.net/api/business/mail?namespace=${namespace}&limit=10`,
{ headers: { "X-API-Key": process.env.QI_KEY } }
);
const { messages } = await res.json();
const msg = messages.find((m) => m.to === to);
if (msg) return msg.text.match(/\b\d{6}\b/)[0];
await new Promise((r) => setTimeout(r, intervalMs));
}
throw new Error("OTP never arrived within " + timeoutMs + "ms");
}
Webhook-based OTP capture (no polling at all)
If your test runner can expose a URL — a CI job with a tunnel, or a long-running test server — a webhook delivers the code the instant it arrives instead of polling for it. Something most competing test-inbox APIs can't do at all, since they don't have webhooks:
// Your own tiny receiver, set as the namespace's webhookUrl
app.post("/hooks/mail", (req, res) => {
const match = req.body.text.match(/\b\d{6}\b/);
if (match) pendingOtp.resolve(match[0]); // wake up the waiting test
res.sendStatus(200);
});
// In the test itself
const code = await new Promise((resolve) => { pendingOtp = { resolve }; });
Errors & troubleshooting
| Status | Meaning |
|---|---|
400 | Bad request body — check the field names and types against the examples above. |
401 | Missing or invalid X-API-Key, or no signed-in session, on an endpoint that requires one. |
403 | Your current plan doesn't allow that action (for example, a plan-gated feature). |
404 | The namespace, key, or resource doesn't exist — or doesn't belong to your account/team. |
409 | Conflict — e.g. that email is already registered. |
429 | Rate limited. Wait a minute and retry. |
503 | A dependency (live updates, email delivery, SMS provisioning) is temporarily unavailable — safe to retry shortly. |
error string field (or, for GraphQL, an errors array) — always safe to read and display directly.Get help
Can't find what you need here, or hit something that looks like a bug? Email support@quickinbox.net — a person reads it, not a bot.