Skip to content

Receive email into SQS ​

A complete inbound-email walkthrough: give an address to a vendor, get a small notice on your SQS queue for every message it receives, and pull the message and its attachments over the API with your own key. The queue carries no mail content. Routing to an HTTPS endpoint instead? It gets the same notice, plus the email's text, HTML and headers. See Receiving over HTTPS.

What you'll build ​

vendor ──▶ invoices@mail.acme.com ──▶ Emithook MX ──▶ SQS  acme-invoices-q   (email.received notice)
                         (SPF/DKIM/DMARC, spam, virus,             │
                          parse, store)                            ▼
                                                    your consumer ──▶ GET /v1/aliases/al_…/messages/evt_…
                                                                   └▶ GET …/attachments/att_…

Why a notice and not the email itself: a message with attachments easily exceeds an SQS message's size limit. The notice is a few KB. Your consumer fetches only what it needs, using its own API key, so it can only read mail that belongs to its org.

You need:

  • An API key (console → Settings → API Keys). write scope to set things up; read is enough for the consumer.
  • An SQS queue that Emithook can write to. Follow Deliver to Amazon SQS steps 1–4 to create the destination and its queue policy. You'll get a dst_… id.
  • For an address on your own domain (invoices@mail.acme.com): an email domain that is verified and receiving, with its dom_… id. Without one, use a platform address instead (invoices.<org-slug>@<platform-domain>, no DNS needed).

1. Create the address with its destination ​

One request creates the address (an alias) and routes it to your queue:

bash
curl -X POST https://api.emithook.com/v1/aliases \
  -H "Authorization: Bearer $EMITHOOK_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "kind": "custom",
    "domainId": "dom_01KX9Y3T6R2M8Q4W7Z1B5N0C9D",
    "localPart": "invoices",
    "destinationIds": ["dst_01KXBRV8HB64MA2H6QM49B2V8F"]
  }'
FieldMeaning
kindcustom (your domain, needs domainId) or platform (shared domain, no domainId).
domainIdThe email domain's id (dom_…), not its name. List them with GET /v1/domains.
localPartThe part before @. * on a custom domain is a catch-all.
destinationIdsDestination ids (dst_…), not URLs.

Optional: allowedSenders (only accept these senders) and maxSizeBytes.

Response 201:

json
{
  "id": "al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7",
  "address": "invoices@mail.acme.com",
  "kind": "custom",
  "localPart": "invoices",
  "domain": "mail.acme.com",
  "destinationIds": ["dst_01KXBRV8HB64MA2H6QM49B2V8F"],
  "createdAt": "2026-10-16T09:00:12.481Z"
}

Keep the id (al_…): it's the alias identifier in every inbox URL.

ErrorWhy
409 conflictThe address already exists in your org.
400 bad_requestInvalid localPart, unknown or unverified domainId, or missing kind.

Check your destination id

destinationIds aren't validated when the alias is created. A typo creates an address whose mail goes nowhere. Copy the id from GET /v1/destinations or the console.

To change routing later: PATCH /v1/aliases/al_… with {"destinationIds": [...]}. The address itself can't be changed.

2. Check whether an address is defined ​

There's no lookup-by-address endpoint. List your aliases and match on address (stored lowercase):

bash
EMAIL="invoices@mail.acme.com"
curl -s "https://api.emithook.com/v1/aliases?limit=100" \
  -H "Authorization: Bearer $EMITHOOK_API_KEY" \
  | jq --arg e "$(echo "$EMAIL" | tr A-Z a-z)" '.data[] | select(.address == $e)'

No output means the address isn't defined in your org. The list is paginated: follow next_cursor if you have more than 100 aliases. Alternatively, just create it and treat 409 as "already exists".

3. What arrives on the queue ​

Send a test email to the address. Within seconds one SQS message arrives. Its MessageBody is the standard delivery envelope, with metadata.source: "email" and an email.received notice as data.body:

json
{
  "version": 2,
  "metadata": {
    "event_id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
    "identifier": "al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7",
    "name": "invoices@mail.acme.com",
    "source": "email",
    "url": null,
    "method": null,
    "path": null,
    "content_type": "application/json",
    "body_encoding": "json",
    "size": 1426,
    "source_ip": null,
    "verification": {
      "status": "unverified",
      "scheme": "none"
    },
    "transformed": false,
    "captured_at": "2026-10-16T09:12:39.107Z",
    "posted_at": "2026-10-16T09:12:41.207Z",
    "expanded_json_paths": []
  },
  "data": {
    "body": {
      "type": "email.received",
      "id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
      "alias_id": "al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7",
      "message_id": "<CAKd9fP3x7Q@mail.gmail.com>",
      "alias": "invoices@mail.acme.com",
      "from": "billing@vendor.com",
      "to": [
        "invoices@mail.acme.com"
      ],
      "cc": [
        "ap-team@acme.com"
      ],
      "subject": "Invoice INV-2291 for October",
      "date": "2026-10-16T09:12:37.000Z",
      "received_at": "2026-10-16T09:12:39.107Z",
      "size_bytes": 271667,
      "auth": "verified",
      "verdicts": {
        "spf": "PASS",
        "dkim": "PASS",
        "dmarc": "PASS",
        "spam": "PASS",
        "virus": "PASS"
      },
      "attachments": [
        {
          "id": "att_01M50GHMJRP15Z4602V9XFVKDA_0",
          "name": "INV-2291.pdf",
          "size": 182340,
          "content_type": "application/pdf",
          "content_id": null,
          "disposition": "attachment",
          "url": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/attachments/att_01M50GHMJRP15Z4602V9XFVKDA_0"
        },
        {
          "id": "att_01M50GHMJRP15Z4602V9XFVKDA_1",
          "name": "Screenshot 2026-10-16 at 12.24.09 AM.png",
          "size": 79848,
          "content_type": "image/png",
          "content_id": "ii_mgb3k9x10",
          "disposition": "inline",
          "url": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/attachments/att_01M50GHMJRP15Z4602V9XFVKDA_1"
        }
      ],
      "api": {
        "message": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA",
        "raw": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/raw"
      }
    },
    "headers": {
      "content-type": "application/json"
    },
    "queries": {}
  }
}

Notice fields ​

FieldMeaning
typeAlways email.received.
idThe message id (evt_…), the same as metadata.event_id. Use it as your idempotency key.
alias_idThe alias id (al_…).
message_idThe email's own RFC Message-ID header. Informational; you don't need it in URLs.
aliasThe address the mail was sent to.
fromSender address (plain, no display name).
to, ccLists of plain addresses. cc is [] when there was none.
subjectSubject line.
dateThe email's own Date: header.
received_atWhen Emithook received it.
size_bytesSize of the whole raw message.
auth, verdictsverdicts gives each check: PASS, FAIL, GRAY or PROCESSING_FAILED. auth summarises SPF/DKIM/DMARC: failed only when one of them is FAIL or PROCESSING_FAILED, otherwise verified. GRAY (no DKIM signature, or no DMARC policy — dmarc=none) is not a failure.
attachments[]id (the download key), name (the exact Unicode filename), size (bytes), content_type, content_id and disposition (below), and a ready-made download url.
api.messageURL for the full parsed message (text, HTML, all headers).
api.rawURL for the original .eml.

Inline images ​

A picture pasted into the body (a screenshot, a signature logo) is a file with disposition: "inline" and a content_id. The HTML refers to it as src="cid:<content_id>":

json
{ "id": "att_01M50GHMJRP15Z4602V9XFVKDA_1", "name": "Screenshot 2026-10-16 at 12.24.09 AM.png", "content_id": "ii_mgb3k9x10", "disposition": "inline", "…": "…" }
html
<img src="cid:ii_mgb3k9x10" alt="Screenshot">

To render the body, download each inline file from its url and replace cid:<content_id> in the HTML with wherever you stored it. A regular attachment has disposition: "attachment" and usually content_id: null. Both are null for emails received before 16 Oct 2026, and those older emails have their inline images embedded in the HTML as data: URIs instead.

For email, metadata.verification is always unverified / none: it describes webhook signatures. Read the sender checks from data.body.auth and data.body.verdicts instead.

Receiving over HTTPS ​

Route the alias to an HTTPS destination and each email arrives as a POST of the same envelope, with the same email.received notice as data.body. HTTPS has no message size limit, so the notice also carries the email's content as three extra keys. Nothing from the queue notice is removed or renamed, so one DTO and one handler serve both:

Extra key (HTTPS only)Meaning
textThe plain-text body, or null.
htmlThe HTML body, or null.
headersEvery header, by lowercase name, as its original text (for example "cc": "\"AP Team\" <ap-team@acme.com>").

The full request, for the same email as the queue sample above:

http
POST /emithook/email HTTP/1.1
Host: api.acme.com
Content-Type: application/json
webhook-id: evt_01M50GHMJRP15Z4602V9XFVKDA
webhook-timestamp: 1792141961
webhook-signature: v1,EAPUv78RVn6zAmgt60WUIW4mhPkQZjLhonQqihqJvC0=

{
  "version": 2,
  "metadata": {
    "event_id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
    "identifier": "al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7",
    "name": "invoices@mail.acme.com",
    "source": "email",
    "url": null,
    "method": null,
    "path": null,
    "content_type": "application/json",
    "body_encoding": "json",
    "size": 2359,
    "source_ip": null,
    "verification": {
      "status": "unverified",
      "scheme": "none"
    },
    "transformed": false,
    "captured_at": "2026-10-16T09:12:39.107Z",
    "posted_at": "2026-10-16T09:12:41.207Z",
    "expanded_json_paths": []
  },
  "data": {
    "body": {
      "type": "email.received",
      "id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
      "alias_id": "al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7",
      "message_id": "<CAKd9fP3x7Q@mail.gmail.com>",
      "alias": "invoices@mail.acme.com",
      "from": "billing@vendor.com",
      "to": [
        "invoices@mail.acme.com"
      ],
      "cc": [
        "ap-team@acme.com"
      ],
      "subject": "Invoice INV-2291 for October",
      "date": "2026-10-16T09:12:37.000Z",
      "received_at": "2026-10-16T09:12:39.107Z",
      "size_bytes": 271667,
      "auth": "verified",
      "verdicts": {
        "spf": "PASS",
        "dkim": "PASS",
        "dmarc": "PASS",
        "spam": "PASS",
        "virus": "PASS"
      },
      "attachments": [
        {
          "id": "att_01M50GHMJRP15Z4602V9XFVKDA_0",
          "name": "INV-2291.pdf",
          "size": 182340,
          "content_type": "application/pdf",
          "content_id": null,
          "disposition": "attachment",
          "url": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/attachments/att_01M50GHMJRP15Z4602V9XFVKDA_0"
        },
        {
          "id": "att_01M50GHMJRP15Z4602V9XFVKDA_1",
          "name": "Screenshot 2026-10-16 at 12.24.09 AM.png",
          "size": 79848,
          "content_type": "image/png",
          "content_id": "ii_mgb3k9x10",
          "disposition": "inline",
          "url": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/attachments/att_01M50GHMJRP15Z4602V9XFVKDA_1"
        }
      ],
      "api": {
        "message": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA",
        "raw": "https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA/raw"
      },
      "text": "Hi team,\n\nPlease find attached invoice INV-2291 for October. The portal error is below:\n\n[image: Screenshot]\n\nThanks,\nBilling",
      "html": "<p>Hi team,</p><p>Please find attached invoice INV-2291 for October. The portal error is below:</p><p><img src=\"cid:ii_mgb3k9x10\" alt=\"Screenshot\"></p><p>Thanks,<br>Billing</p>",
      "headers": {
        "received": "from mail-yx1-f52.google.com (mail-yx1-f52.google.com [74.125.224.52]) by inbound-smtp.ap-south-1.amazonaws.com with SMTP id 4kq2v9; Fri, 16 Oct 2026 09:12:38 +0000\nby mail-yx1-f52.google.com with SMTP id 00721157ae682; Fri, 16 Oct 2026 02:12:37 -0700",
        "from": "Billing <billing@vendor.com>",
        "to": "invoices@mail.acme.com",
        "cc": "\"AP Team\" <ap-team@acme.com>",
        "subject": "Invoice INV-2291 for October",
        "date": "Fri, 16 Oct 2026 09:12:37 +0000",
        "message-id": "<CAKd9fP3x7Q@mail.gmail.com>",
        "mime-version": "1.0",
        "content-type": "multipart/mixed; boundary=\"000000000000b1c2d3e4f5\""
      }
    },
    "headers": {
      "content-type": "application/json"
    },
    "queries": {}
  }
}

Compared with the queue message, metadata and every data.body key up to api are identical. text, html and headers are the only additions. (metadata.size is the size of data.body, so it's larger here.)

The webhook-* headers are sent when the destination has a signing secret; the signature above was computed with an example secret, so verify against your destination's own whsec_….

  • Attachments and the raw .eml are never pushed. Download them from the urls exactly as a queue consumer does.
  • The request is signed with Standard Webhooks headers (webhook-id = the email's evt_… id, webhook-timestamp, webhook-signature) when the destination has a signing secret. Verify against the raw request bytes. See the delivery envelope.
  • Reply 2xx quickly, then process in the background. Anything else (or a timeout) is retried with backoff, so make the handler idempotent on data.body.id.

A NestJS controller that queues the work:

ts
@Post()
@HttpCode(HttpStatus.OK)
async receive(@Body() body: EmithookEnvelopeDto<EmithookEmailBodyInterface>) {
  if (body?.version === 2 && body.metadata?.source === 'email' && body.data?.body?.type === 'email.received') {
    await this.handleEmithookEmailJob.dispatch(body) // same job the SQS consumer runs
  }
  return { success: true }
}

Type the HTTPS-only keys as optional (text?: string | null; html?: string | null; headers?: Record<string, string>) so the same interface fits the queue notice.

4. Fetch the message and attachments ​

Call the URLs from the notice with your API key. A read-scoped key works, and it only sees your org's mail.

bash
MSG=https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/messages/evt_01M50GHMJRP15Z4602V9XFVKDA

# The parsed message
curl -s "$MSG" -H "Authorization: Bearer $EMITHOOK_API_KEY"

# One attachment (raw bytes)
curl -s "$MSG/attachments/att_01M50GHMJRP15Z4602V9XFVKDA_0" \
  -H "Authorization: Bearer $EMITHOOK_API_KEY" -o INV-2291.pdf

# The original .eml
curl -s "$MSG/raw" -H "Authorization: Bearer $EMITHOOK_API_KEY" -o message.eml

GET $MSG response:

json
{
  "id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
  "message_id": "<CAKd9fP3x7Q@mail.gmail.com>",
  "event_id": "evt_01M50GHMJRP15Z4602V9XFVKDA",
  "alias": "invoices@mail.acme.com",
  "from": "billing@vendor.com",
  "to": ["invoices@mail.acme.com"],
  "cc": ["ap-team@acme.com"],
  "subject": "Invoice INV-2291 for October",
  "received_at": "2026-10-16T09:12:39.107Z",
  "date": "2026-10-16T09:12:37.000Z",
  "status": "accepted",
  "auth": "verified",
  "text": "Hi team,\nPlease find attached invoice INV-2291 …",
  "html": "<p>Hi team,</p><p>Please find attached invoice INV-2291 …</p>",
  "size_bytes": 271667,
  "headers": {
    "from": "Billing <billing@vendor.com>",
    "to": "invoices@mail.acme.com",
    "cc": "\"AP Team\" <ap-team@acme.com>",
    "subject": "Invoice INV-2291 for October",
    "message-id": "<CAKd9fP3x7Q@mail.gmail.com>",
    "date": "Fri, 16 Oct 2026 09:12:37 +0000",
    "content-type": "multipart/mixed; boundary=\"000000000000b1c2d3\""
  },
  "verdicts": { "spf": "PASS", "dkim": "PASS", "dmarc": "PASS", "spam": "PASS", "virus": "PASS" },
  "attachments": [
    { "id": "att_01M50GHMJRP15Z4602V9XFVKDA_0", "name": "INV-2291.pdf", "size": 182340, "content_type": "application/pdf",
      "content_id": null, "disposition": "attachment" },
    { "id": "att_01M50GHMJRP15Z4602V9XFVKDA_1", "name": "Screenshot 2026-10-16 at 12.24.09 AM.png", "size": 79848, "content_type": "image/png",
      "content_id": "ii_mgb3k9x10", "disposition": "inline" }
  ]
}
  • headers holds each header's original text, so Cc keeps its display names. Use the to and cc lists for plain addresses; don't split the header strings yourself. A header that appears more than once (each Received hop) is one string with one occurrence per line, in message order.
  • Attachment downloads return the raw bytes with the attachment's content-type. Take the filename from name on the message. Content-Disposition carries an ASCII fallback (filename="…") plus the exact name as RFC 5987 filename*=UTF-8''…, so names with non-ASCII characters (like the narrow space in macOS screenshot names) download fine.
StatusWhy
401Missing or invalid API key.
404Not found for this key: wrong id, another org's alias, an attachment id from a different message, or the stored bytes have expired.
400Malformed attachment id.

5. A consumer ​

Node.js ​

With the AWS SDK v3 and Node 18+ fetch:

ts
import { SQSClient, ReceiveMessageCommand, DeleteMessageCommand } from '@aws-sdk/client-sqs'
import { writeFile } from 'node:fs/promises'

const sqs = new SQSClient({ region: 'ap-south-1' })
const QueueUrl = process.env.QUEUE_URL!
const auth = { Authorization: `Bearer ${process.env.EMITHOOK_API_KEY}` }

interface EmailAttachment {
  id: string; name: string | null; size: number; content_type: string
  content_id: string | null; disposition: 'inline' | 'attachment' | null; url?: string
}
interface EmailReceived {
  type: 'email.received'
  id: string; alias_id: string | null; message_id: string | null; alias: string | null
  from: string | null; to: string[]; cc: string[]; subject: string | null
  date: string | null; received_at: string | null; size_bytes: number | null
  auth: 'verified' | 'failed' | null
  verdicts: Record<'spf' | 'dkim' | 'dmarc' | 'spam' | 'virus', string> | null
  attachments: EmailAttachment[]
  api: { message: string; raw: string } | null
}

async function handle(n: EmailReceived): Promise<void> {
  if (await seenBefore(n.id)) return // at-least-once delivery: dedupe on the message id

  // Full message only when you need the body or headers
  const msg = await (await fetch(n.api!.message, { headers: auth })).json()

  for (const a of n.attachments) {
    const res = await fetch(a.url!, { headers: auth })
    if (!res.ok) throw new Error(`attachment ${a.id}: HTTP ${res.status}`)
    await writeFile(`./inbox/${a.id}-${a.name ?? 'attachment'}`, Buffer.from(await res.arrayBuffer()))
  }
  await saveEmail(n, msg)
  await markSeen(n.id)
}

while (true) {
  const { Messages = [] } = await sqs.send(
    new ReceiveMessageCommand({ QueueUrl, MaxNumberOfMessages: 10, WaitTimeSeconds: 20 }),
  )
  for (const m of Messages) {
    const env = JSON.parse(m.Body!)
    if (env.version !== 2) throw new Error(`unsupported envelope version ${env.version}`)
    if (env.metadata.source === 'email' && env.data.body?.type === 'email.received') {
      await handle(env.data.body as EmailReceived)
    }
    // delete only after it was handled; a throw leaves it on the queue for a retry
    await sqs.send(new DeleteMessageCommand({ QueueUrl, ReceiptHandle: m.ReceiptHandle! }))
  }
}

Python ​

python
import json, os, boto3, requests

sqs = boto3.client("sqs", region_name="ap-south-1")
QUEUE_URL = os.environ["QUEUE_URL"]
AUTH = {"Authorization": f"Bearer {os.environ['EMITHOOK_API_KEY']}"}

while True:
    resp = sqs.receive_message(QueueUrl=QUEUE_URL, MaxNumberOfMessages=10, WaitTimeSeconds=20)
    for m in resp.get("Messages", []):
        env = json.loads(m["Body"])
        n = env["data"]["body"]
        if env["metadata"]["source"] == "email" and n.get("type") == "email.received" and not seen_before(n["id"]):
            msg = requests.get(n["api"]["message"], headers=AUTH, timeout=30).json()
            for a in n["attachments"]:
                r = requests.get(a["url"], headers=AUTH, timeout=60)
                r.raise_for_status()
                save_attachment(n["id"], a["name"], a["content_type"], r.content)
            save_email(n, msg)
            mark_seen(n["id"])
        sqs.delete_message(QueueUrl=QUEUE_URL, ReceiptHandle=m["ReceiptHandle"])

6. Re-deliver ​

One email ​

From the console (Inbound email → message → Re-deliver) or the API:

bash
curl -X POST https://api.emithook.com/v1/events/evt_01M50GHMJRP15Z4602V9XFVKDA/replay \
  -H "Authorization: Bearer $EMITHOOK_API_KEY"
# → 202

Every email an alias received in a time range ​

Pass the alias and a window. Every matching email is sent again to the destinations the alias routes to now, including ones already delivered:

bash
# 1. Preview: how many would be re-sent
curl -X POST https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/replay \
  -H "Authorization: Bearer $EMITHOOK_API_KEY" -H "Content-Type: application/json" \
  -d '{ "since": "2026-10-15T00:00:00Z", "until": "2026-10-15T23:59:59Z", "dry_run": true }'
json
{
  "replay_id": null,
  "alias_id": "al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7",
  "dry_run": true,
  "scanned": 42,
  "replayed": 41,
  "enqueued": 41,
  "skipped": 0,
  "excluded": 1,
  "destinations": ["dst_01KXBRV8HB64MA2H6QM49B2V8F"],
  "next_cursor": null
}
bash
# 2. Run it (same body without dry_run) → 202
curl -X POST https://api.emithook.com/v1/aliases/al_01KXC4Q2M7Z5T8N3R6W9Y1B4D7/replay \
  -H "Authorization: Bearer $EMITHOOK_API_KEY" -H "Content-Type: application/json" \
  -d '{ "since": "2026-10-15T00:00:00Z", "until": "2026-10-15T23:59:59Z" }'
# → { "replay_id": "rpl_ee19dc5fb2be4881851e84f872ec9a93", "replayed": 41, "enqueued": 41, "next_cursor": null, … }
Body fieldMeaning
since, untilRequired. ISO-8601 date-times, inclusive, on when Emithook received the email.
statusOptional: only emails whose delivery is currently delivered, failed or dlq. Use failed or dlq to retry just the ones that didn't land.
dry_runtrue counts without sending (200). Otherwise the emails are queued (202).
cursorContinue a large window: each call handles up to 1,000 emails and returns next_cursor while more remain. Repeat with "cursor": "<next_cursor>" until it is null.
  • The alias can be its id (al_…) or its address, percent-encoded (invoices%40mail.acme.com).
  • excluded counts quarantined emails in the window. They are never sent to destinations.
  • 404 means no such alias in your org. 409 means the alias has no destinations. 400 means a bad date, or since is after until.

Re-sent notices carry the same id and a new metadata.posted_at, and every attempt in the run is stamped with the replay_id. If your consumer dedupes on id, it will drop replays, so make the handler an idempotent upsert if replays must be reprocessed.

7. Troubleshoot ​

  • Delivery status for each message is on the console's Delivery tab: attempts, the latest result and the next retry.
  • Nothing on the queue? Check that the alias's destinationIds contains your SQS destination, and that the destination shows Reachable (SQS guide, step 4).
  • Quarantined mail (a failing verdict, e.g. spam) is stored and visible in the console inbox, but isn't routed to destinations.
  • The old URL forms still work. You can use the alias address and the RFC Message-ID in place of the ids, percent-encoded (/v1/aliases/invoices%40mail.acme.com/messages/%3CCAKd9fP3x7Q%40mail.gmail.com%3E), and an attachment's index (/attachments/0) in place of its id. New code should use the ids from the notice.
  • HTTPS destinations get the same notice plus text, html and headers (Receiving over HTTPS). Attachment bytes and the raw .eml are never pushed; fetch them from the URLs.

Emithook · a Finnoto product