Project API

Inbound API

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

View as Markdown ↗

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. How keys and scopes work.

Endpoints

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

Email people send you, parsed into something your application can act on: a webhook the moment it arrives, and this API for the body and the files.

Where to receive

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

Your receiving address
anything@<project-slug>.inbound.emailbump.com

It's a catch-all, so support@, replies@, and invoice-4821@ all arrive — filter on received_for, the address that routed it here, to tell them apart. GET /v1/inbound/address returns your project's domain and an example address.

cURL
curl https://emailbump.com/api/v1/inbound/address \
  -H "Authorization: Bearer ebk_your_key"
200 response
{
  "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 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, which is not a mistake you want to make on a Friday.

The email.received webhook

Subscribe to email.received under Developers → Webhooks and we POST as each message arrives. Like every webhook we send, it's signed and retried — and it carries metadata only, so the delivery stays small:

POST body
{
  "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": [],
      "received_for": "support@acme-newsletter-3f9a2b.inbound.emailbump.com",
      "subject": "Photo of the damage",
      "message_id": "CAF…@mail.gmail.com",
      "in_reply_to": null,
      "attachment_count": 1,
      "attachments": [
        {
          "id": "877ba06a-…",
          "filename": "photo.png",
          "content_type": "image/png",
          "content_disposition": "inline",
          "content_id": "photo001",
          "size_bytes": 20481,
          "inline": true
        }
      ],
      "size_bytes": 38210,
      "spam": "PASS", "spf": "PASS", "dkim": "PASS", "dmarc": "PASS",
      "received_at": "2026-08-01T22:34:26Z"
    }
  }
}

Fetch the body with the id. The message is stored before the webhook fires, so a handler that was down loses nothing — the mail is here when you come back for it.

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.

cURL
curl https://emailbump.com/api/v1/inbound/3de5e8ff-d56a-4120-b3b7-15d6969b82ba \
  -H "Authorization: Bearer ebk_your_key"
200 response
{
  "id": "3de5e8ff-…",
  "from": "jamie@example.com",
  "to": "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.

Threading

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 — that's how you build a conversation rather than a pile of messages.

Attachments

Download one by id. It comes back as the bytes that were sent, with the original filename and content type:

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

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.

Images inside the message

An image the sender embedded arrives as an attachment with content_disposition: "inline" and a content_id, and the HTML points at it with <img src="cid:photo001">. To display the message, swap each cid: reference for that attachment's bytes:

Rendering a message with embedded images
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}`)
}

The dashboard does exactly this when it shows you a message, which is why an embedded photo appears there rather than a broken image.

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", because one product's junk is another's signal.
  • Authentication is advisory. A failed SPF or DMARC often means a forwarded message, not a forged one — weigh it, don't gate on it alone.