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

# Email & SMS

> Inboxes and phone numbers on an identity, outbound send, and the unified inbound message feed.

<Info>Email and SMS hang off an [identity](/docs/api/identities) — the persona is decided by **where** a message arrived, not by who sent it. Deliverability comes from a verified [domain](/docs/api/domains).</Info>

An inbox and a phone number are **identity-level**; the domain behind an inbox is **org-level**. One identity may hold several endpoints, and everything that arrives at any of them lands in the same [message feed](#list-inbound-messages).

## Email inboxes

### The inbox object

<ResponseField name="id" type="string">Unique id (e.g. `eml_01H...`).</ResponseField>
<ResponseField name="object" type="string">Always `email_inbox`.</ResponseField>
<ResponseField name="identity_id" type="string">The persona that owns the address.</ResponseField>
<ResponseField name="domain_id" type="string">The [domain](/docs/api/domains) it is hosted on.</ResponseField>
<ResponseField name="address" type="string">The full address, e.g. `ava-sales@mail.acme.dev`.</ResponseField>
<ResponseField name="localpart" type="string">The part before the `@`.</ResponseField>
<ResponseField name="owning_agent_id" type="string">Inbound routing keys off this: mail to the address wakes exactly this agent.</ResponseField>
<ResponseField name="status" type="string">`active` or `disabled`.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

### Create an inbox

`POST /v1/identities/{id}/emails` — scope `agents:write`

Give **either** an explicit `address` **or** a `domain_id` — with `domain_id` the localpart is derived from the agent's name (lowercased, hyphenated, capped at 32 characters, then suffixed `-2`, `-3`, … against what is taken).

<ParamField body="owning_agent_id" type="string" required>The agent inbound mail wakes.</ParamField>

<ParamField body="address" type="string">
  A full address to claim. The localpart must match `[a-z0-9._-]{2,64}`. Mutually exclusive with `domain_id`.
</ParamField>

<ParamField body="domain_id" type="string">
  Host on this domain and auto-derive the localpart. Mutually exclusive with `address`.
</ParamField>

<CodeGroup>
  ```bash cURL theme={"system"}
  curl -fsSL https://api.vetta.sh/v1/identities/idn_01H.../emails \
    -H "authorization: Bearer sk_live_..." \
    -H "content-type: application/json" \
    -d '{ "domain_id": "dom_01H9DM...", "owning_agent_id": "agt_01H..." }'
  ```
</CodeGroup>

<ResponseExample>
  ```json Response theme={"system"}
  {
    "id": "eml_01H9EM...",
    "object": "email_inbox",
    "identity_id": "idn_01H...",
    "domain_id": "dom_01H9DM...",
    "address": "ava-sales@mail.acme.dev",
    "localpart": "ava-sales",
    "owning_agent_id": "agt_01H...",
    "status": "active",
    "created_at": "2026-08-20T17:05:00Z"
  }
  ```
</ResponseExample>

**Errors** — `validation_failed` (400) for a malformed localpart, or for supplying both `address` and `domain_id`; `not_found` (404) for an unknown identity, domain, or agent; `conflict` (409) when the address is already claimed.

### List inboxes

`GET /v1/identities/{id}/emails` — scope `agents:read`. Returns a [paginated list](/docs/api/pagination) of inbox objects; accepts `limit`.

### Delete an inbox

`DELETE /v1/identities/{id}/emails/{eid}` — scope `agents:write`. Mail to the address stops being accepted.

### Send email

`POST /v1/identities/{id}/emails/{eid}/send` — scope `sessions:write`

Gated on the domain's `dns_status`. A send attempted before SPF/DKIM are provisioned is refused with a **retryable** `job_not_ready` — verify the [domain](/docs/api/domains#verify-a-domain) and try again.

<ParamField body="to" type="string[]" required>Recipient addresses.</ParamField>
<ParamField body="subject" type="string" required>Subject line.</ParamField>
<ParamField body="text" type="string" required>Plain-text body.</ParamField>

Answers `202` with a receipt, not a message object — the provider's own message id is deliberately not surfaced.

<ResponseExample>
  ```json Response theme={"system"}
  {
    "object": "email_send",
    "accepted": true,
    "from": "ava-sales@mail.acme.dev",
    "to": ["buyer@example.com"],
    "sent_at": "2026-08-20T17:06:00Z"
  }
  ```
</ResponseExample>

**Errors** — `job_not_ready` (409, retryable) while the domain is unverified; `validation_failed` (400); `insufficient_credits` (402).

## Phone numbers

### The phone number object

<ResponseField name="id" type="string">Unique id (e.g. `pho_01H...`).</ResponseField>
<ResponseField name="object" type="string">Always `phone_number`.</ResponseField>
<ResponseField name="identity_id" type="string">The persona that owns the number.</ResponseField>
<ResponseField name="e164" type="string">The number in E.164 form, e.g. `+14155550123`.</ResponseField>
<ResponseField name="country" type="string">ISO country code.</ResponseField>

<ResponseField name="capabilities" type="object">
  `sms_inbound` is live the moment the number is bought. `sms_outbound` is true **iff** `campaign_status` is `approved`. `voice` is always `false` — voice is not supported, and the field exists so its absence is explicit.
</ResponseField>

<ResponseField name="tier" type="string">`sole_prop` or `standard` — the carrier registration tier.</ResponseField>
<ResponseField name="brand_status" type="string">`none`, `pending_verification`, `verified`, or `rejected`.</ResponseField>
<ResponseField name="campaign_status" type="string">`none`, `pending`, `approved`, or `rejected`. Gates outbound.</ResponseField>
<ResponseField name="provisioned_at" type="string | null">When the carrier released the number.</ResponseField>
<ResponseField name="created_at" type="string">Creation timestamp.</ResponseField>

### Provision a number

`POST /v1/identities/{id}/phones` — scope `billing:write`

A number is a recurring charge, which is why this needs `billing:write` and not `agents:write`.

<ParamField body="tier" type="string" required>`sole_prop` or `standard`.</ParamField>
<ParamField body="legal_name" type="string" required>The registering entity, as the carrier will see it.</ParamField>
<ParamField body="country" type="string">ISO country code. Defaults to `US`.</ParamField>
<ParamField body="tax_id" type="string">`standard` tier only. Write-through: handed to the carrier and never stored.</ParamField>
<ParamField body="contact_phone" type="string">`sole_prop` only — the handset that receives the verification passcode. Never stored.</ParamField>
<ParamField body="e164" type="string">Request a specific number instead of the next available one.</ParamField>

Answers **`202`, not `201`**: the number exists and can receive immediately, but outbound waits on a carrier decision.

**Errors** — `validation_failed` (400) when a tier-specific field is missing or supplied for the wrong tier; `insufficient_credits` (402); `insufficient_scope` (403) without `billing:write`.

### List numbers

`GET /v1/identities/{id}/phones` — scope `agents:read`. Returns a [paginated list](/docs/api/pagination); accepts `limit`.

### Release a number

`DELETE /v1/identities/{id}/phones/{pid}` — scope `billing:write`. Stops the recurring charge. The number returns to the carrier and cannot be reclaimed.

### Send SMS

`POST /v1/identities/{id}/phones/{pid}/send` — scope `sessions:write`

<ParamField body="to" type="string" required>Recipient in E.164 form.</ParamField>
<ParamField body="text" type="string" required>Message body.</ParamField>

<Warning>
  Refused with `compliance_pending` until `campaign_status` is `approved`. That is a **carrier rule**, not a platform one — no scope or plan bypasses it, and inbound keeps working the whole time.
</Warning>

<ResponseExample>
  ```json Response theme={"system"}
  {
    "object": "sms_send",
    "accepted": true,
    "from": "+14155550123",
    "to": "+14155550188",
    "sent_at": "2026-08-20T17:07:00Z"
  }
  ```
</ResponseExample>

## List inbound messages

`GET /v1/identities/{id}/messages` — scope `agents:read`

Everything that arrived for this persona, both channels unless one is named.

<ParamField query="channel" type="string">`email` or `sms`. Omit for both.</ParamField>
<ParamField query="limit" type="integer">Page size. See [Pagination](/docs/api/pagination).</ParamField>

<ResponseField name="id" type="string">Unique id (e.g. `inb_01H...`).</ResponseField>
<ResponseField name="channel" type="string">`email` or `sms`.</ResponseField>
<ResponseField name="endpoint_id" type="string">The `eml_`/`pho_` endpoint it arrived at.</ResponseField>
<ResponseField name="from" type="string">Sender address or number.</ResponseField>
<ResponseField name="to" type="string">The endpoint address or number.</ResponseField>
<ResponseField name="subject" type="string | null">Email subject; `null` for SMS.</ResponseField>
<ResponseField name="text" type="string">Plain-text body.</ResponseField>
<ResponseField name="auth" type="object">SPF/DKIM/DMARC results for mail, carrier origin for SMS — kept for the audit of why the message was accepted.</ResponseField>
<ResponseField name="session_id" type="string | null">The session this message woke.</ResponseField>
<ResponseField name="received_at" type="string">Arrival timestamp.</ResponseField>

Inbound arrives at the platform's own receivers — see [Inbound receivers](/docs/api/webhooks#inbound-receivers) for how a message becomes a message object.

<Card title="Identities" icon="arrow-left" href="/docs/api/identities">
  The persona these endpoints hang off.
</Card>
