# InboxSpec documentation
Public guides for receiving and testing email. The API's [OpenAPI schema](https://api.inboxspec.com/openapi.json) owns request and response contracts.
---
Source: https://www.inboxspec.com/docs
# Build with InboxSpec
Receive real email in your tests and give agents a predictable way to wait for it.
InboxSpec gives your automated tests and agents real email addresses. Receive a message, wait for the code or link you need, and continue your application flow.
**Start with one real email**
The first-email guide takes you from an empty project to a matched message in your preferred language.
[Follow the quickstart →](https://www.inboxspec.com/docs/markdown/first-email.md)
## Choose your path
[Receive your first email Create an inbox and wait for a verification code. Get started →](https://www.inboxspec.com/docs/markdown/first-email.md)
[Add email to your tests Build isolated fixtures and assert the right message. Write a test →](https://www.inboxspec.com/docs/markdown/ci.md)
[Capture application mail Connect your application's SMTP configuration. Set up capture →](https://www.inboxspec.com/docs/markdown/capture-smtp.md)
[Connect an agent Understand MCP setup and available connections. Explore MCP →](https://www.inboxspec.com/docs/markdown/mcp.md)
## One workflow, your choice of client
Use [REST](https://www.inboxspec.com/docs/markdown/rest.md), the [TypeScript SDK](https://www.inboxspec.com/docs/markdown/sdk.md) or the [CLI](https://www.inboxspec.com/docs/markdown/cli.md) to register an inbox and wait for a matching message. A wait returns a match or an explicit timeout. Authentication, quota and service failures remain errors you can act on.
Already integrating? Open the [API reference](https://www.inboxspec.com/docs/markdown/api-reference.md) for request schemas and permissions.
---
Source: https://www.inboxspec.com/docs/first-email
# 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.
```javascript
// 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.
```
```bash
node first-email.mjs
```
### Python
Use Python 3. Save as `first_email.py`; the example uses only the standard library.
```python
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.
```
```bash
python3 first_email.py
```
### Java
Use Java 17+ and Gson for JSON parsing. Save as `FirstEmail.java`.
```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:
```bash
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.java
```
### Bash / 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.
```bash
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.
```bash
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.
```bash
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.
```bash
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](https://www.inboxspec.com/docs/markdown/message-bodies.md) and [clean up this run](https://www.inboxspec.com/docs/markdown/cleanup.md). 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.
[Full error codes and remedies](https://api.inboxspec.com/docs/errors)
---
Source: https://www.inboxspec.com/docs/how-it-works
# How it works
Understand projects, inboxes and the two ways email reaches InboxSpec.
## Projects and inboxes
A project owns inboxes, messages and API credentials. Console sign-in identifies a person; an API key authorizes access to one project. Create a separate project when you need to isolate local development from shared CI.
## Two delivery paths
For incoming mail, register an inbox and send to its returned address through normal MX delivery. For outgoing application mail, configure [Capture SMTP](https://www.inboxspec.com/docs/markdown/capture-smtp.md). Capture files messages into separate capture inboxes; it does not deliver into your MX test fixture.
## Wait, inspect, continue
A blocking wait selects the oldest matching message in the chosen inbox. Read the result status before using ranked code candidates or links. Waits do not consume messages, so isolate each new test journey with a fresh run.
[Try the complete first-email workflow →](https://www.inboxspec.com/docs/markdown/first-email.md)
---
Source: https://www.inboxspec.com/docs/inboxes
# Inboxes
Create a deterministic address and keep each test run separate.
## Register before sending
Use the [first-email recipe](https://www.inboxspec.com/docs/markdown/first-email.md) to register a deterministic inbox and keep its returned address. Guessing an address does not register it. The API key and inbox must belong to the same project.
## Isolate each test run
Give each independent journey a fresh run ID. For repeatable suites, [fixture manifests](https://www.inboxspec.com/docs/markdown/fixtures.md) provision named inboxes and detect drift without clearing existing mail.
## Inspect and clean up
Watch arrivals under Project → Inboxes in the console. Read full message content with a project-authenticated client. After producers and waits finish, clean up the exact run or let its configured retention expire.
---
Source: https://www.inboxspec.com/docs/capture-smtp
# Capture SMTP
Capture outgoing application mail over authenticated SMTP with STARTTLS.
## Connect your application
1. Open your project in the console and choose Capture SMTP.
2. Issue a capture credential and save its show-once value in your application's secret environment.
3. Copy the hostname and username displayed for your deployment. Use port 587 with STARTTLS and SMTP authentication.
4. Send a synthetic message from your application and inspect its capture inbox.
## Check actual delivery
Confirm the expected message arrives and inspect its recipient and content. A successful connection alone does not prove delivery. Capture inboxes are separate from registered MX fixtures; choose the corresponding inbox when waiting.
Read the [API reference](https://api.inboxspec.com/docs) for capture credential operations and permissions.
---
Source: https://www.inboxspec.com/docs/message-bodies
# Inspecting messages
Read text, HTML, links and attachments after a matching email arrives.
For the Bash commands below, continue in the same terminal after the Bash / curl tab of the [first-email quickstart](https://www.inboxspec.com/docs/markdown/first-email.md#first-email-bash). Its run ID, inbox and matched message variables are used below.
## Read the matched message body
Run after a match in the same terminal. The key remains in your environment.
```bash
if [ -n "$MESSAGE_ID" ]; then
curl --fail-with-body --silent --show-error "$INBOXSPEC_API_URL/v1/messages/$MESSAGE_ID/body?format=text" \
-H "Authorization: Bearer $INBOXSPEC_API_KEY"
else
printf '%s\n' 'A matched message is required; inspect the wait outcome.' >&2
false
fi
```
If the InboxSpec CLI is already installed, it uses the same API address and key environment variables:
```bash
if [ -n "$MESSAGE_ID" ]; then
inboxspec messages body "$MESSAGE_ID" --format text
else
printf '%s\n' 'A matched message is required; inspect the wait outcome.' >&2
false
fi
```
A message without a text part returns `unsupported_format`; use a format listed in that error. [The API reference](https://api.inboxspec.com/docs) also covers HTML, links and attachments.
---
Source: https://www.inboxspec.com/docs/waits
# Waiting for email
Distinguish a match, a timeout and an error within one test deadline.
### Outcomes and retry budget
Waiting is unmetered and exempt from request rate limits, but received-message quota and concurrent-wait limits still apply. Use a 60-second wait within a fixed total test budget. After timeout, inspect the hint and non-matching count. Reissue only while time remains; do not turn the wait into endless polling. Code values are ranked candidates, not proof of which code your app expects.
Authentication and invalid-input errors need repair. Check Usage for quota errors. For retryable transport or concurrency failures, respect the returned retry guidance and remaining budget. [Error reference](https://api.inboxspec.com/docs/errors).
---
Source: https://www.inboxspec.com/docs/otp-tests
# OTP and link assertions
Correlate verification codes and reset links with the exact application journey.
### OTP login, correlation and bounded waits
This SDK recipe uses application-specific trigger and completion functions. Set a total test deadline longer than the wait and its transport margin. Configure the application to send through the fixture domain’s MX route: authenticated Capture SMTP files mail under separate capture inboxes.
```typescript
import { InboxSpec } from "@inboxspec/sdk";
const client = new InboxSpec({
apiKey: process.env.INBOXSPEC_API_KEY!,
baseUrl: process.env.INBOXSPEC_API_URL!,
});
const runId = "ci.4182.a1.w1"; // Supply your run, attempt and worker IDs.
const correlationId = runId + ".signup";
try {
const setup = await client.fixtures.ensure({
run_id: runId,
manifest: { version: 1, fixtures: [
{ name: "signup", retention_seconds: 600 },
] },
});
if (setup.status !== "ready") throw new Error("Fixture setup failed");
const inbox = setup.fixtures.find((f) => f.name === "signup")!.inbox!;
const since = new Date().toISOString();
// Your application sends X-InboxSpec-Correlation-ID: correlationId.
await triggerYourApplication(inbox.address, correlationId);
const outcome = await client.messages.wait({
inbox: inbox.id,
match: { since, correlation_id: correlationId, has_code: true },
request_id: "wait-" + correlationId,
timeout_ms: 60_000,
});
if (outcome.status === "timeout") throw new Error(outcome.reason);
const code = outcome.message.codes[0];
if (!code) throw new Error("No verification code");
await completeYourApplicationFlow(code);
} finally {
const cleanup = await client.fixtures.cleanup(runId);
if (cleanup.status !== "complete") throw new Error("Cleanup incomplete");
}
```
If the sender cannot set correlation headers, use a fresh inbox per journey and narrow with `since`, `subject_contains` and `from_contains`. `to_contains` matches headers and may omit BCC recipients; the selected inbox is the envelope-delivery boundary. `correlation_id` exactly matches `X-InboxSpec-Correlation-ID`, case-sensitively. For password reset or verification links, use `has_link_kind` with `reset_password` or `verify` and validate the application origin before following a link.
Timeouts return HTTP 200 with `status: timeout` and `reason: no_arrivals` or `no_matching_arrivals`. Check the hint and arrival count. Authentication, quota, concurrency and infrastructure errors remain separate. Responses include `started_at`, `completed_at`, `waited_ms` and `request_id`; matched mail includes `received_at`, measured at SMTP DATA completion.
If workspace receiving is blocked and no stored message matches, waits return HTTP 402 with `organisation_limit_exceeded`. For an exhausted message allowance, read `details.used`, `limit` and `period_resets_at`. Pause the sending batch and review workspace usage before retrying. Existing mail remains readable; deleting it does not reset monthly usage. Before creating test accounts, use `GET /v1/usage` with `usage:read` scope to inspect the effective `organisation` allowance and extra-usage settings.
Request IDs trace attempts; they are not durable replay keys. Waits do not consume messages, so repeated waits can return the same oldest match. Use a fresh namespace for a new test attempt and reuse a run only for setup retries. Avoid polling loops. Honor retry guidance within one deadline; use SDK `signal` or CLI Ctrl+C to cancel.
```bash
inboxspec fixtures cleanup --run ci.4182.a1.w1 --json
```
Run cleanup after producers and waits finish, in `finally` or a CI `always()` step. It uses both the run prefix and exact reserved run label, preventing deletion of nested runs. Incomplete cleanup is nonzero and can be resumed with the same run ID. Expiry is a fallback; physical removal may lag. Cleanup does not reset usage or delete accounting history.
---
Source: https://www.inboxspec.com/docs/fixtures
# Fixtures
Provision repeatable test inboxes and detect configuration drift.
### Ensure fixtures and isolate each run
Commit a non-secret `inboxspec.fixtures.json` manifest:
```json
{
"version": 1,
"fixtures": [{ "name": "signup", "retention_seconds": 600 }]
}
```
```bash
inboxspec fixtures ensure --file inboxspec.fixtures.json \
--run ci.4182.a1.w1 --project-id "$EXPECTED_PROJECT_ID" \
--environment staging --scope messages:read --scope messages:wait --json
```
The REST operation is `POST /v1/fixtures/ensure`. Require `status: ready`; HTTP 200 can also contain `partial` or `failed`. The CLI exits zero only for readiness. The same run and definition reuse inbox IDs and any messages still within retention. Inboxes remain until explicitly deleted. Changed labels, domain or retention return `fixture_drift`; setup never clears mail to reconcile a change.
Include run number, retry attempt and worker in a short run ID. Give each independent test journey its own fixture or run. Names use lowercase letters, digits, underscores and hyphens; run IDs also allow single dots between levels. The full address local part, including the project key, must fit 64 characters. Retention must be at least 60 seconds and within your plan ceiling.
---
Source: https://www.inboxspec.com/docs/ci
# CI email tests
Connect setup, application delivery, assertions and scoped cleanup.
Install a [reviewed client](https://www.inboxspec.com/docs/markdown/installation.md) before adding these steps to your test runner.
1. [Check configuration and permissions](https://www.inboxspec.com/docs/markdown/troubleshooting.md)
2. [Ensure isolated fixtures](https://www.inboxspec.com/docs/markdown/fixtures.md)
3. [Trigger your app and assert the correlated email](https://www.inboxspec.com/docs/markdown/otp-tests.md)
4. [Clean up after the test](https://www.inboxspec.com/docs/markdown/cleanup.md#fixture-cleanup)
## Reliable CI email tests
Set up fixtures, trigger your application, wait for one correlated message, then clean up the exact run. Setup readiness checks authentication and inbox provisioning; it does not prove that your application sent mail.
---
Source: https://www.inboxspec.com/docs/workloads
# GitHub and Vercel
Use trusted workload identity for short-lived project access.
### GitHub Actions and Vercel previews
An organisation owner configures a workload binding in Credentials for a dedicated test project. Members can inspect trust. These integrations exchange platform identity for an InboxSpec key lasting at most five minutes; no standing InboxSpec secret needs copying between platforms. Grants reach the whole dedicated project, with inbox and message test permissions, and cannot administer keys, domains, webhooks or memberships. Revoking a binding immediately revokes its issued keys.
For GitHub, use the authentication Action from the reviewed private release or source directory `.github/actions/inboxspec-auth`. Bind the exact subject, immutable repository and owner IDs, branch ref and workflow ref, plus environment if used. Pin and review the workflow and Action. Only trusted branch push/manual-dispatch contexts are accepted; pull requests, tags and workflow\_run are denied.
```yaml
permissions:
contents: read
id-token: write
# After trusted checkout and installing the reviewed CLI:
steps:
- uses: ./.github/actions/inboxspec-auth
with:
api_url: ${{ vars.INBOXSPEC_API_URL }}
binding_id: ${{ vars.INBOXSPEC_GITHUB_BINDING_ID }}
- run: inboxspec auth check --json
```
This is a job fragment, not a complete workflow. The Action obtains identity, masks the exchanged key, and exports API configuration for subsequent steps. Run fixture setup and the mail test immediately; acquire a fresh identity for another grant after expiry.
For Vercel, trust an exact team, project, subject and preview environment, including immutable IDs. All code with that project’s preview identity can request a grant; use a dedicated project whose preview code is trusted. Production and local development identities are rejected. Configure non-secret `INBOXSPEC_API_URL` and `INBOXSPEC_VERCEL_BINDING_ID`.
```typescript
import { getVercelOidcToken } from "@vercel/oidc";
import { inboxSpecFromVercel } from "@inboxspec/sdk";
const { client, expiresAt } = await inboxSpecFromVercel({
apiUrl: process.env.INBOXSPEC_API_URL!,
bindingId: process.env.INBOXSPEC_VERCEL_BINDING_ID!,
getToken: getVercelOidcToken,
});
```
Use `@vercel/oidc` 3.7 or newer in server-side preview builds/functions. The adapter requests a custom audience and fresh JTI. Reuse the client only until expiry; a fresh grant requires fresh identity. Do not return credentials or provider tokens to a browser. Marketplace installation and public npm distribution are not required for this flow and are not currently offered.
---
Source: https://www.inboxspec.com/docs/cleanup
# Cleaning up
Remove only your test run after producers and waits have finished.
For the Bash commands below, continue in the same terminal after the Bash / curl tab of the [first-email quickstart](https://www.inboxspec.com/docs/markdown/first-email.md#first-email-bash). Its run ID, inbox and matched message variables are used below.
## Clean up this test run
Optional, after inspection: this permanently deletes inboxes with this run’s tag prefix, their messages and stored content.
Keep the generated run ID; do not replace it with a shared prefix. Cleanup needs inbox write permission.
```bash
if [[ "$RUN_ID" =~ ^first-[a-f0-9]{24}$ ]]; then
curl --fail-with-body --silent --show-error "$INBOXSPEC_API_URL/v1/inboxes/cleanup" \
-H "Authorization: Bearer $INBOXSPEC_API_KEY" \
-H "content-type: application/json" \
-d "$(jq -n --arg tag_prefix "$RUN_ID" '{tag_prefix: $tag_prefix}')"
else
printf '%s\n' 'Use the complete run ID created in step 1; cleanup was not sent.' >&2
false
fi
```
## Clean up CI fixtures
For a fixture manifest, use the exact run ID passed to fixture setup. The example below matches the run in the [OTP test recipe](https://www.inboxspec.com/docs/markdown/otp-tests.md); replace it with your own run ID.
```bash
inboxspec fixtures cleanup --run ci.4182.a1.w1 --json
```
Run cleanup after producers and waits finish, in a finally block or an always-run CI step. Check for complete cleanup and resume an incomplete cleanup with the same run ID. Cleanup does not reset usage or remove accounting history.
---
Source: https://www.inboxspec.com/docs/rest
# REST API
Choose JavaScript, Python, Go, Java, C#, PHP or curl examples for the hosted API.
## Set up a client
REST works directly with your project key and the API address below. SDK and CLI archives are available in the private Clients 0.1.0 release. npm registry publication is pending. The SDK supports Node.js 24+ or Bun 1.4+; the CLI needs Bun 1.4+ on Linux or macOS.
```bash
export INBOXSPEC_API_URL='https://api.inboxspec.com'
# Set INBOXSPEC_API_KEY to your saved project key in this terminal.
```
## Make your first request
The [curl quickstart](https://www.inboxspec.com/docs/markdown/first-email.md#first-email-bash) includes setup, inbox registration, a bounded wait and response handling. Keep the same terminal environment when continuing to body inspection and cleanup.
## Choose your language
These examples make the same read-only request: list inboxes in your project. Set `INBOXSPEC_API_KEY` in your environment to a project key with inbox read permission. Optionally set `INBOXSPEC_API_URL` to your API origin without a trailing slash. Run these on your server or development machine; keep credentials out of source control.
JavaScript / TypeScript
Save as list-inboxes.mjs and run node list-inboxes.mjs with Node.js 24+. The same code works in a server-side TypeScript module.
```javascript
const origin = process.env.INBOXSPEC_API_URL || "https://api.inboxspec.com";
const key = process.env.INBOXSPEC_API_KEY;
if (!key) throw new Error("Set INBOXSPEC_API_KEY first");
const response = await fetch(origin + "/v1/inboxes", {
headers: { Authorization: "Bearer " + key },
signal: AbortSignal.timeout(10_000),
});
if (!response.ok) throw new Error("HTTP " + response.status);
console.log(await response.json());
```
Python
Save as list\_inboxes.py and run python3 list\_inboxes.py. Uses Python 3’s standard library.
```python
import json
import os
import urllib.request
origin = os.environ.get("INBOXSPEC_API_URL", "https://api.inboxspec.com")
key = os.environ["INBOXSPEC_API_KEY"]
request = urllib.request.Request(
origin + "/v1/inboxes",
headers={"Authorization": "Bearer " + key},
)
# HTTP errors raise HTTPError; they are not an empty inbox list.
with urllib.request.urlopen(request, timeout=10) as response:
print(json.load(response))
```
Java
Save as ListInboxes.java and run java ListInboxes.java with Java 17+. Uses the standard HTTP client.
```java
import java.net.URI;
import java.net.http.*;
import java.time.Duration;
class ListInboxes {
public static void main(String[] args) throws Exception {
String origin = System.getenv("INBOXSPEC_API_URL");
if (origin == null) origin = "https://api.inboxspec.com";
String key = System.getenv("INBOXSPEC_API_KEY");
if (key == null || key.isBlank())
throw new IllegalStateException("Set INBOXSPEC_API_KEY first");
var client = HttpClient.newBuilder()
.connectTimeout(Duration.ofSeconds(10)).build();
var request = HttpRequest.newBuilder(URI.create(origin + "/v1/inboxes"))
.header("Authorization", "Bearer " + key)
.timeout(Duration.ofSeconds(10)).GET().build();
var response = client.send(request, HttpResponse.BodyHandlers.ofString());
if (response.statusCode() < 200 || response.statusCode() >= 300)
throw new IllegalStateException("HTTP " + response.statusCode());
System.out.println(response.body());
}
}
```
curl
Run in a Bash terminal with curl installed.
```bash
# Set INBOXSPEC_API_KEY in this terminal first.
: "${INBOXSPEC_API_KEY:?Set your project key first}"
curl --fail-with-body --silent --show-error --max-time 10 \
"${INBOXSPEC_API_URL:-https://api.inboxspec.com}/v1/inboxes" \
-H "Authorization: Bearer $INBOXSPEC_API_KEY"
```
A successful response contains this project’s inbox list, which may be empty. These examples stop on an HTTP error. They do not create an inbox or send mail; follow the [first-email workflow](https://www.inboxspec.com/docs/markdown/first-email.md) next.
### More languages and operations
The [interactive API reference](https://api.inboxspec.com/docs) generates examples from the same OpenAPI schema as the API. In **Client Libraries**, select a language; use **More** to see all clients. Each operation also has a language selector above its request example.
- **JavaScript / TypeScript:** choose Node.js → Fetch for server-side code. The [JavaScript quickstart](https://www.inboxspec.com/getting-started#javascript) shows the complete inbox, wait and body flow.
- **Python:** choose Requests, HTTPX or http.client.
- **Go:** choose NewRequest.
- **Java:** choose java.net.http or OkHttp.
- **C#:** choose HttpClient.
- **PHP:** choose cURL, Guzzle or Laravel HTTP Client.
- **Ruby:** choose net::http.
- **curl:** choose Shell → Curl, or follow the [step-by-step quickstart](https://www.inboxspec.com/docs/markdown/first-email.md).
These are HTTP client examples; install any third-party library you select. Keep project keys in your server-side environment or secret store, never browser code. Register an inbox before waiting, allow more time in your HTTP client than the requested wait, and check `status` before reading a matched message. Follow [cleanup](https://www.inboxspec.com/docs/markdown/cleanup.md) after your test senders and waits have finished.
## Request schemas
[Browse the API reference](https://api.inboxspec.com/docs) or download the [OpenAPI schema](https://api.inboxspec.com/openapi.json). [Error codes and remedies](https://api.inboxspec.com/docs/errors) explain failures separately from a successful wait that timed out.
---
Source: https://www.inboxspec.com/docs/installation
# Client installation
Install the reviewed private SDK and CLI release archives.
## Set up a client
REST works directly with your project key and the API address below. SDK and CLI archives are available in the private Clients 0.1.0 release. npm registry publication is pending. The SDK supports Node.js 24+ or Bun 1.4+; the CLI needs Bun 1.4+ on Linux or macOS.
```bash
export INBOXSPEC_API_URL='https://api.inboxspec.com'
# Set INBOXSPEC_API_KEY to your saved project key in this terminal.
```
### Install a reviewed release
Download all archives and SHA256SUMS from the [private release](https://github.com/hussainanjar/seagrass/releases/tag/clients-v0.1.0). Repository access is required; contact your InboxSpec owner if it is unavailable. REST works without these packages.
```bash
sha256sum -c SHA256SUMS
# macOS: shasum -a 256 -c SHA256SUMS
npm install ./inboxspec-sdk-0.1.0.tgz
npm install --global ./inboxspec-cli-0.1.0.tgz
inboxspec --help
```
These npm commands install local archives. Pin the reviewed version and checksums in CI. In an installed SDK example, import `InboxSpec` from `@inboxspec/sdk`.
---
Source: https://www.inboxspec.com/docs/sdk
# TypeScript SDK
Register an inbox and wait for email from Node.js or Bun.
First follow [client installation](https://www.inboxspec.com/docs/markdown/installation.md) and set your API address and project key in the terminal environment. The examples below also support a repository checkout.
### TypeScript SDK from the same checkout
Save this as `first-email.ts` at the repository root and run `bun run first-email.ts`. Send your email to the printed address while the wait is pending. The client needs an explicit hosted origin; its local default is not your hosted project.
```typescript
import { InboxSpec } from "./packages/sdk/src/index.ts";
const baseUrl = process.env.INBOXSPEC_API_URL;
const apiKey = process.env.INBOXSPEC_API_KEY;
if (!baseUrl || !apiKey) throw new Error("Set the API address and project key");
const client = new InboxSpec({ baseUrl, apiKey });
const run_id = "first-" + crypto.randomUUID();
const inbox = await client.inboxes.forRun({ run_id });
console.log("Send test mail to:", inbox.address);
const result = await client.messages.wait({
inbox: inbox.address, match: { has_code: true }, timeout_ms: 60000,
});
if (result.status === "timeout") {
console.log(result.hint, result.non_matching_arrivals);
} else {
console.log("Code candidates:", result.message.codes);
console.log(await client.messages.body(result.message.id, "text"));
}
```
---
Source: https://www.inboxspec.com/docs/cli
# Command line
Use the InboxSpec CLI in a terminal or test runner.
First follow [client installation](https://www.inboxspec.com/docs/markdown/installation.md) and set your API address and project key in the terminal environment. The examples below also support a repository checkout.
### Alternative: CLI from a source checkout
Run from the repository root. The function makes the commands in this guide available in the same Bash terminal.
```bash
bun install --frozen-lockfile
export INBOXSPEC_CHECKOUT="$PWD"
inboxspec() { bun run "$INBOXSPEC_CHECKOUT/packages/cli/src/bin.ts" "$@"; }
inboxspec --help
```
Run the [first-email recipe](https://www.inboxspec.com/docs/markdown/first-email.md), then use `inboxspec messages body "$MESSAGE_ID" --format text`. Wait exits 0 for a match, 1 for a timeout, 2 for invalid usage, 3 for authentication, 4 for limits and 5 for service failures. Ctrl+C cancels the client; do not retry indefinitely.
---
Source: https://www.inboxspec.com/docs/mcp
# MCP
Use Connect agent in your project to set up Claude Code, Codex or another MCP client with a project key.
## Connect your AI agent
Create a test inbox, wait for email and read its verification code from your agent.
Connect to hosted InboxSpec at `https://api.inboxspec.com/mcp` with your project key. No local server is needed.
## 1\. Save your project key
In the console, choose your project and open Connect agent in the sidebar to create a dedicated key and follow the setup. You can also manage keys under Credentials. Save its show-once value as `INBOXSPEC_API_KEY` in the environment that launches your client. Keep it out of prompts and configuration files.
Set the key in a Bash terminal
Run this command, paste your saved key at the hidden prompt and press Enter. Launch your client from this same terminal. The key stays out of shell history.
```bash
read -r -s -p 'InboxSpec project key: ' INBOXSPEC_API_KEY
export INBOXSPEC_API_KEY
printf '\n'
```
The key selects your project. Use a dedicated test project and review tool calls that delete messages or clean up a run. This release uses project keys; it does not offer browser sign-in.
## 2\. Choose your client
Claude Code
Add the InboxSpec entry to your project’s `.mcp.json`. Keep any existing server entries.
```json
{
"mcpServers": {
"inboxspec": {
"type": "http",
"url": "https://api.inboxspec.com/mcp",
"timeout": 300000,
"headers": {
"Authorization": "Bearer ${INBOXSPEC_API_KEY}"
}
}
}
}
```
Start Claude Code with the key in its environment, approve this project’s MCP configuration and open `/mcp` to check the connection. The configuration reads the key from your environment each time.
Codex
Add this entry to `~/.codex/config.toml`, then launch Codex with the key in its environment:
```text
[mcp_servers.inboxspec]
url = "https://api.inboxspec.com/mcp"
bearer_token_env_var = "INBOXSPEC_API_KEY"
tool_timeout_sec = 300
```
Open `/mcp` to check that InboxSpec is connected. If a shared background server was started without the key, launch this session with `codex --no-daemon` from the configured terminal.
Other MCP clients
Use Streamable HTTP at `https://api.inboxspec.com/mcp`. Configure `Authorization: Bearer` with your project key using the client’s secret storage, and allow 300 seconds per tool call.
Your client must support bearer headers and cancellation. Clients that require browser sign-in cannot use this setup. Do not use the legacy SSE transport.
## 3\. Try one email
Ask your agent:
```text
Use InboxSpec to create an inbox
for a new, unique test run.
Show me its email address, then
wait up to 60 seconds for an email
with a verification code.
```
Send a test email to that address while the wait is pending. A match returns the message and code candidates; a timeout means no matching email arrived. Read `structuredContent.status` before using a result.
After the test, ask the agent to clean up only that run once all senders and waits have finished. See [wait outcomes and retries](https://www.inboxspec.com/docs/markdown/waits.md) and [cleanup](https://www.inboxspec.com/docs/markdown/cleanup.md) for details.
For client configuration details, see the [Claude Code](https://code.claude.com/docs/en/mcp) and [Codex](https://developers.openai.com/codex/mcp) references.
---
Source: https://www.inboxspec.com/docs/api-reference
# API reference
Find request schemas, management operations and the OpenAPI document.
### Management operations
Use your project key with [the API reference](https://api.inboxspec.com/docs). Domain management supports REST, SDK and CLI; webhooks and audit history support REST and SDK. MCP does not expose these management operations.
```bash
curl --fail-with-body --silent --show-error "$INBOXSPEC_API_URL/v1/domains" \
-H "Authorization: Bearer $INBOXSPEC_API_KEY"
curl --fail-with-body --silent --show-error "$INBOXSPEC_API_URL/v1/webhooks" \
-H "Authorization: Bearer $INBOXSPEC_API_KEY"
```
These read existing configuration. Use the reference’s create/verify/update operations only when you intend to change it. The console cannot rename or delete projects; no alternative customer operation is currently offered for those tasks.
[Download the OpenAPI schema](https://api.inboxspec.com/openapi.json) · [Error reference](https://api.inboxspec.com/docs/errors)
---
Source: https://www.inboxspec.com/docs/limits
# Limits and credentials
Understand project access, quotas, key lifetimes and replacement.
### Developer credentials and safe rotation
In Project → Credentials, an owner or admin can issue a CI tests key (30 days by default) or Local development key (seven days). Both support fixture setup, message read/wait and cleanup, and exclude domain, webhook and credential administration. Keys reach the entire project: use a separate development project to isolate local mail from hosted CI.
1. Choose Replace on an existing key. The replacement inherits its project and permissions; the old key stays active.
2. Save the show-once replacement in the intended caller’s secret environment.
3. Run auth check and the actual mail journey from that caller.
4. Check the new key’s last-used timestamp, then revoke the old key.
An expiring key’s replacement defaults to seven days; a non-expiring key stays non-expiring unless a bounded lifetime is requested. Expired keys can be replaced; revoked keys cannot. Last-used writes are coalesced to once a minute and show successful authentication, not proof that every caller migrated. Lost issuance responses cannot recover the secret; do not blindly retry replacement. Never put keys in logs, committed files or browser code.
## Usage and wait limits
Waiting is unmetered and exempt from request rate limits, but received-message quota and concurrent-wait limits still apply. Inspect Usage for the current allowance and reset time. Follow returned retry guidance within your test deadline; a quota error is not a timeout.
Retention controls how long messages remain available. Fixture retention must be at least 60 seconds and stay within the project's plan ceiling. See [wait outcomes](https://www.inboxspec.com/docs/markdown/waits.md) for error handling.
---
Source: https://www.inboxspec.com/docs/troubleshooting
# Troubleshooting
Diagnose API configuration, environment and permission errors.
### Check configuration before running tests
Use `INBOXSPEC_API_KEY` and `INBOXSPEC_API_URL`. The CLI accepts legacy key aliases `SEAGRASS_API_KEY` and `SEAGRASS_PI_KEY`, and URL aliases `INBOXSPEC_BASE_URL`, `SEAGRASS_API_URL` and `SEAGRASS_BASE_URL`, in that priority order after the preferred name. Explicit flags override environment values, which override the user config file. An empty preferred value fails instead of silently using an older key; conflicts report variable names only. Update shell scripts explicitly; the SDK takes constructor options.
```bash
inboxspec auth check --project-id "$EXPECTED_PROJECT_ID" \
--environment staging --scope inboxes:write \
--scope messages:read --scope messages:wait --json
```
Choose the environment you intend to test. `GET /v1/auth/check` reports project, scopes, key expiry and deployment context without returning the key. Console sign-in and project API authentication are separate.
- `expired_key` or `revoked_key`: obtain an active credential through Credentials.
- `invalid_key_or_environment`: check the selected key source and API origin. This deployment cannot know whether an unrecognized key belongs to another environment.
- `project_mismatch`, `environment_mismatch` or `environment_unknown`: correct the expected context or server configuration.
- `wrong_api_hostname` or `invalid_api_url`: use the API origin, with HTTPS outside loopback. Authenticated redirects are refused.
- `insufficient_scope`: ask for the required permissions. DNS, TLS and timeout errors are transport failures, not evidence of an invalid key.
---
Source: https://www.inboxspec.com/docs/service-health
# Service health
Interpret fixture diagnostics and regional mail delivery observations.
### Fixture diagnostics and service health
Project → Fixtures compares exact run IDs with optional expected names and shows existence, expiry, latest arrival, credential status and recent API request metadata. It does not fetch message bodies. Request history is best-effort, limited to 100 records within 24 hours; invalid keys cannot be attributed to a project. Missing or truncated observations are unknown, not proof of absence.
Use [service status](https://www.inboxspec.com/status) for regional synthetic MX/capture journeys and [the site JSON report](https://www.inboxspec.com/service-status.json) for automation. The API’s [service-status endpoint](https://api.inboxspec.com/service-status) returns 200 for recent healthy routes and 503 for degraded or unknown state. `/health` reports process liveness only. Staging runs scheduled probes; samples older than five minutes are unknown.
Latency is synthetic send-to-read duration, including network and request overhead. Reports include sample counts, sparse-sample flags, p50/p95/p99 and failure rate. Project delays require that project’s own probe observations; global health does not fill missing project data. These measurements cannot prove that StaticForms or another sender submitted a particular message.
See the [API reference](https://api.inboxspec.com/docs) for full request schemas and permission requirements.
---
Source: https://www.inboxspec.com/docs/mcp-self-hosting
# Self-hosted MCP
Run the MCP server for your own InboxSpec installation and check the connection.
## Prepare your installation
This guide is for teams running their own InboxSpec installation from a repository checkout. For hosted availability, see [Connect your AI agent](https://www.inboxspec.com/docs/markdown/mcp.md).
Configure the MCP service with the same migrated database, object store and receiving configuration as your API. The local server uses these dependencies directly; it does not connect to the hosted REST API.
## Start the server
For a client that launches a local process over stdio, supply a project key in the process’s secret environment at startup. From the configured repository root, use:
```bash
bun run --cwd packages/mcp stdio
```
For a shared Streamable HTTP service, use:
```bash
bun run --cwd packages/mcp http
```
The default port is 8082 and the path is `/mcp`. Configure your MCP client with the URL of your own service and an `Authorization: Bearer` header sourced from its secret environment. Authentication uses a project API key, not browser sign-in. Never paste a project key into a prompt.
## Check the connection
1. Connect your client and list the available tools.
2. Call `inbox_for_run` to create an inbox for your test run.
3. Call `wait_for_message`, then send test mail to that inbox while the wait is pending.
4. Read `structuredContent.status` before using the result. A timeout is not a matching message.
Your client must support cancellation and a timeout longer than the requested wait; HTTP clients must support Streamable HTTP. See [wait outcomes and retry limits](https://www.inboxspec.com/docs/markdown/waits.md). After producers and waits finish, [clean up only your test run](https://www.inboxspec.com/docs/markdown/cleanup.md).