Building Webhook Payloads (Contract Module)
Construct schema-valid email.received webhook payloads with the Node SDK's contract module, for anyone producing test fixtures or simulating inbound mail that must match Primitive's wire contract exactly.
The contract subpath of @primitivedotdev/sdk (@primitivedotdev/sdk/contract) builds schema-valid email.received webhook payloads in code, instead of you hand-assembling JSON that has to match Primitive's wire contract byte-for-byte. Reach for it when you're writing test fixtures or a mock inbound-webhook producer that must emit a payload the SDKs' receive() / handleWebhook() will accept.
This module is Node-only, separate from the Workers-safe @primitivedotdev/sdk/api subpath used inside Primitive Functions handlers. Import it from tooling and test code, not from a deployed Function.
If you're consuming inbound webhooks rather than producing them, you don't need this page. Use primitive.receive(...) as described in Receiving Inbound Email, and see the Inbound and Outbound Email Model for the normalized ReceivedEmail shape and the raw email.received event this module builds.
What the contract module builds#
The contract module assembles the full EmailReceivedEvent envelope from the pieces of an email, which is the inverse of parsing one. That envelope is the shape validated by the shared JSON Schema at json-schema/email-received-event.schema.json and catalogued on Webhook Events Overview.
Two entry points cover the common cases:
| Function | Use when |
|---|---|
buildEmailReceivedEvent | You're writing the input by hand for a specific test case and want the full event envelope built for you. |
buildEventFromParsedData | You already have an intermediate ParsedInput shape, for example the output of your own MIME-parsing step, and want that folded into the envelope. |
Both live under @primitivedotdev/sdk/contract, alongside the producer-side TypeScript types documented on Primitive Contract Types Reference (EmailReceivedEventInput, ParsedInput, RawContentInline, RawContentDownloadOnly).
Build a fixture from a hand-written input#
Install the SDK, import buildEmailReceivedEvent with its input type, and hand it an EmailReceivedEventInput. The result is a complete EmailReceivedEvent you can serialize as a webhook body.
- 1
Install the SDK#
The contract module ships inside the main package; there's no separate install.
npm install @primitivedotdev/sdk - 2
Import the builder and its input type#
// fixtures/inbound.ts import { buildEmailReceivedEvent } from "@primitivedotdev/sdk/contract"; import type { EmailReceivedEventInput } from "@primitivedotdev/sdk/contract";The exact field list on
EmailReceivedEventInputis on Primitive Contract Types Reference; build your input against those types so the compiler tells you what's missing. - 3
Build the event#
// fixtures/inbound.ts (continued) const input: EmailReceivedEventInput = { /* fields per the contract types reference */ } as EmailReceivedEventInput; const event = buildEmailReceivedEvent(input); console.log(event.event); // "email.received"The result is a full
EmailReceivedEventobject: the same shapeprimitive.receive(...)normalizes andvalidateEmailReceivedEventaccepts. - 4
Serialize it as a webhook body#
// fixtures/inbound.ts (continued) const body = JSON.stringify(event); // POST `body` to your handler, or pass it straight into // validateEmailReceivedEvent(JSON.parse(body)) to confirm it's schema-valid.
Build from an intermediate ParsedInput shape#
Use buildEventFromParsedData when your harness already has its own MIME-parsing step whose output is close to, but not exactly, the wire shape, so you don't hand-map every field.
// fixtures/from-parsed.ts
import { buildEventFromParsedData } from "@primitivedotdev/sdk/contract";
import type { ParsedInput } from "@primitivedotdev/sdk/contract";
const parsed: ParsedInput = {
/* your parser's output, mapped to ParsedInput */
} as ParsedInput;
const event = buildEventFromParsedData(parsed);
buildEventFromParsedData is the right call when you're bridging from another parser's output; buildEmailReceivedEvent is the right call when you're writing the input by hand.
Raw content is a discriminated union: use RawContentInline when the raw bytes are included inline, and RawContentDownloadOnly when you're modelling content too large to inline. The schema discriminates on included, so a partial mix of the two shapes is rejected. Full field lists are on Primitive Contract Types Reference.
Validate what you built#
Run every built event through validateEmailReceivedEvent from @primitivedotdev/sdk/webhook, the same validator your handler uses in production, before you rely on the fixture.
// fixtures/validate.ts
import { validateEmailReceivedEvent } from "@primitivedotdev/sdk/webhook";
const validated = validateEmailReceivedEvent(event);
validateEmailReceivedEvent throws a WebhookValidationError when the payload fails the schema; safeValidateEmailReceivedEvent from the same subpath returns a result instead of throwing.
Keep this validation step in CI. A future schema change that tightens a constraint should fail your fixture build, not a customer's production handler.
Next steps#
Browse every producer-side type (EmailReceivedEventInput, ParsedInput, RawContentInline/DownloadOnly) exported from the contract module.
Receiving Inbound EmailSee how primitive.receive() normalizes the email.received event this module builds.
Webhook Events OverviewUnderstand the shared webhook contract, signature verification, and the full event catalog this payload shape belongs to.
Parsing Raw Email (.eml)Parse real MIME bytes into structured bodies and attachments, the inverse operation of building a fixture by hand.
Was this page helpful?