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

# Inbound Email

> Receive email at addresses on your workspace's domain and get it delivered as a webhook.

SenderKit can receive email, not just send it. Every workspace gets a shared
receiving domain — `{slug}.in.senderkit.email` — and you provision addresses on
it from the dashboard or the API. Mail sent to an address is parsed, delivered
to your backend as a `message.received` webhook, and optionally forwarded to a
real inbox.

## How it works

1. Create an inbound address from the dashboard or `POST /v1/inbound/addresses`.
2. Point whatever should reach you — a support alias, a reply-to on an outbound
   email, a webhook endpoint you hand to a third party — at that address.
3. When mail arrives, SenderKit parses it and fires a `message.received`
   [webhook](/webhooks) to every endpoint subscribed to that event (or to one
   specific endpoint, if the address is bound to it).
4. Optionally, the address also forwards a copy of the mail to a real inbox.

## The shared receiving domain

The first time you create an inbound address, SenderKit provisions
`{slug}.in.senderkit.email` for your workspace automatically — there's no DNS
to configure, since it's a domain SenderKit already owns and receives mail on.
To receive on your own domain instead, see [Custom domains](#custom-domains)
below.

## Custom domains

Instead of (or alongside) the shared domain, you can claim your own domain to
receive mail on — e.g. `inbound.acme.com`.

**Dashboard:** open **Inbound**, and use the domain picker in **New address**
to add one, or manage existing ones from the domains section at the top of the
page.

**API:**

```bash theme={null}
curl https://api.senderkit.com/v1/inbound/domains \
  -H "Authorization: Bearer $SENDERKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "domain": "inbound.acme.com" }'
```

The response carries the DNS `records` to publish:

* **An MX record** on the domain, pointing at SenderKit's inbound mail host —
  required for every custom domain.
* **A DKIM TXT record**, proving ownership — only when this domain doesn't
  already have a verified [custom sending domain](/concepts/channels-and-providers#custom-sending-domains)
  on the same workspace. If it does, that identity is reused and the MX is all
  that's outstanding.

Nothing is received until the records are live and a background sweep flips
the domain's `status` from `pending` to `verified` — the same polling and
72-hour expiry window as [custom sending domains](/concepts/channels-and-providers#custom-sending-domains).
A domain that doesn't verify in time flips to `failed` and can be re-claimed.

**Conflict protection:** claiming a domain that already has live MX records
pointing somewhere else — a domain actively receiving mail via Google
Workspace, for example — is rejected with `409` and code `existing_mx`,
naming the current mail host(s), instead of silently redirecting that
domain's mail to SenderKit. Confirm you want to proceed, then retry the same
request with `"acknowledgeExistingMx": true`.

A domain already claimed by another workspace (or already claimed as a
[sending domain](/concepts/channels-and-providers#custom-sending-domains) by a
*different* workspace) is rejected as `domain_claimed`. Deleting a custom
domain stops its addresses from receiving mail immediately; the shared
domain can't be deleted. `GET /v1/inbound/domains` lists every domain on the
workspace, including the shared one.

## Creating an address

**Dashboard:** open **Inbound** in the sidebar and click **New address**.

**API:**

```bash theme={null}
curl https://api.senderkit.com/v1/inbound/addresses \
  -H "Authorization: Bearer $SENDERKIT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "localPart": "support",
    "description": "Customer support intake",
    "forwardTo": "team@acme.com"
  }'
```

* Omit `localPart` to get an unguessable generated one (`rcv-xxxxxxxxxx`) —
  useful when you're embedding the address somewhere you don't want a
  guessable alias, like a reply-to header on an outbound email. Otherwise
  it's 1-64 characters of `a-z`, `0-9`, `.`, `_`, `-`, must start and end
  alphanumeric, and is lowercased automatically.
* Pass `localPart: "*"` for a **catch-all** address that receives mail for
  every local part on its domain that no exact address already claims — an
  exact-match address always takes priority over the catch-all, and the
  catch-all sentinel bypasses the character rules and the reserved-name list.
  A catch-all counts as one address against the plan cap like any other.
* `domainId` mints the address on a verified [custom domain](#custom-domains)
  (its `id` from `GET /v1/inbound/domains`) instead of the shared domain.
* `livemode` defaults to `true`. A test-mode address (`false`) still receives
  real mail and fans out to test-mode webhook endpoints, but any configured
  `forwardTo` is recorded as a test send rather than actually delivered.
  Every address — test or live — counts the same toward the plan's inbound
  address cap below; only *messages* received on a test-mode address skip the
  message quota (see [Billing](#billing)).
* `forwardTo` is optional and forwards a copy of every received message to a
  real inbox. It can't point at another inbound address — that's rejected as a
  mail loop.
* `webhookEndpointId` optionally binds the address to one specific webhook
  endpoint — that endpoint receives `message.received` even if it isn't
  itself subscribed to the event, but it must be active and in the same mode
  (`livemode`) as the address. Left unset, `message.received` events fan out
  to every active endpoint subscribed to that event in the address's mode.
* A few local parts (`postmaster`, `abuse`, `unsubscribe`) are reserved and
  can't be claimed.

Creating and deleting addresses and domains, and reading received mail, all
require an API key with the `inbound` [scope](/authentication#scopes).

## Reading received mail

`GET /v1/inbound/messages` lists received-message summaries newest first, with
`limit` / `before` pagination and an optional `address` filter.
`GET /v1/inbound/messages/{id}` returns the full message — parsed `text` and
`html` bodies, `from`/`to`/`cc`, raw `headers`, a `strippedReply` (the
plain-text reply with quoted history and signature stripped for reply-parsing
use cases), and scanning `verdicts` (spam, virus, SPF, DKIM), as reported.

Each message also has a `status`:

| Status           | Meaning                                                                             |
| ---------------- | ----------------------------------------------------------------------------------- |
| `received`       | Matched an active address and delivered as a webhook (and forwarded, if configured) |
| `dropped`        | No address on the domain matched the recipient                                      |
| `quota_exceeded` | Matched an address, but the workspace's monthly message quota was already exhausted |

Only `received` messages trigger the `message.received` webhook and any
forward.

Combined `text` + `html` body storage is capped at 256 KB; longer messages are
still delivered in full to your webhook endpoint's payload size limits with
`truncated: true` set.

### Attachments and the raw message

Attachments aren't stored separately from the original message — fetch one at
a time from `GET /v1/inbound/messages/{id}/attachments/{index}` (the `index`
matches `attachments[].index` on the message), or fetch the full original
`message/rfc822` source from `GET /v1/inbound/messages/{id}/raw`. Both require
the `inbound` scope and are only available for 30 days after receipt —
afterward they return `410` with code `raw_expired`.

## Billing

A received live message counts against your plan's monthly message quota,
the same pool outbound sends draw from — one received email is one message,
just like one outbound send. Test-mode addresses (`livemode: false`) don't
consume quota. See [Messages → Retention](/concepts/messages#retention) for
how the quota itself is enforced.

Inbound *addresses* are a separate, smaller plan-limited resource:

| Plan    | Inbound addresses |
| ------- | ----------------- |
| Free    | 1                 |
| Starter | 10                |
| Pro     | 50                |

Creating an address past your plan's cap returns `403` with code
`plan_limit`.

## Deleting an address

Deleting an address doesn't delete its message history — past received
messages stay queryable. Mail sent to a deleted address afterward is simply
dropped, the same as mail to any other unmatched recipient.

<CardGroup cols={2}>
  <Card title="Webhooks" icon="webhook" href="/webhooks#events">
    Verify signatures and handle the `message.received` payload.
  </Card>

  <Card title="Authentication" icon="key" href="/authentication#scopes">
    The `inbound` API key scope.
  </Card>
</CardGroup>
