# Inbound API

Receive email: a webhook when it arrives, and an API for the body and attachments.

**Project key.** Acts in the project the key belongs to. An all-access key works too — name the project with an `X-Project-Id` header. See [Authentication](https://emailbump.com/docs/api-keys).

## Endpoints

- `GET /v1/inbound`
- `GET /v1/inbound/{id}`
- `GET /v1/inbound/{id}/attachments/{attachment_id}`
- `GET /v1/inbound/address`

## Where to receive

Every project can receive mail immediately, with nothing to configure — any address on its own subdomain works:

```
anything@<project-slug>.inbound.emailbump.com
```

It's a catch-all, so `support@`, `replies@`, and `invoice-4821@` all arrive — filter on `received_for` to route them.

```bash
curl https://emailbump.com/api/v1/inbound/address \
  -H "Authorization: Bearer ebk_your_key"
```

```json
{
  "domain": "acme-newsletter-3f9a2b.inbound.emailbump.com",
  "example": "hello@acme-newsletter-3f9a2b.inbound.emailbump.com",
  "catch_all": true,
  "custom_domain_record": {
    "type": "MX",
    "value": "inbound-smtp.us-east-1.amazonaws.com",
    "priority": 10
  }
}
```

### Receiving on your own domain

Add that MX record to the domain you want to receive on, and mail to any address there arrives in the same place. The domain has to be one you've [verified](https://emailbump.com/docs/domains) for sending, which is what proves it's yours.

**An MX record redirects all mail for that name.** If the domain already receives email — Google Workspace, Outlook, anything — point a **subdomain** at us instead, e.g. `inbox.acme.com`. Adding this to a root domain that already has mail will redirect your company's email here.

## The email.received webhook

Subscribe to `email.received` under **Developers → Webhooks** and we POST as each message arrives. Signed and retried like every other webhook. It carries the metadata and the attachment list — never the bodies or the bytes:

```json
{
  "id": "2c28f706-5f9b-489a-ada6-0bab22460dbe",
  "type": "email.received",
  "created": "2026-08-01T22:34:28Z",
  "data": {
    "email": {
      "id": "3de5e8ff-d56a-4120-b3b7-15d6969b82ba",
      "from": "jamie@example.com",
      "from_name": "Jamie",
      "to": ["support@acme-newsletter-3f9a2b.inbound.emailbump.com"],
      "cc": ["billing@acme-newsletter-3f9a2b.inbound.emailbump.com"],
      "received_for": "support@acme-newsletter-3f9a2b.inbound.emailbump.com",
      "subject": "Photo of the damage + invoice",
      "message_id": "CAF\u2026@mail.gmail.com",
      "in_reply_to": null,
      "attachment_count": 2,
      "attachments": [
        {
          "id": "877ba06a-b03e-4dd7-b6dd-2475baf3957d",
          "filename": "photo.png",
          "content_type": "image/png",
          "content_disposition": "inline",
          "content_id": "photo001",
          "size_bytes": 20481,
          "inline": true
        },
        {
          "id": "b88c647b-a199-4c76-ab03-2b01b3ffba5f",
          "filename": "invoice.pdf",
          "content_type": "application/pdf",
          "content_disposition": "attachment",
          "content_id": null,
          "size_bytes": 38210,
          "inline": false
        }
      ],
      "size_bytes": 61204,
      "spam": "PASS", "spf": "PASS", "dkim": "PASS", "dmarc": "PASS",
      "received_at": "2026-08-01T22:34:26Z"
    }
  }
}
```

`to` and `cc` are the addresses the sender wrote; `received_for` is the address that actually routed the message to your project — they differ when a message was forwarded or bcc'd, and `received_for` is the one to branch on.

The attachment list comes with the webhook so a handler can decide whether it cares before fetching anything. Bodies and bytes never do. The message is stored *before* the webhook fires, so a handler that was down loses nothing.

## Read a message

`GET /v1/inbound` lists what you've received, newest first, with `?limit=`, `?offset=`, and `?search=` over sender, subject, and recipient. `GET /v1/inbound/{id}` adds the body and the attachment list.

```bash
curl https://emailbump.com/api/v1/inbound/3de5e8ff-d56a-4120-b3b7-15d6969b82ba \
  -H "Authorization: Bearer ebk_your_key"
```

```json
{
  "id": "3de5e8ff-…",
  "from": "jamie@example.com",
  "to": ["support@acme-newsletter-3f9a2b.inbound.emailbump.com"],
  "cc": [],
  "received_for": "support@acme-newsletter-3f9a2b.inbound.emailbump.com",
  "subject": "Re: your invoice",
  "text": "Thanks — see the file attached.",
  "html": "<p>Thanks — see the file attached.</p>",
  "body_truncated": false,
  "attachments": [
    {
      "id": "91ca5a56-…",
      "filename": "people.csv",
      "content_type": "text/csv",
      "size_bytes": 22,
      "inline": false,
      "content_id": null
    }
  ],
  "spam": "PASS", "spf": "PASS", "dkim": "PASS", "dmarc": "PASS",
  "received_at": "2026-08-01T22:34:26Z"
}
```

A very large body is stored up to a limit and marked `body_truncated`; the complete message is always kept intact behind the scenes.

`message_id` and `in_reply_to` are the sender's own headers, so a reply to something you sent can be matched back to it.

## Attachments

```bash
curl https://emailbump.com/api/v1/inbound/MESSAGE_ID/attachments/ATTACHMENT_ID \
  -H "Authorization: Bearer ebk_your_key" \
  -o people.csv
```

### Images embedded in the body

An image the sender put *inside* the message arrives as an attachment with `content_disposition: "inline"` and a `content_id`, and the HTML refers to it as `<img src="cid:photo001">`. To display the message, swap each `cid:` reference for that attachment's bytes — download it by id and inline it as a data URI, or host it yourself:

```js
let html = message.html
for (const a of message.attachments.filter((a) => a.inline && a.content_id)) {
  const res = await fetch(
    `https://emailbump.com/api/v1/inbound/${message.id}/attachments/${a.id}`,
    { headers: { Authorization: `Bearer ${process.env.EMAILBUMP_API_KEY}` } },
  )
  const b64 = Buffer.from(await res.arrayBuffer()).toString("base64")
  html = html.replaceAll(`cid:${a.content_id}`, `data:${a.content_type};base64,${b64}`)
}
```

Every download is served as an attachment with `X-Content-Type-Options: nosniff` — a file a stranger sent you is never rendered in a browser on our domain. Treat it as untrusted in your own application too.

## Spam and authentication

Every message is scanned before you see it, and the verdicts come with it: `spam`, `spf`, `dkim`, and `dmarc`, each `PASS`, `FAIL`, `GRAY`, or `PROCESSING_FAILED`.

- **Mail carrying a virus never arrives.** It's dropped on receipt — no row, no webhook.
- **Spam does arrive, flagged.** You decide what to do with `spam: "FAIL"`.
- **Authentication is advisory.** A failed SPF or DMARC often means a forwarded message, not a forged one.
