Getting started
Your first email
Receive your first verification code with JavaScript/TypeScript, Python, Java or Bash/curl.
Receive your first email
Register an inbox, wait for a code, then read the matching message.
Set INBOXSPEC_API_KEY in the environment that runs your example. Use a key with inbox read/write and message wait/read permissions. Keep the key out of source code, browser code and chat. You can override the API origin with INBOXSPEC_API_URL (no trailing slash).
Choose your language
Run one example. It creates a fresh inbox and prints its address. While the wait is running, send “Your code is 123456” to that address from your app or email client. A match returns code candidates; a timeout means no matching mail arrived. HTTP and permission errors are separate failures.
JavaScript / TypeScript
Use Node.js 24+. Save as first-email.mjs; no packages are required. This JavaScript also works in a server-side TypeScript module.
// Save as first-email.mjs. Requires Node.js 24+; no packages to install.
const apiUrl = process.env.INBOXSPEC_API_URL || "https://api.inboxspec.com";
const apiKey = process.env.INBOXSPEC_API_KEY;
if (!apiKey) throw new Error("Set INBOXSPEC_API_KEY to your project API key first.");
async function request(path, body) {
const response = await fetch(new URL(path, apiUrl), {
method: body === undefined ? "GET" : "POST",
headers: { Authorization: "Bearer " + apiKey, "Content-Type": "application/json" },
body: body === undefined ? undefined : JSON.stringify(body),
signal: AbortSignal.timeout(70_000),
});
if (!response.ok) {
// An HTTP error is different from a successful wait that found no email.
const problem = await response.json().catch(() => null);
throw Object.assign(new Error("HTTP " + response.status + ": check the API error reference before retrying."), {
code: problem?.error?.code,
});
}
return response;
}
const runId = "first-" + crypto.randomUUID();
const inbox = await (await request("/v1/inboxes/for-run", {
run_id: runId, retention_seconds: 600,
})).json();
if (typeof inbox.address !== "string") throw new Error("No inbox address returned.");
console.log("Run ID:", runId);
console.log("Send a test email containing 'Your code is 123456' to: " + inbox.address);
console.log("Waiting up to 60 seconds…");
const result = await (await request("/v1/messages/wait", {
inbox: inbox.address, match: { has_code: true }, timeout_ms: 60_000,
})).json();
if (result.status === "timeout") {
console.log("No matching email. Check the address and your sender, then try a new run.");
console.log({ reason: result.reason, arrivals: result.non_matching_arrivals, hint: result.hint });
process.exitCode = 1;
} else if (result.status === "matched" && typeof result.message?.id === "string") {
console.log("Code candidates:", result.message.codes);
const bodyPath = "/v1/messages/" + encodeURIComponent(result.message.id) + "/body?format=";
let body;
try {
body = await request(bodyPath + "text");
} catch (error) {
if (error.code !== "unsupported_format") throw error;
console.log("This email is HTML-only. Email HTML:");
body = await request(bodyPath + "html");
}
console.log(await body.text());
} else {
throw new Error("Unexpected wait response. Check the API error reference.");
}
// Messages expire after ten minutes; the inbox remains until explicitly deleted. Accepted messages still count toward usage.
node first-email.mjsPython
Use Python 3. Save as first_email.py; the example uses only the standard library.
import json
import os
import sys
import uuid
from urllib.request import Request, urlopen
from urllib.error import HTTPError
from urllib.parse import quote
origin = os.environ.get("INBOXSPEC_API_URL", "https://api.inboxspec.com")
key = os.environ["INBOXSPEC_API_KEY"]
def request(path, body=None):
data = None if body is None else json.dumps(body).encode()
req = Request(origin + path, data=data, headers={
"Authorization": "Bearer " + key,
"Content-Type": "application/json",
})
# Longer than the server's 60-second wait; HTTP errors remain errors.
with urlopen(req, timeout=70) as response:
return response.read().decode()
run_id = "first-" + str(uuid.uuid4())
inbox = json.loads(request("/v1/inboxes/for-run", {
"run_id": run_id, "retention_seconds": 600,
}))
print("Run ID:", run_id)
print("Send 'Your code is 123456' to:", inbox["address"], flush=True)
result = json.loads(request("/v1/messages/wait", {
"inbox": inbox["address"], "match": {"has_code": True}, "timeout_ms": 60000,
}))
if result["status"] == "timeout":
print("No matching email. Check the address and sender.")
print(result)
sys.exit(1)
if result["status"] != "matched" or not isinstance(result.get("message", {}).get("id"), str):
raise ValueError("Unexpected wait response")
print("Code candidates:", result["message"].get("codes", []))
path = "/v1/messages/" + quote(result["message"]["id"], safe="") + "/body?format="
try:
body = request(path + "text")
except HTTPError as error:
problem = json.loads(error.read())
if problem.get("error", {}).get("code") != "unsupported_format":
raise
print("This email is HTML-only. Email HTML:")
body = request(path + "html")
print(body)
# Messages expire after ten minutes; delete this run's inbox when finished.
python3 first_email.pyJava
Use Java 17+ and Gson for JSON parsing. Save as FirstEmail.java.
import com.google.gson.*;
import java.net.*;
import java.net.http.*;
import java.nio.charset.StandardCharsets;
import java.time.Duration;
import java.util.*;
class FirstEmail {
static final Gson JSON = new Gson();
static final String ORIGIN = Optional.ofNullable(System.getenv("INBOXSPEC_API_URL"))
.orElse("https://api.inboxspec.com");
static final String KEY = System.getenv("INBOXSPEC_API_KEY");
static final HttpClient CLIENT = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)).build();
static class ApiError extends Exception {
final String code;
ApiError(int status, String code) { super("HTTP " + status); this.code = code; }
}
static String request(String path, Object body) throws Exception {
var builder = HttpRequest.newBuilder(URI.create(ORIGIN + path))
.header("Authorization", "Bearer " + KEY)
.header("Content-Type", "application/json")
.timeout(Duration.ofSeconds(70));
if (body == null) builder.GET();
else builder.POST(HttpRequest.BodyPublishers.ofString(JSON.toJson(body)));
var response = CLIENT.send(builder.build(), HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300) {
String code = "";
try { code = JsonParser.parseString(response.body()).getAsJsonObject()
.getAsJsonObject("error").get("code").getAsString(); }
catch (RuntimeException ignored) { }
throw new ApiError(response.statusCode(), code);
}
return response.body();
}
public static void main(String[] args) throws Exception {
if (KEY == null || KEY.isBlank()) throw new IllegalStateException("Set INBOXSPEC_API_KEY first");
String runId = "first-" + UUID.randomUUID();
var inbox = JsonParser.parseString(request("/v1/inboxes/for-run",
Map.of("run_id", runId, "retention_seconds", 600))).getAsJsonObject();
String address = inbox.get("address").getAsString();
System.out.println("Run ID: " + runId);
System.out.println("Send 'Your code is 123456' to: " + address);
var result = JsonParser.parseString(request("/v1/messages/wait", Map.of(
"inbox", address, "match", Map.of("has_code", true), "timeout_ms", 60000))).getAsJsonObject();
String status = result.get("status").getAsString();
if (status.equals("timeout")) {
System.out.println("No matching email. Check the address and sender.");
System.out.println(result);
System.exit(1);
}
if (!status.equals("matched")) throw new IllegalStateException("Unexpected wait response");
var message = result.getAsJsonObject("message");
System.out.println("Code candidates: " + message.get("codes"));
String id = URLEncoder.encode(message.get("id").getAsString(), StandardCharsets.UTF_8);
String path = "/v1/messages/" + id + "/body?format=";
String body;
try { body = request(path + "text", null); }
catch (ApiError error) {
if (!error.code.equals("unsupported_format")) throw error;
System.out.println("This email is HTML-only. Email HTML:");
body = request(path + "html", null);
}
System.out.println(body);
// Messages expire after ten minutes; delete this run's inbox when finished.
}
}Download Gson from Maven Central once, then run the example in the same folder:
curl --fail --location --output gson-2.14.0.jar \
https://repo.maven.apache.org/maven2/com/google/code/gson/gson/2.14.0/gson-2.14.0.jar
java --class-path gson-2.14.0.jar FirstEmail.javaBash / curl
Use a Bash terminal with curl, jq and OpenSSL installed. Keep these commands in the same terminal.
If a command fails, stop and use the recovery guidance below before continuing.
- 1
Set up this run
Use a fresh run ID to keep this test separate from other mail.
unset RUN_ID INBOX WAIT_RESULT MESSAGE_ID export INBOXSPEC_API_URL='https://api.inboxspec.com' if [ -n "$INBOXSPEC_API_KEY" ] && RUN_ID=$(openssl rand -hex 12) && [[ "$RUN_ID" =~ ^[a-f0-9]{24}$ ]]; then export RUN_ID="first-$RUN_ID" else unset RUN_ID printf '%s\n' 'Setup failed. Set your saved project key and check OpenSSL, then retry.' >&2 false fi - 2
Register the inbox
Waiting requires an existing inbox. Keep the returned address; guessing an address does not register it.
unset INBOX WAIT_RESULT MESSAGE_ID if [[ "$RUN_ID" =~ ^first-[a-f0-9]{24}$ ]] && INBOX=$(set -o pipefail curl --fail-with-body --silent --show-error "$INBOXSPEC_API_URL/v1/inboxes/for-run" \ -H "Authorization: Bearer $INBOXSPEC_API_KEY" \ -H "content-type: application/json" \ -d "$(jq -n --arg run_id "$RUN_ID" '{run_id: $run_id}')" | jq -ers 'if length == 1 and (.[0].address | type == "string" and test("^[^@[:space:]]+@[^@[:space:]]+$")) then .[0].address else error("Expected one inbox address") end'); then export INBOX printf 'Send your test email to: %s\n' "$INBOX" else unset INBOX printf '%s\n' 'Registration failed. Fix the error and repeat setup or registration before waiting.' >&2 false fi - 3
Start waiting, then send your email
This terminal waits up to 60 seconds. While it is waiting, use your app or email client to send a message containing a verification code to the printed address. A matching message that arrived just before the wait can also be returned.
unset WAIT_RESULT MESSAGE_ID if [ -n "$INBOX" ] && WAIT_RESULT=$(curl --fail-with-body --silent --show-error "$INBOXSPEC_API_URL/v1/messages/wait" \ -H "Authorization: Bearer $INBOXSPEC_API_KEY" \ -H "content-type: application/json" \ -d "$(jq -n --arg inbox "$INBOX" '{inbox: $inbox, match: {has_code: true}, timeout_ms: 60000}')"); then printf '%s\n' 'Wait returned. Check the outcome in step 4.' else unset WAIT_RESULT printf '%s\n' 'Wait failed. Check registration and the request error before retrying.' >&2 false fi - 4
Check the outcome
A match includes a message ID and ranked code candidates. Check which code your test expects. A timeout includes a hint and the count of non-matching arrivals; inspect those before starting another bounded wait.
unset MESSAGE_ID if MESSAGE_ID=$(printf '%s\n' "$WAIT_RESULT" | jq -ers ' if length != 1 then error("Expected one wait result") elif .[0].status == "matched" and (.[0].message.id | type == "string" and test("^[A-Za-z0-9_-]+$")) then .[0].message.id elif .[0].status == "timeout" then "" else error("Unexpected wait result") end'); then printf '%s\n' "$WAIT_RESULT" | jq ' if .status == "matched" then {status, message_id: .message.id, codes: .message.codes} else {status, non_matching_arrivals, hint} end' else unset MESSAGE_ID printf '%s\n' 'No valid outcome. Inspect the wait error before reading a body.' >&2 false fi
The JavaScript, Python and Java examples also print the matched body, falling back to HTML for an HTML-only message. Treat email HTML as untrusted text. They request ten-minute message retention; the inbox remains until you delete it. Accepted messages still count toward usage.
After all senders and waits finish, delete only this test’s inbox from the console. For Bash / curl, continue in the same terminal to read the matched body and clean up this run. Those Bash guides use the Bash tab’s variables; program variables do not carry over into your terminal.
If the request does not succeed
- Missing inbox
- Run registration first and use the returned address with a key from the same project.
- Invalid input
- Check the error’s field details. Preserve the documented field names and milliseconds for timeout.
- Sign-in or permission error
- Check the project key and its scopes. Console sign-in does not authenticate these terminal commands.
- Quota exhausted
- Check Usage and the reported reset time. Waiting is unmetered but still checks the received-message quota.
- Too many waits or requests
- Reduce concurrency and follow the server’s retry guidance within your test’s total time budget.
- Network or service failure
- Check the API address and connection. Retry within a bounded budget; do not treat the error as a timeout result.