> ## Documentation Index
> Fetch the complete documentation index at: https://docs.devhelm.io/llms.txt
> Use this file to discover all available pages before exploring further.

# email

> DevHelm CLI email commands: get testing addresses, inject and wait for messages, read bodies and OTP codes, and manage mail domains

Get testing addresses, inject or wait for messages, read their bodies, OTP codes, and links, and manage the domains they arrive on. For how testing mailboxes work, see [Email testing](/testing/email/overview).

<Note>
  These commands manage **testing mailboxes** that receive mail. To have DevHelm send alert emails, use an email alert channel: see [`alert-channels`](/cli/commands/alert-channels).
</Note>

## Setup

The testing API needs an API key and a workspace ID. Export the key, look up the workspace, then export its ID:

```bash theme={null}
export DEVHELM_API_TOKEN=dh_live_xxxxxxxx
curl https://api.devhelm.io/api/v1/workspaces -H "Authorization: Bearer $DEVHELM_API_TOKEN"
export DEVHELM_WORKSPACE_ID=<workspace-id>
```

The CLI and Python SDK read both variables. The TypeScript SDK reads `DEVHELM_WORKSPACE_ID` and takes the key in its constructor. With curl, send the workspace as a header:

```bash theme={null}
-H "Authorization: Bearer $DEVHELM_API_TOKEN" \
-H "x-phelm-workspace-id: $DEVHELM_WORKSPACE_ID"
```

`--api-token` and `--api-url` override `DEVHELM_API_TOKEN` and `DEVHELM_API_URL` per command. There is no workspace flag. Without `DEVHELM_WORKSPACE_ID` the CLI sends workspace `1` and fails with 404 `Workspace doesn't exist`.

Every command takes `-o table|json|yaml` (default `table`) and `-v`. JSON and YAML print the bare object, without the API's `data` envelope.

Message commands take the mailbox as `--to <address>`, the **full** address (`local@domain`). The domain part selects the mail domain; the local part selects the mailbox.

## Commands

| Command | Description |
| - | - |
| `devhelm email address` | Print a new address on the workspace's assigned domain |
| `devhelm email receive` | Inject a test message into an address |
| `devhelm email wait` | Wait for a message to an address |
| `devhelm email clear` | Delete every message for one address |
| `devhelm email messages list` | List messages for one address |
| `devhelm email messages get <messageId>` | Get one message, with its text and HTML |
| `devhelm email messages source <messageId>` | Print the stored RFC822 source |
| `devhelm email messages raw <messageId>` | Download the raw `.eml` |
| `devhelm email attachments get <messageId> <attachmentId>` | Download one attachment |
| `devhelm email domains list` | List mail domains in the workspace |
| `devhelm email domains get <name>` | Show a domain and its DNS records |
| `devhelm email domains create` | Create a custom domain |
| `devhelm email domains verify <name>` | Check a custom domain's DNS |
| `devhelm email domains update <name>` | Disable a domain |
| `devhelm email domains delete <name>` | Delete a domain and its messages |
| `devhelm email domains activity` | Message counts for the last 24 hours |

## email address

Print a fresh address. Any local part on a domain receives mail, so no mailbox is created; use one address per test.

```bash CLI theme={null}
devhelm email address --label signup -o json
```

| Flag | Type | Description |
| - | - | - |
| `--label` | string | Local-part prefix. The CLI appends `-` and 8 random hex characters |
| `--domain` | string | Use this domain instead of the assigned one |

* Table output prints the address only. JSON prints `{email, domain, localPart}`.
* Without `--domain`, the CLI lists the workspace's domains and uses the active assigned one, creating it if there is none. No call is made for the address itself.
* The domain cap counts every domain in the organization. On Free or Developer, a second workspace gets 403 `INBOUND_EMAIL_DOMAIN_LIMIT_REACHED`.
* Read the domain from this output or `email domains list`. Do not build it by hand.

## email receive

Inject a message without a mail provider. It runs the same parsing as real mail, including OTP codes and links.

```bash CLI theme={null}
devhelm email receive \
  --to <address> \
  --from noreply@example.com \
  --subject "Your code" \
  --text "Your verification code is 482913"
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--to` | string | Yes | Full address. Only the local part picks the mailbox |
| `--from` | string | Yes | Sender address |
| `--subject` | string | — | Subject |
| `--text` | string | — | Plain-text body |
| `--html` | string | — | HTML body |

* The API answers 202 with `eventId`, `inbox`, and `receivedAt`: accepted, not yet stored. Use `email wait` to see the message.
* The CLI has no headers flag. To set `Message-ID`, `Date`, or custom headers, inject through the SDK or the API.
* Inject cannot add attachments.
* A disabled domain returns 400 `INBOUND_INJECT_DOMAIN_DISABLED`. A custom domain still pending DNS accepts inject.

## email wait

Block until a message to the address has been stored, then print it.

```bash CLI theme={null}
SINCE=$(date -u +%Y-%m-%dT%H:%M:%SZ)
devhelm email receive --to <address> --from noreply@example.com --subject "Your code" --text "Code: 482913"
devhelm email wait --to <address> --received-after "$SINCE" --subject-contains "your code" -o json
```

| Flag | Type | Default | Description |
| - | - | - | - |
| `--to` | string | — | Required. Full address |
| `--received-after` | string | local clock at call time | ISO 8601 timestamp. Only messages received at or after it match |
| `--subject-contains` | string | — | Case-insensitive substring of the subject |
| `--timeout-ms` | integer | `30000` | How long to wait. Values above `120000` are clamped |

<Warning>
  Always pass `--received-after` with a time taken **before** your app sends the mail. The default is the local clock when `wait` starts, so a message that arrived first is skipped and the wait times out.
</Warning>

* Wait returns the **oldest** match and does not consume it. For the next message, pass the previous `receivedAt` plus 1 ms.
* The result has `otp`, `links`, `bodyPreview`, and headers. `text` and `html` are `null`; run `email messages get` with the message ID for the body.
* The CLI matches the full address only. To wait on a local part plus a domain, use the SDK or the API.
* With no match before the timeout, the CLI prints `No matching inbound item arrived before timeout (<address>)` and exits with code `11`. A wait on a disabled domain times out the same way.
* All waits in the organization share 40 requests per minute, across email, inboxes, and every workspace. Over the limit returns 429.

Read the code from the JSON with `jq -r '.otp[0].value'` when the email has one code. See [Wait for an email in a test](/testing/email/wait) and [Read OTP codes and links](/testing/email/otp-and-links).

## email clear

Delete every message for one address, in teardown.

```bash CLI theme={null}
devhelm email clear --to <address> --yes
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--to` | string | Yes | Full address |
| `--yes`, `-y` | boolean | — | Skip the prompt. Non-interactive runs without it are refused |

The CLI cannot delete a single message. Use `message.delete()` in the SDKs, or `DELETE /api/v1/email/domains/<domain>/messages/<message-id>`.

## email messages list

List messages for one address, newest first. The table shows `ID`, `FROM`, `SUBJECT`, `OTP`, and `RECEIVED`.

```bash CLI theme={null}
devhelm email messages list --to <address> --query "reset" --limit 10
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--to` | string | Yes | Full address |
| `--query` | string | — | Substring of the subject, sender, or local part. Not the body |
| `--limit` | integer | — | Page size, 1–100 (default `50`) |
| `--cursor` | string | — | Cursor from a previous page |

The CLI prints the rows of one page and not the next cursor. Page through with the SDK or the API. As with `wait`, `text` and `html` are `null` in list rows.

## email messages get

Get one message. Use `-o json` for the body; the table shows only the summary columns.

```bash CLI theme={null}
devhelm email messages get <message-id> --to <address> -o json
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--to` | string | Yes | Full address the message was sent to |

* `text` and `html` are clipped at 256 KB; `bodyTruncated` says whether they were cut.
* `html` is returned unsanitized. Render it only in a sandboxed frame.
* `links[].href` keeps `&amp;` from the HTML today. Replace `&amp;` with `&` before you open a link.

## email messages source

Print the stored message as RFC822 text, clipped at 256 KB. Table output prints the source and warns when it was cut; JSON prints `{source, truncated}`.

```bash CLI theme={null}
devhelm email messages source <message-id> --to <address>
```

## email messages raw

Download the full message as `.eml`. The signed link lasts 60 seconds; the CLI downloads it at once. The file's SHA-256 matches the message's `sha256`.

```bash CLI theme={null}
devhelm email messages raw <message-id> --to <address> --file ./mail/
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--to` | string | Yes | Full address |
| `--file` | string | Yes | File or directory to write. A directory gets `message.eml` |

## email attachments get

Download one attachment by ID.

```bash CLI theme={null}
devhelm email attachments get <message-id> <attachment-id> --to <address> --file ./mail/
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--to` | string | Yes | Full address |
| `--file` | string | Yes | File or directory to write |

Inject cannot add attachments, and the `attachments` list is not filled for real mail today.

## email domains list

List the workspace's mail domains. The table shows `NAME`, `KIND` (`assigned` or `custom`), `STATUS`, and `MX`.

```bash CLI theme={null}
devhelm email domains list -o json
```

| Flag | Type | Description |
| - | - | - |
| `--search` | string | Match the domain name |

## email domains get

Table output prints the DNS records; `-o json` prints the whole domain, including `status` and each record's `found`.

```bash CLI theme={null}
devhelm email domains get <domain>
```

## email domains create

Create a custom domain and print the two DNS records to publish: a TXT record at `_devhelm-verification.<domain>` and an MX record to `mx.devhelmmail.com` at priority 10.

```bash CLI theme={null}
devhelm email domains create --name mail-test.example.com
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--name` | string | Yes | The custom domain. Use a subdomain you do not receive real mail on |

* Custom domains need the Team testing plan or above. Otherwise create returns 403 `INBOUND_CUSTOM_MX_PLAN_REQUIRED`.
* The domain starts as `pending_dns`.
* `create` makes custom domains only. The assigned domain comes from `email address`.

See [Receive on your own domain](/testing/email/custom-domains).

## email domains verify

Check the DNS records now. Prints `<name> <status>`, then any records that still do not match.

```bash CLI theme={null}
devhelm email domains verify mail-test.example.com
```

When both records match, the domain becomes `active`. Verify also re-enables a disabled custom domain whose records match.

## email domains update

Disable a domain. `disabled` is the only status the CLI accepts.

```bash CLI theme={null}
devhelm email domains update mail-test.example.com --status disabled --yes
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--status` | string | Yes | `disabled` |
| `--yes`, `-y` | boolean | — | Skip the prompt |

A disabled domain refuses inject, and waits on it time out. To turn it back on, run `email domains verify` for a custom domain. For the assigned domain, PATCH `{"status": "active"}` through the SDK or the API.

## email domains delete

Delete a domain and every message on it.

```bash CLI theme={null}
devhelm email domains delete <domain> --yes
```

<Warning>
  Deleting the assigned domain breaks every address on it. The next `email address` creates a domain with a new random name.
</Warning>

## email domains activity

Message counts in 24 hourly UTC buckets. The table shows the total per domain; JSON has the buckets. Get domain IDs from `email domains list -o json`.

```bash CLI theme={null}
devhelm email domains activity --id <domain-id> -o json
```

| Flag | Type | Required | Description |
| - | - | - | - |
| `--id` | string | Yes | Domain ID. Repeat for up to 100 domains |

## Common errors

| Error | Cause | Fix |
| - | - | - |
| 404 `Workspace doesn't exist` | `DEVHELM_WORKSPACE_ID` is unset or wrong | Export the workspace ID |
| `Expected a full address`, or 400 `INBOUND_WAIT_TO_INVALID` from `wait` | `--to` was a local part | Pass `local@domain` |
| 403 `INBOUND_EMAIL_DOMAIN_LIMIT_REACHED` | The organization is at its domain cap (one on Free and Developer, disabled domains included) | Run tests in the workspace that has the domain, delete an unused domain, or move to a larger testing plan |
| 400 `INBOUND_INJECT_DOMAIN_DISABLED` | `email receive` on a disabled domain | Re-enable the domain |
| `No matching inbound item arrived before timeout`, exit `11` | No match in time, or `--received-after` was later than the message | Capture the time before the send and pass it |
| 429 on `wait` | Over 40 waits per minute in the organization | Back off for the `Retry-After` seconds |

Every code and limit is in [Limits, quotas, and errors](/testing/limits).

## Next steps

<CardGroup cols={2}>
  <Card title="Email testing" icon="envelope" href="/testing/email/overview">
    Domains, addresses, inject, and real mail.
  </Card>

  <Card title="Wait for an email in a test" icon="hourglass" href="/testing/email/wait">
    One address per test, waits that do not flake.
  </Card>

  <Card title="Read OTP codes and links" icon="key" href="/testing/email/otp-and-links">
    Pull the code or link out of a message.
  </Card>

  <Card title="inboxes commands" icon="inbox" href="/cli/commands/inboxes">
    The CLI for webhook testing inboxes.
  </Card>
</CardGroup>


This documentation is built and hosted on [Mintlify](https://mintlify.com), a developer documentation platform.