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

  1. Sign up with an email and password, then enter the 6-digit code we email you to verify the address.
  2. Verifying your email automatically creates your first namespace and your default team.
  3. Open your dashboard and create an API key — the raw key is shown once, so copy it immediately.
  4. Call the API with that key in the X-API-Key header. 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 with qk_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"
}
FieldMeaning
score0-100. 100 is clean; lower means more spam-like.
spf / dkim / dmarcThe 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.
contentFlagsThe 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.
enginespamassassin-live or heuristic — which engine actually scored this message. See below.
Two engines, same output shape: when the dedicated spam-scanning add-on is reachable, 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 spam field on the message (REST, GraphQL, or the webhook payload) — score below ~70 usually means something in the content or auth setup needs fixing.
  • Check spf / dkim / dmarc first 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 at contentFlags for 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 now
r = 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.

ParamMeaning
namespacerequired — which namespace to read
since / untiloptional — unix-seconds bounds on receivedAt (inclusive); omit either side to leave it open
limitoptional — max messages to return
offsetoptional — 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.score
GET /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 event
import 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
end
req, _ := 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:

ArgumentMeaning
since / untilunix-seconds bounds on receivedAt (inclusive); omit either side to leave it open
limitmax messages to return
offsetskip 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

QueryReturns
viewerYour 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.
MutationDoes
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

StatusMeaning
400Bad request body — check the field names and types against the examples above.
401Missing or invalid X-API-Key, or no signed-in session, on an endpoint that requires one.
403Your current plan doesn't allow that action (for example, a plan-gated feature).
404The namespace, key, or resource doesn't exist — or doesn't belong to your account/team.
409Conflict — e.g. that email is already registered.
429Rate limited. Wait a minute and retry.
503A dependency (live updates, email delivery, SMS provisioning) is temporarily unavailable — safe to retry shortly.
Every error response is JSON with an 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.