# Your first email

Receive your first verification code with JavaScript/TypeScript, Python, Java or Bash/curl.

<a id="guide-content"></a>



<a id="quickstart"></a>



<a id="receive-your-first-email"></a>

## 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).

<a id="choose-your-language"></a>

### 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.

<a id="first-email-javascript"></a>

### 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
```

<a id="first-email-python"></a>

### 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
```

<a id="first-email-java"></a>

### 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
```

<a id="first-email-bash"></a>

### 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.  <a id="set-up-this-run"></a>
    
    ### 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.  <a id="register-the-inbox"></a>
    
    ### 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.  <a id="start-waiting-then-send-your-email"></a>
    
    ### 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.  <a id="check-the-outcome"></a>
    
    ### 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.

<a id="if-the-request-does-not-succeed"></a>

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)
