What's in your webhook payload

Every email that arrives at your alias is transformed into structured JSON and delivered to your webhook endpoint. This reference covers every field you can expect, including the enrichment features available on each plan.

The example and the field tables below are generated from the schema the server actually emits, so they cannot drift from reality. Everything else on this page is written by hand.

Full payload example

A Business+ delivery with every optional block present. Lower plans and per-alias settings remove blocks — the tables below say which.

{
  "message": {
    "sender": {
      "email": "alice@example.com",
      "name": "Alice Martin"
    },
    "recipient": {
      "email": "invoices+acme@yourdomain.com",
      "name": null,
      "tag": "acme"
    },
    "subject": "Invoice #2026-0042",
    "content": {
      "text": "Hi team,\n\nPlease find invoice #2026-0042 attached.\n\nBest,\nAlice",
      "html": "<p>Hi team,</p><p>Please find invoice #2026-0042 attached.</p>",
      "markdown": "Hi team,\n\nPlease find invoice #2026-0042 attached.",
      "links": [
        {
          "url": "https://billing.example.com/inv/2026-0042",
          "domain": "billing.example.com",
          "anchorText": "View it online",
          "source": "html"
        }
      ]
    },
    "date": "2026-07-13T10:30:00.000Z",
    "attachments": [
      {
        "filename": "invoice-2026-0042.pdf",
        "contentType": "application/pdf",
        "size": 245680,
        "downloadUrl": "https://app.emailconnect.eu/attachments/9f8e7d6c5b4a3f2e/download",
        "status": "completed",
        "uploadType": "sync",
        "virusScan": {
          "status": "clean",
          "duration": 42
        }
      }
    ]
  },
  "envelope": {
    "messageId": "<abc123@example.com>",
    "xMailer": null,
    "xOriginalTo": "invoices+acme@yourdomain.com",
    "returnPath": "alice@example.com",
    "allRecipients": {
      "to": [
        "invoices+acme@yourdomain.com"
      ],
      "cc": [],
      "bcc": []
    },
    "headers": {
      "from": "alice@example.com",
      "subject": "Invoice #2026-0042"
    },
    "processed": {
      "timestamp": "2026-07-13T10:30:01.000Z",
      "domain": "yourdomain.com",
      "alias": "invoices@yourdomain.com",
      "originalSize": 248320
    }
  },
  "spam": {
    "score": -0.99,
    "engine": "rspamd",
    "symbols": [
      {
        "name": "R_DKIM_ALLOW",
        "weight": -0.2,
        "description": "example.com:s=selector1"
      }
    ],
    "authentication": {
      "dkim": {
        "result": "pass",
        "domain": "example.com"
      },
      "spf": {
        "result": "pass",
        "domain": "example.com"
      },
      "dmarc": {
        "result": "pass",
        "domain": "example.com"
      }
    }
  },
  "classification": {
    "type": "normal",
    "confidence": "definite",
    "signals": []
  },
  "integrity": {
    "contentHash": "a3f2b8c1d4e5f6a7b8c9d0e1f2a3b4c5d6e7f8a9b0c1d2e3f4a5b6c7d8e9f0a1",
    "rawEmailHash": "9f8e7d6c5b4a3f2e1d0c9b8a7f6e5d4c3b2a1f0e9d8c7b6a5f4e3d2c1b0a9f8e"
  },
  "security": {
    "virusScan": {
      "scanned": true,
      "engine": "clamav",
      "attachmentsScanned": 1,
      "attachmentsSkipped": 0,
      "threatsFound": 0
    }
  },
  "domainId": "dom_a1b2c3",
  "aliasId": "ali_d4e5f6"
}

Field reference

Plan is the capability gate. A (note) in the description says when a field may be absent — either because it is optional, or because a per-alias setting removes it before delivery.

message

Field Type Plan Description
message object All plans
message.attachments array All plans Attachments, capped at 20 per email (alias: allowAttachments)
message.attachments[].content string All plans Base64-encoded file content, for inline delivery. Present only for attachments small enough to inline (2 MB on paid plans, 128 KB on Free) (optional, alias: allowAttachments)
message.attachments[].contentId string All plans CID for inline images referenced from the HTML body () (optional, nullable, alias: allowAttachments)
message.attachments[].contentType string All plans MIME type (alias: allowAttachments)
message.attachments[].downloadUrl string All plans Download URL for offloaded files. It points at EmailConnect, not at S3 — we redirect you to a short-lived signed URL at fetch time, so downloads can be counted, expired and kept in-region. It stays valid for as long as your retention window (optional, alias: allowAttachments)
message.attachments[].excluded boolean All plans true when the file was deliberately not delivered (optional, alias: allowAttachments)
message.attachments[].excludeReason string All plans Why the file was excluded. NOT a closed set — it is either a slug (virus-detected, too-large, disabled-in-alias-settings, system-domain-no-storage, inline-not-allowed, excluded, rejected, failed-to-process: ) or a human-readable sentence explaining the limit that was hit. Match on a prefix, not on equality. (optional, alias: allowAttachments)
message.attachments[].filename string All plans Original filename (nullable, alias: allowAttachments)
message.attachments[].manageUrl string All plans Link to the setting that caused this file to be excluded, when one did (optional, alias: allowAttachments)
message.attachments[].metadata object Maker+ Extracted file metadata (Maker+) (optional, alias: allowAttachments)
message.attachments[].metadata.hash string Maker+ SHA-256 of the file (optional, alias: allowAttachments)
message.attachments[].metadata.height number Maker+ Image height in pixels (optional, alias: allowAttachments)
message.attachments[].metadata.pageCount number Maker+ Page count, for PDFs (optional, alias: allowAttachments)
message.attachments[].metadata.width number Maker+ Image width in pixels (optional, alias: allowAttachments)
message.attachments[].size number All plans Size in bytes (alias: allowAttachments)
message.attachments[].status included | pending | completed | failed | rejected | unknown All plans included = inline in this payload · completed = uploaded, downloadUrl ready · pending = still uploading, retry shortly · failed / rejected / unknown = not available (optional, alias: allowAttachments)
message.attachments[].storage inline | s3 | unknown All plans Where the file ended up: inline in this payload, or offloaded to S3 (optional, alias: allowAttachments)
message.attachments[].uploadType sync | async All plans sync = uploaded before delivery, so downloadUrl works immediately · async = queued, so the file may briefly 202 while it finishes (optional, alias: allowAttachments)
message.attachments[].virusScan object Business+ Per-file scan result (Business+). Infected files are excluded from delivery and carry no downloadUrl (optional, alias: allowAttachments)
message.attachments[].virusScan.duration number Business+ Scan duration in milliseconds (optional, alias: allowAttachments)
message.attachments[].virusScan.reason string Business+ Why the file was skipped, when status is skipped (optional, alias: allowAttachments)
message.attachments[].virusScan.status clean | infected | skipped | error Business+ skipped = the file type is not scannable · error = the scan itself failed (alias: allowAttachments)
message.attachments[].virusScan.threat string Business+ Threat name when infected (e.g. Eicar-Test-Signature) (optional, alias: allowAttachments)
message.content object All plans
message.content.html string All plans HTML body (nullable, alias: includeHtml)
message.content.links array All plans URLs extracted from the HTML and plain-text bodies (optional)
message.content.links[].anchorText string Maker+ The clickable text of the link (Maker+ — Free plans receive url and domain only) (optional, nullable)
message.content.links[].domain string All plans The URL's host, for quick filtering
message.content.links[].source html | text Maker+ Which body the link came from (Maker+) (optional)
message.content.links[].url string All plans The extracted URL
message.content.markdown string Maker+ HTML body converted to clean Markdown, server-side. Useful for LLM pipelines and readable logging without an HTML parser (optional, nullable, alias: includeMarkdown (off by default))
message.content.reply object Maker+ The new content of a reply, separated from the quoted history. Requires the includeReplyParsing alias setting, which is off by default (optional, alias: includeReplyParsing (off by default))
message.content.reply.text string Maker+ The reply portion, with the quoted thread stripped (alias: includeReplyParsing (off by default))
message.content.text string All plans Plain text body (nullable, alias: includeText)
message.date string All plans When the email was sent (ISO 8601)
message.recipient object All plans
message.recipient.email string All plans The address exactly as it was written, including any +tag — so you can always reconstruct what the sender typed
message.recipient.name string All plans Recipient display name (nullable)
message.recipient.tag string All plans The +tag from a plus-addressed recipient (inbox+support@ → "support"), or null. A natural routing key: one alias can back many tags. Treat it as untrusted input — senders invent tags freely (optional, nullable)
message.sender object All plans
message.sender.email string All plans Sender email address
message.sender.name string All plans Sender display name, or null if the address carried none (nullable)
message.subject string All plans Email subject line (nullable)

envelope

Field Type Plan Description
envelope object All plans Technical envelope, for debugging and routing. Removed unless the alias enables "include envelope" — when off, only messageId remains (alias: includeEnvelope (reduced to messageId when off))
envelope.allRecipients object All plans Every recipient on the message, not just the alias that matched (alias: includeEnvelope (reduced to messageId when off))
envelope.allRecipients.bcc array All plans All Bcc: addresses visible to us (alias: includeEnvelope (reduced to messageId when off))
envelope.allRecipients.cc array All plans All Cc: addresses (alias: includeEnvelope (reduced to messageId when off))
envelope.allRecipients.to array All plans All To: addresses (alias: includeEnvelope (reduced to messageId when off))
envelope.headers record All plans The raw email headers, lower-cased (alias: includeEnvelope (reduced to messageId when off))
envelope.messageId string All plans RFC 5322 Message-ID. Stable across redeliveries — use it to deduplicate (alias: includeEnvelope (reduced to messageId when off))
envelope.processed object All plans What EmailConnect did with it (alias: includeEnvelope (reduced to messageId when off))
envelope.processed.alias string All plans The alias that matched (alias: includeEnvelope (reduced to messageId when off))
envelope.processed.domain string All plans The domain that received it (alias: includeEnvelope (reduced to messageId when off))
envelope.processed.originalSize number All plans Raw message size in bytes (alias: includeEnvelope (reduced to messageId when off))
envelope.processed.timestamp string All plans When EmailConnect processed the message (ISO 8601) (alias: includeEnvelope (reduced to messageId when off))
envelope.returnPath string All plans Bounce address (the SMTP envelope sender) (nullable, alias: includeEnvelope (reduced to messageId when off))
envelope.xMailer string All plans The sending client, when it identified itself (nullable, alias: includeEnvelope (reduced to messageId when off))
envelope.xOriginalTo string All plans The address the message was originally sent to, before any forwarding (nullable, alias: includeEnvelope (reduced to messageId when off))

spam

Field Type Plan Description
spam union All plans Spam analysis (Maker+). On Free plans this is replaced by a truthy stub — { available: false, message } — so check for spam.score, NOT for the presence of spam (optional)
spam.authentication object Maker+ DKIM, SPF and DMARC results — the strongest signal of who really sent this (optional)
spam.authentication.dkim object Maker+ Proves the message was not altered in transit (nullable)
spam.authentication.dkim.domain string Maker+ The signing domain (optional)
spam.authentication.dkim.result string Maker+ pass, fail, or none
spam.authentication.dmarc object Maker+ The domain owner's policy on SPF/DKIM failure (nullable)
spam.authentication.dmarc.domain string Maker+ The policy domain (optional)
spam.authentication.dmarc.result string Maker+ pass, fail, or none
spam.authentication.spf object Maker+ Proves the sending server was authorised by the domain (nullable)
spam.authentication.spf.domain string Maker+ The envelope domain checked (optional)
spam.authentication.spf.result string Maker+ pass, fail, softfail, or none
spam.available false All plans Spam analysis is not included on your plan
spam.engine rspamd Maker+ The scanner that produced the score (optional)
spam.message string All plans Which plan unlocks it
spam.report string Maker+ The raw rspamd report, for debugging (optional, nullable)
spam.score number Maker+ rspamd composite score. Negative is clean; 5+ is worth suspicion; 7+ is very likely spam. Calibrate to your own traffic rather than trusting those as absolutes. There is deliberately no isSpam verdict — the cut-off is your policy, not ours
spam.symbols array Maker+ The individual rules that fired, and what each contributed — this is how you explain a score (optional)
spam.symbols[].description string Maker+ Rule detail, when rspamd supplied any (nullable)
spam.symbols[].name string Maker+ rspamd rule name, e.g. DMARC_POLICY_ALLOW
spam.symbols[].weight number Maker+ What this rule contributed to the score (can be negative)

routing

Field Type Plan Description
aliasId string All plans The ID of the alias that matched. Catch-all deliveries carry the catch-all alias's id — every domain's *@ catch-all is a real alias. null only for domain-level webhook deliveries with no alias involved (optional, nullable)
domainId string All plans The ID of the domain that received the email — correlate a delivery back to a resource in your account (optional, nullable)

classification

Field Type Plan Description
classification object All plans Routing hints derived from RFC headers — filter out bounces and auto-replies without writing your own heuristics (optional)
classification.confidence definite | likely All plans definite = an unambiguous header said so · likely = inferred from weaker signals
classification.signals array All plans The exact headers behind the verdict, so you can audit it rather than trust it
classification.signals[].header string All plans The RFC header that carried the signal, e.g. auto-submitted
classification.signals[].value string All plans That header's value
classification.type bounce | auto-reply | mailing-list | read-receipt | normal All plans What kind of message this is. Derived from RFC headers, not from content — so it is a fact about the message, not a guess. "normal" means none of the automated shapes matched

integrity

Field Type Plan Description
integrity object All plans Content hashes (Maker+). Detect duplicate deliveries, or build an audit trail. NOTE: this is not the webhook signature — that is an HTTP header (see webhook signing) (optional)
integrity.contentHash string Maker+ SHA-256 of the processed content, as a bare hex digest (no "sha256:" prefix)
integrity.rawEmailHash string Maker+ SHA-256 of the raw message as it arrived, before any processing

security

Field Type Plan Description
security object All plans Virus scan summary (Business+, and requires the virus-scanning setting to be on). Infected attachments are excluded from delivery with excludeReason: "virus-detected" and carry no downloadUrl (optional)
security.virusScan union Business+ Check scanned first: when it is false, this object carries ONLY a reason — no engine, no counts
security.virusScan.attachmentsScanned number Business+ How many files were scanned
security.virusScan.attachmentsSkipped number Business+ How many were skipped as not scannable
security.virusScan.definitionsUpdated string Business+ When the virus definitions were last updated (optional)
security.virusScan.engine string Business+ Scanning engine (currently clamav)
security.virusScan.engineVersion string Business+ Engine version (optional)
security.virusScan.reason scanner-unavailable | disabled Business+ Why it did not run
security.virusScan.scanned true | false Business+ The scan ran
security.virusScan.threatsFound number Business+ How many threats were found

Plus addressing and recipient.tag

Anything after a + in the recipient's address is a tag. It rides along with the email and arrives in your payload as message.recipient.tag, so one alias and one endpoint can serve many purposes.

Mail sent to inbox+support@yourdomain.com is delivered to the webhook configured for your inbox@yourdomain.com alias — the tag doesn't need its own alias, and you don't need to create one per tag. The payload arrives as:

"recipient": {
  "name": null,
  "email": "inbox+support@yourdomain.com",
  "tag": "support"
}

Note that recipient.email keeps the full address that was actually used, tag included — so you can always reconstruct exactly what the sender typed. The tag field is simply that same value, pre-parsed for you.

A few details worth knowing:

  • Untagged mail sets tag to null, never an empty string or a missing key. You can rely on the field being present.
  • Only the first + separates. inbox+a+b@yourdomain.com yields the tag a+b.
  • A trailing + carries no tag. inbox+@yourdomain.com yields null, since there's nothing after the delimiter.
  • Tags are free-form. You don't register them anywhere. Any tag a sender invents reaches your endpoint, so treat tag as untrusted input and match it against values you expect rather than acting on it blindly.

This makes tag a natural routing key. A single inbox@ alias can back inbox+support@, inbox+billing@, and inbox+sales@, with your endpoint switching on recipient.tag instead of you maintaining three aliases and three webhooks.

Reading the spam score

We report the score and let you judge. There's no isSpam verdict, no threshold, and no action field — deliberately. A spam verdict is a policy decision that depends on your tolerance, not ours, and a boolean derived from a threshold you chose tells you nothing the score didn't already.

As a rough calibration: scores below 0 are clean, 5 and above is worth treating with suspicion, and 7+ is very likely spam. Tune to your own traffic rather than trusting these as absolutes.

Compare spam.score against your own cut-off, or express the cut-off declaratively with alias rules and let EmailConnect accept, block, or tag the mail before it ever reaches your endpoint.

On Free plans the spam object is replaced by a stub — { "available": false, "message": "…" } — rather than removed. Check for spam.score, not for the presence of spam.

How attachments reach you

Small files arrive inline, base64-encoded in message.attachments[].content. Larger ones are offloaded and arrive as a downloadUrl.

That URL points at EmailConnect, not at S3. We redirect you to a short-lived signed URL when you fetch it. That's deliberate: it means downloads can be counted, revoked, and kept in-region, and it means the link in your payload doesn't rot the moment a signature expires. Its real lifetime is your retention window — once the email is purged, the file is gone and the URL returns 404.

  • On paid plans, files up to 2 MB are inlined; larger ones are offloaded to S3 — yours, if you've configured a storage connection, otherwise ours. So is any file we can't sensibly inline (a .zip, a video), whatever its size — a 40 KB archive goes to S3.
  • On Free, there is no offloading. Files up to 128 KB are inlined and anything larger is rejected, arriving as excluded: true with a reason. S3 offloading starts at Maker.
  • The whole email, including attachments, must fit within 10 MB. Bigger messages are rejected, so there's no "use S3 for files over 10 MB" path.
  • A maximum of 20 attachments per email are delivered. Beyond that, the rest are dropped.

Files uploaded synchronously (uploadType: "sync") have a downloadUrl that works immediately. Asynchronous uploads (uploadType: "async") may briefly return 202 with a retryAfter while the upload finishes. See Attachment processing for the full picture.

Verifying what you received

integrity.contentHash and integrity.rawEmailHash (Maker+) are SHA-256 digests — of the processed content, and of the raw message as it arrived. Use them to detect duplicate deliveries or build an audit trail.

These are not the webhook signature. The signature that proves the request came from us is an HTTP header, not a payload field — see webhook configuration.

Features by plan

Feature Free Maker Business Platform
Message basics (sender, recipient, subject, date) + + + +
Text + HTML body + + + +
Attachments (inline base64) + + + +
Link extraction (url, domain) + + + +
Email classification (type, confidence, signals) + + + +
Link anchor text - + + +
Markdown conversion - + + +
Integrity hashes - + + +
Spam analysis (rspamd) - + + +
Attachment metadata (hash, dimensions, page count) - + + +
S3 attachment offloading - + + +
Virus scanning (ClamAV) - - + +
Custom payload builder - - + +

Markdown and reply parsing are also per-alias settings, and both are off by default — a Maker plan alone won't produce them until you enable them on the alias.

Related topics