Webhook Payload
Primitive delivers inbound email as an email.received event. The same event shape is sent to self-hosted webhooks and hosted Primitive Functions.
Top-Level Shape
This is the actual email.received payload shape Primitive sends to webhook endpoints and Primitive Functions.
{ "id": "evt_0d4ce769a6f5822afa37417473d33ff8ae0254f280802d94f4eff284cde30567", "event": "email.received", "version": "2025-12-14", "delivery": { "endpoint_id": "ep_01HZYABCDEF1234567890", "attempt": 1, "attempted_at": "2026-01-01T00:00:05.000Z" }, "email": { "id": "em_01HZYABCDEF1234567890", "received_at": "2026-01-01T00:00:00.000Z", "smtp": { "helo": "mail.example.com", "mail_from": "bounce@example.com", "rcpt_to": [ "support@your-org.primitive.email" ] }, "headers": { "message_id": "<message-id@example.com>", "subject": "Need help", "from": "Alice <alice@example.com>", "to": "Support <support@your-org.primitive.email>", "date": "Thu, 01 Jan 2026 00:00:00 +0000" }, "content": { "raw": { "included": true, "encoding": "base64", "max_inline_bytes": 262144, "size_bytes": 257, "sha256": "8bf8f3c56544d3e56ac84f414754674d51c55d6504d847a9868af46ab65d4f0f", "data": "RnJvbTogQWxpY2UgPGFsaWNlQGV4YW1wbGUuY29tPg0KVG86IFN1cHBvcnQgPHN1cHBvcnRAeW91ci1vcmcucHJpbWl0aXZlLmVtYWlsPg0KQ2M6IEJvYiA8Ym9iQGV4YW1wbGUuY29tPg0KUmVwbHktVG86IEFsaWNlIFN1cHBvcnQgPHJlcGx5QGV4YW1wbGUuY29tPg0KU3ViamVjdDogTmVlZCBoZWxwDQpEYXRlOiBUaHUsIDAxIEphbiAyMDI2IDAwOjAwOjAwICswMDAwDQpNZXNzYWdlLUlEOiA8bWVzc2FnZS1pZEBleGFtcGxlLmNvbT4NCg0KSGVsbG8=" }, "download": { "url": "https://api.primitive.dev/v1/emails/em_01HZYABCDEF1234567890/raw?token=download-token", "expires_at": "2026-01-02T00:00:00.000Z" } }, "parsed": { "status": "complete", "error": null, "body_text": "Hello", "body_html": "<p>Hello</p>", "reply_to": [ { "address": "reply@example.com", "name": "Alice Support" } ], "cc": [ { "address": "bob@example.com", "name": "Bob" } ], "bcc": null, "to_addresses": [ { "address": "support@your-org.primitive.email", "name": "Support" } ], "in_reply_to": [ "<previous@example.com>" ], "references": [ "<root@example.com>", "<previous@example.com>" ], "attachments": [ { "filename": "invoice.pdf", "content_type": "application/pdf", "size_bytes": 24576, "sha256": "b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3d4e5f6a1b2c3", "part_index": 0, "tar_path": "0_invoice.pdf" } ], "attachments_download_url": "https://api.primitive.dev/v1/emails/em_01HZYABCDEF1234567890/attachments.tar.gz?token=download-token" }, "analysis": { "spamassassin": { "score": 0.1 }, "sender": { "authenticated": true, "basis": "dmarc_aligned", "reasons": [ "DKIM signature aligned with the From domain" ] } }, "auth": { "spf": "pass", "dmarc": "pass", "dmarcPolicy": "reject", "dmarcFromDomain": "example.com", "dmarcSpfAligned": false, "dmarcDkimAligned": true, "dmarcSpfStrict": false, "dmarcDkimStrict": false, "dkimSignatures": [ { "domain": "example.com", "selector": "default", "result": "pass", "aligned": true, "keyBits": 2048, "algo": "rsa-sha256" } ] }, "thread_id": "thr_01HZYABCDEF1234567890" } }
Stable Identifiers
The top-level id is stable across retries and manual replays to the same endpoint. Use it for webhook dedupe.
email.id is the stored email id. Use it with REST endpoints such as /v1/emails/{id}, /v1/emails/{id}/replay, and /v1/emails/{id}/reply.
Delivery Metadata
The delivery object records attempt count, endpoint information, and routing metadata. Retries increment delivery.attempt while preserving the event id.
Parsed Content
When parsing succeeds, email.parsed.status is complete and body fields are available. Large raw content and attachments can be stored separately and exposed through signed download URLs.
If parsing fails, email.parsed.status is failed, email.parsed.error is populated, body and address fields are null, and attachments are empty or partial metadata only.
Headers
Headers preserve the important RFC 5322 fields needed for audit trails. Use email.headers.message_id, email.parsed.in_reply_to, email.parsed.references, and email.thread_id when building threading logic.
Raw Content
Small raw emails are included inline at email.content.raw.data as base64. If the raw email is larger than email.content.raw.max_inline_bytes, email.content.raw.included is false and the raw content must be fetched from email.content.download.url before expires_at.
Attachments are listed as metadata in email.parsed.attachments. Download the files from email.parsed.attachments_download_url when it is not null.
Auth And Analysis
email.auth contains SPF, DMARC, and DKIM results from the inbound message. email.analysis.sender is Primitive's sender authentication verdict derived from those auth fields. email.analysis.spamassassin is present when spam scoring ran. email.analysis.forward is optional; in managed Primitive webhook deliveries it is currently included only when forward analysis detects forwarded content.
Field Reference
These descriptions are sourced from the webhook JSON schema used by the SDK, with the managed email.analysis.sender extension documented from the webhook delivery implementation. Required means required when its parent object or union variant is present.
Top Level
| Field | Type | Required | Description |
|---|---|---|---|
id | string | yes | Unique delivery event ID. |
event | "email.received" | yes | Event type identifier. Always "email.received" for this event type. |
version | WebhookVersion | yes | API version in date format (YYYY-MM-DD). Use this to detect version mismatches between webhook and SDK. |
delivery | object | yes | Metadata about this webhook delivery. |
email | object | yes | The email that triggered this event. |
Delivery
| Field | Type | Required | Description |
|---|---|---|---|
delivery.endpoint_id | string | yes | ID of the webhook endpoint receiving this event. Matches the endpoint ID from your Primitive dashboard. |
delivery.attempt | integer | yes | Delivery attempt number, starting at 1. Increments with each retry if previous attempts failed. |
delivery.attempted_at | string | yes | ISO 8601 timestamp (UTC) when this delivery was attempted. |
Email And SMTP
| Field | Type | Required | Description |
|---|---|---|---|
email.id | string | yes | Unique email ID in Primitive. Use this ID when calling Primitive APIs to reference this email. |
email.thread_id | string | null | no | Conversation thread this email belongs to. Inbound and outbound messages in the same conversation share a thread_id. Null on messages received before threading was enabled. |
email.received_at | string | yes | ISO 8601 timestamp (UTC) when Primitive received the email. |
email.smtp | object | yes | SMTP envelope information. This is the real sender and recipient info from the SMTP transaction, which may differ from headers. |
email.smtp.helo | string | null | yes | HELO/EHLO hostname from the sending server. Null if not provided during SMTP transaction. |
email.smtp.mail_from | string | yes | SMTP envelope sender (MAIL FROM command). This is the bounce address, which may differ from the From header. |
email.smtp.rcpt_to | string[] | yes | SMTP envelope recipients (RCPT TO commands). All addresses that received this email in a single delivery. |
email.headers | object | yes | Parsed email headers. These are extracted from the email content, not the SMTP envelope. |
email.headers.message_id | string | null | yes | Message-ID header value. Null if the email had no Message-ID header. |
email.headers.subject | string | null | yes | Subject header value. Null if the email had no Subject header. |
email.headers.from | string | yes | From header value. May include display name: "John Doe" <john@example.com> |
email.headers.to | string | yes | To header value. May include multiple addresses or display names. |
email.headers.date | string | null | yes | Date header value as it appeared in the email. Null if the email had no Date header. |
Raw Content
| Field | Type | Required | Description |
|---|---|---|---|
email.content | object | yes | Raw email content and download information. |
email.content.raw | RawContent | yes | Raw email in RFC 5322 format. May be inline (base64) or download-only depending on size. |
email.content.raw.included | boolean | yes | Whether the raw content is included inline. true means data is present. false means download is required. |
email.content.raw.encoding | "base64" | when inline | Encoding used for the data field. Always "base64". |
email.content.raw.reason_code | "size_exceeded" | when download-only | Reason the content was not included inline. |
email.content.raw.max_inline_bytes | integer | yes | Maximum size in bytes for inline inclusion. Emails larger than this threshold require download. |
email.content.raw.size_bytes | integer | yes | Actual size of the raw email in bytes. |
email.content.raw.sha256 | string | yes | SHA-256 hash of the raw email content (hex-encoded). Use this to verify integrity after base64 decoding or download. |
email.content.raw.data | string | when inline | Base64-encoded raw email (RFC 5322 format). Decode with Buffer.from(data, 'base64') in Node.js. |
email.content.download | object | yes | Download information for the raw email. Always present, even if raw content is inline. |
email.content.download.url | string | yes | URL to download the raw email as-is in RFC 5322 format. Managed Primitive always issues HTTPS. |
email.content.download.expires_at | string | yes | ISO 8601 timestamp (UTC) when this URL expires. Download before this time or the URL will return 403. |
Parsed Content
| Field | Type | Required | Description |
|---|---|---|---|
email.parsed | ParsedData | yes | Parsed email content (body text, HTML, attachments). Check status to determine if parsing succeeded. |
email.parsed.status | "complete" | "failed" | yes | Discriminant indicating whether parsing succeeded. |
email.parsed.error | ParsedError | null | yes | Always null when parsing succeeds. Contains failure details when parsing fails. |
email.parsed.error.code | "PARSE_FAILED" | "ATTACHMENT_EXTRACTION_FAILED" | when failed | Error code indicating the type of failure. |
email.parsed.error.message | string | when failed | Human-readable error message describing what went wrong. |
email.parsed.error.retryable | boolean | when failed | Whether retrying might succeed. If true, the error was transient. If false, the email itself is problematic. |
email.parsed.body_text | string | null | yes | Plain text body of the email. Null if the email had no text/plain part or parsing failed. |
email.parsed.body_html | string | null | yes | HTML body of the email. Null if the email had no text/html part or parsing failed. |
email.parsed.reply_to | EmailAddress[] | null | yes | Parsed Reply-To header addresses. Null if the email had no Reply-To header or parsing failed. |
email.parsed.cc | EmailAddress[] | null | yes | Parsed CC header addresses. Null if the email had no CC header or parsing failed. |
email.parsed.bcc | EmailAddress[] | null | yes | Parsed BCC header addresses. Null if the email had no BCC header or parsing failed. |
email.parsed.to_addresses | EmailAddress[] | null | yes | Parsed To header addresses. Null if the email had no To header or parsing failed. |
email.parsed.reply_to[].address | string | yes | The email address portion. This is the raw value from the email header with no validation applied. |
email.parsed.reply_to[].name | string | null | yes | The display name portion, if present. Null if the address had no display name. |
email.parsed.cc[].address | string | yes | The email address portion. |
email.parsed.cc[].name | string | null | yes | The display name portion, if present. |
email.parsed.bcc[].address | string | yes | The email address portion. |
email.parsed.bcc[].name | string | null | yes | The display name portion, if present. |
email.parsed.to_addresses[].address | string | yes | The email address portion. |
email.parsed.to_addresses[].name | string | null | yes | The display name portion, if present. |
email.parsed.in_reply_to | string[] | null | yes | In-Reply-To header values (Message-IDs of the email or emails being replied to). |
email.parsed.references | string[] | null | yes | References header values (Message-IDs of the email thread). |
email.parsed.attachments | WebhookAttachment[] | yes | List of attachments with metadata. May contain partial attachment metadata even when parsing failed. |
email.parsed.attachments[].filename | string | null | yes | Original filename from the email. May be null if the attachment had no filename specified. |
email.parsed.attachments[].content_type | string | yes | MIME content type, for example "application/pdf" or "image/png". |
email.parsed.attachments[].size_bytes | integer | yes | Size of the attachment in bytes. |
email.parsed.attachments[].sha256 | string | yes | SHA-256 hash of the attachment content (hex-encoded). Use this to verify attachment integrity after download. |
email.parsed.attachments[].part_index | integer | yes | Zero-based index of this part in the MIME structure. |
email.parsed.attachments[].tar_path | string | yes | Path to this attachment within the downloaded tar.gz archive. |
email.parsed.attachments_download_url | string | null | yes | URL to download all attachments as a tar.gz archive. Null if the email had no attachments or parsing failed. |
Auth
| Field | Type | Required | Description |
|---|---|---|---|
email.auth | EmailAuth | yes | Email authentication results for SPF, DKIM, and DMARC. |
email.auth.spf | SpfResult | yes | SPF verification result. |
email.auth.dmarc | DmarcResult | yes | DMARC verification result. |
email.auth.dmarcPolicy | DmarcPolicy | yes | DMARC policy from the sender's DNS record. |
email.auth.dmarcFromDomain | string | null | yes | The organizational domain used for DMARC lookups. |
email.auth.dmarcSpfAligned | boolean | yes | Whether SPF aligned with the From domain for DMARC purposes. |
email.auth.dmarcDkimAligned | boolean | yes | Whether DKIM aligned with the From domain for DMARC purposes. |
email.auth.dmarcSpfStrict | boolean | null | yes | Whether DMARC SPF alignment mode is strict. |
email.auth.dmarcDkimStrict | boolean | null | yes | Whether DMARC DKIM alignment mode is strict. |
email.auth.dkimSignatures | DkimSignature[] | yes | All DKIM signatures found in the email with their verification results. |
email.auth.dkimSignatures[].domain | string | yes | The domain that signed this DKIM signature (d= tag). This may differ from the From domain. |
email.auth.dkimSignatures[].selector | string | null | yes | The DKIM selector used to locate the public key (s= tag). |
email.auth.dkimSignatures[].result | DkimResult | yes | Verification result for this specific signature. |
email.auth.dkimSignatures[].aligned | boolean | yes | Whether this signature's domain aligns with the From domain for DMARC. |
email.auth.dkimSignatures[].keyBits | integer | null | yes | Key size in bits. Null if the key size could not be determined. |
email.auth.dkimSignatures[].algo | string | null | yes | Signing algorithm, for example "rsa-sha256" or "ed25519-sha256". |
Analysis
| Field | Type | Required | Description |
|---|---|---|---|
email.analysis | EmailAnalysis | yes | Email analysis and classification results. Fields may be absent when that analysis was not performed. |
email.analysis.spamassassin | object | no | SpamAssassin analysis results. Present when spam scoring ran. |
email.analysis.spamassassin.score | number | yes | Overall spam score. Higher scores indicate higher likelihood of spam. |
email.analysis.sender | object | no | Primitive managed sender authentication verdict derived from email.auth. |
email.analysis.sender.authenticated | boolean | yes | True when the From domain proved control of the send through aligned SPF, aligned DKIM, or provider attestation. |
email.analysis.sender.basis | "dmarc_aligned" | "google_provider_attested" | "spf_aligned" | "unauthenticated" | yes | The public basis for the sender authentication verdict. |
email.analysis.sender.reasons | string[] | yes | Short factual reasons for the sender authentication verdict. |
email.analysis.forward | ForwardAnalysis | no | Forward detection and analysis results. In managed Primitive webhook deliveries, this object is included only when forwarded content is detected. |
email.analysis.forward.detected | boolean | yes | Whether any forwards were detected in the email. In managed Primitive webhook deliveries, this is true when the object is present. |
email.analysis.forward.results | ForwardResult[] | yes | Analysis results for each detected forward. |
email.analysis.forward.results[].type | "inline" | "attachment" | yes | Whether the detected forward was inline or in an attachment. |
email.analysis.forward.results[].attachment_tar_path | string | when attachment | Path to the attachment in the attachments tar archive. |
email.analysis.forward.results[].attachment_filename | string | null | when attachment | Original filename of the attachment, if available. |
email.analysis.forward.results[].analyzed | boolean | when attachment | Whether this attachment was analyzed. |
email.analysis.forward.results[].original_sender | ForwardOriginalSender | null | yes | Original sender of the forwarded email, if extractable. |
email.analysis.forward.results[].original_sender.email | string | yes | Email address of the original sender. |
email.analysis.forward.results[].original_sender.domain | string | yes | Domain of the original sender. |
email.analysis.forward.results[].verification | ForwardVerification | null | yes | Verification result for the forwarded email. Null when an attachment forward was not analyzed. |
email.analysis.forward.results[].verification.verdict | ForwardVerdict | yes | Overall verdict on whether the forward is authentic. |
email.analysis.forward.results[].verification.confidence | AuthConfidence | yes | Confidence level for this verdict. |
email.analysis.forward.results[].verification.dkim_verified | boolean | yes | Whether a valid DKIM signature was found that verifies the original sender. |
email.analysis.forward.results[].verification.dkim_domain | string | null | yes | Domain of the DKIM signature that verified the forward, if any. |
email.analysis.forward.results[].verification.dmarc_policy | DmarcPolicy | yes | DMARC policy of the original sender's domain. |
email.analysis.forward.results[].summary | string | yes | Human-readable summary of the forward analysis. |
email.analysis.forward.attachments_found | integer | yes | Total number of .eml attachments found. |
email.analysis.forward.attachments_analyzed | integer | yes | Number of .eml attachments that were analyzed. |
email.analysis.forward.attachments_limit | integer | null | yes | Maximum number of attachments that will be analyzed, or null if unlimited. |
Security
Every webhook includes a Primitive-Signature header. Verify it before trusting the payload. See Signature Verification.