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

FieldTypeRequiredDescription
idstringyesUnique delivery event ID.
event"email.received"yesEvent type identifier. Always "email.received" for this event type.
versionWebhookVersionyesAPI version in date format (YYYY-MM-DD). Use this to detect version mismatches between webhook and SDK.
deliveryobjectyesMetadata about this webhook delivery.
emailobjectyesThe email that triggered this event.

Delivery

FieldTypeRequiredDescription
delivery.endpoint_idstringyesID of the webhook endpoint receiving this event. Matches the endpoint ID from your Primitive dashboard.
delivery.attemptintegeryesDelivery attempt number, starting at 1. Increments with each retry if previous attempts failed.
delivery.attempted_atstringyesISO 8601 timestamp (UTC) when this delivery was attempted.

Email And SMTP

FieldTypeRequiredDescription
email.idstringyesUnique email ID in Primitive. Use this ID when calling Primitive APIs to reference this email.
email.thread_idstring | nullnoConversation 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_atstringyesISO 8601 timestamp (UTC) when Primitive received the email.
email.smtpobjectyesSMTP envelope information. This is the real sender and recipient info from the SMTP transaction, which may differ from headers.
email.smtp.helostring | nullyesHELO/EHLO hostname from the sending server. Null if not provided during SMTP transaction.
email.smtp.mail_fromstringyesSMTP envelope sender (MAIL FROM command). This is the bounce address, which may differ from the From header.
email.smtp.rcpt_tostring[]yesSMTP envelope recipients (RCPT TO commands). All addresses that received this email in a single delivery.
email.headersobjectyesParsed email headers. These are extracted from the email content, not the SMTP envelope.
email.headers.message_idstring | nullyesMessage-ID header value. Null if the email had no Message-ID header.
email.headers.subjectstring | nullyesSubject header value. Null if the email had no Subject header.
email.headers.fromstringyesFrom header value. May include display name: "John Doe" <john@example.com>
email.headers.tostringyesTo header value. May include multiple addresses or display names.
email.headers.datestring | nullyesDate header value as it appeared in the email. Null if the email had no Date header.

Raw Content

FieldTypeRequiredDescription
email.contentobjectyesRaw email content and download information.
email.content.rawRawContentyesRaw email in RFC 5322 format. May be inline (base64) or download-only depending on size.
email.content.raw.includedbooleanyesWhether the raw content is included inline. true means data is present. false means download is required.
email.content.raw.encoding"base64"when inlineEncoding used for the data field. Always "base64".
email.content.raw.reason_code"size_exceeded"when download-onlyReason the content was not included inline.
email.content.raw.max_inline_bytesintegeryesMaximum size in bytes for inline inclusion. Emails larger than this threshold require download.
email.content.raw.size_bytesintegeryesActual size of the raw email in bytes.
email.content.raw.sha256stringyesSHA-256 hash of the raw email content (hex-encoded). Use this to verify integrity after base64 decoding or download.
email.content.raw.datastringwhen inlineBase64-encoded raw email (RFC 5322 format). Decode with Buffer.from(data, 'base64') in Node.js.
email.content.downloadobjectyesDownload information for the raw email. Always present, even if raw content is inline.
email.content.download.urlstringyesURL to download the raw email as-is in RFC 5322 format. Managed Primitive always issues HTTPS.
email.content.download.expires_atstringyesISO 8601 timestamp (UTC) when this URL expires. Download before this time or the URL will return 403.

Parsed Content

FieldTypeRequiredDescription
email.parsedParsedDatayesParsed email content (body text, HTML, attachments). Check status to determine if parsing succeeded.
email.parsed.status"complete" | "failed"yesDiscriminant indicating whether parsing succeeded.
email.parsed.errorParsedError | nullyesAlways null when parsing succeeds. Contains failure details when parsing fails.
email.parsed.error.code"PARSE_FAILED" | "ATTACHMENT_EXTRACTION_FAILED"when failedError code indicating the type of failure.
email.parsed.error.messagestringwhen failedHuman-readable error message describing what went wrong.
email.parsed.error.retryablebooleanwhen failedWhether retrying might succeed. If true, the error was transient. If false, the email itself is problematic.
email.parsed.body_textstring | nullyesPlain text body of the email. Null if the email had no text/plain part or parsing failed.
email.parsed.body_htmlstring | nullyesHTML body of the email. Null if the email had no text/html part or parsing failed.
email.parsed.reply_toEmailAddress[] | nullyesParsed Reply-To header addresses. Null if the email had no Reply-To header or parsing failed.
email.parsed.ccEmailAddress[] | nullyesParsed CC header addresses. Null if the email had no CC header or parsing failed.
email.parsed.bccEmailAddress[] | nullyesParsed BCC header addresses. Null if the email had no BCC header or parsing failed.
email.parsed.to_addressesEmailAddress[] | nullyesParsed To header addresses. Null if the email had no To header or parsing failed.
email.parsed.reply_to[].addressstringyesThe email address portion. This is the raw value from the email header with no validation applied.
email.parsed.reply_to[].namestring | nullyesThe display name portion, if present. Null if the address had no display name.
email.parsed.cc[].addressstringyesThe email address portion.
email.parsed.cc[].namestring | nullyesThe display name portion, if present.
email.parsed.bcc[].addressstringyesThe email address portion.
email.parsed.bcc[].namestring | nullyesThe display name portion, if present.
email.parsed.to_addresses[].addressstringyesThe email address portion.
email.parsed.to_addresses[].namestring | nullyesThe display name portion, if present.
email.parsed.in_reply_tostring[] | nullyesIn-Reply-To header values (Message-IDs of the email or emails being replied to).
email.parsed.referencesstring[] | nullyesReferences header values (Message-IDs of the email thread).
email.parsed.attachmentsWebhookAttachment[]yesList of attachments with metadata. May contain partial attachment metadata even when parsing failed.
email.parsed.attachments[].filenamestring | nullyesOriginal filename from the email. May be null if the attachment had no filename specified.
email.parsed.attachments[].content_typestringyesMIME content type, for example "application/pdf" or "image/png".
email.parsed.attachments[].size_bytesintegeryesSize of the attachment in bytes.
email.parsed.attachments[].sha256stringyesSHA-256 hash of the attachment content (hex-encoded). Use this to verify attachment integrity after download.
email.parsed.attachments[].part_indexintegeryesZero-based index of this part in the MIME structure.
email.parsed.attachments[].tar_pathstringyesPath to this attachment within the downloaded tar.gz archive.
email.parsed.attachments_download_urlstring | nullyesURL to download all attachments as a tar.gz archive. Null if the email had no attachments or parsing failed.

Auth

FieldTypeRequiredDescription
email.authEmailAuthyesEmail authentication results for SPF, DKIM, and DMARC.
email.auth.spfSpfResultyesSPF verification result.
email.auth.dmarcDmarcResultyesDMARC verification result.
email.auth.dmarcPolicyDmarcPolicyyesDMARC policy from the sender's DNS record.
email.auth.dmarcFromDomainstring | nullyesThe organizational domain used for DMARC lookups.
email.auth.dmarcSpfAlignedbooleanyesWhether SPF aligned with the From domain for DMARC purposes.
email.auth.dmarcDkimAlignedbooleanyesWhether DKIM aligned with the From domain for DMARC purposes.
email.auth.dmarcSpfStrictboolean | nullyesWhether DMARC SPF alignment mode is strict.
email.auth.dmarcDkimStrictboolean | nullyesWhether DMARC DKIM alignment mode is strict.
email.auth.dkimSignaturesDkimSignature[]yesAll DKIM signatures found in the email with their verification results.
email.auth.dkimSignatures[].domainstringyesThe domain that signed this DKIM signature (d= tag). This may differ from the From domain.
email.auth.dkimSignatures[].selectorstring | nullyesThe DKIM selector used to locate the public key (s= tag).
email.auth.dkimSignatures[].resultDkimResultyesVerification result for this specific signature.
email.auth.dkimSignatures[].alignedbooleanyesWhether this signature's domain aligns with the From domain for DMARC.
email.auth.dkimSignatures[].keyBitsinteger | nullyesKey size in bits. Null if the key size could not be determined.
email.auth.dkimSignatures[].algostring | nullyesSigning algorithm, for example "rsa-sha256" or "ed25519-sha256".

Analysis

FieldTypeRequiredDescription
email.analysisEmailAnalysisyesEmail analysis and classification results. Fields may be absent when that analysis was not performed.
email.analysis.spamassassinobjectnoSpamAssassin analysis results. Present when spam scoring ran.
email.analysis.spamassassin.scorenumberyesOverall spam score. Higher scores indicate higher likelihood of spam.
email.analysis.senderobjectnoPrimitive managed sender authentication verdict derived from email.auth.
email.analysis.sender.authenticatedbooleanyesTrue 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"yesThe public basis for the sender authentication verdict.
email.analysis.sender.reasonsstring[]yesShort factual reasons for the sender authentication verdict.
email.analysis.forwardForwardAnalysisnoForward detection and analysis results. In managed Primitive webhook deliveries, this object is included only when forwarded content is detected.
email.analysis.forward.detectedbooleanyesWhether any forwards were detected in the email. In managed Primitive webhook deliveries, this is true when the object is present.
email.analysis.forward.resultsForwardResult[]yesAnalysis results for each detected forward.
email.analysis.forward.results[].type"inline" | "attachment"yesWhether the detected forward was inline or in an attachment.
email.analysis.forward.results[].attachment_tar_pathstringwhen attachmentPath to the attachment in the attachments tar archive.
email.analysis.forward.results[].attachment_filenamestring | nullwhen attachmentOriginal filename of the attachment, if available.
email.analysis.forward.results[].analyzedbooleanwhen attachmentWhether this attachment was analyzed.
email.analysis.forward.results[].original_senderForwardOriginalSender | nullyesOriginal sender of the forwarded email, if extractable.
email.analysis.forward.results[].original_sender.emailstringyesEmail address of the original sender.
email.analysis.forward.results[].original_sender.domainstringyesDomain of the original sender.
email.analysis.forward.results[].verificationForwardVerification | nullyesVerification result for the forwarded email. Null when an attachment forward was not analyzed.
email.analysis.forward.results[].verification.verdictForwardVerdictyesOverall verdict on whether the forward is authentic.
email.analysis.forward.results[].verification.confidenceAuthConfidenceyesConfidence level for this verdict.
email.analysis.forward.results[].verification.dkim_verifiedbooleanyesWhether a valid DKIM signature was found that verifies the original sender.
email.analysis.forward.results[].verification.dkim_domainstring | nullyesDomain of the DKIM signature that verified the forward, if any.
email.analysis.forward.results[].verification.dmarc_policyDmarcPolicyyesDMARC policy of the original sender's domain.
email.analysis.forward.results[].summarystringyesHuman-readable summary of the forward analysis.
email.analysis.forward.attachments_foundintegeryesTotal number of .eml attachments found.
email.analysis.forward.attachments_analyzedintegeryesNumber of .eml attachments that were analyzed.
email.analysis.forward.attachments_limitinteger | nullyesMaximum 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.