Documentation Index: Fetch llms.txt first to discover every published page. This page is also available as Markdown at /node-sdk-contract-module.md.
Verified · 8/11/2026

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.

Note

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:

FunctionUse when
buildEmailReceivedEventYou're writing the input by hand for a specific test case and want the full event envelope built for you.
buildEventFromParsedDataYou 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. 1

    Install the SDK#

    The contract module ships inside the main package; there's no separate install.

    npm install @primitivedotdev/sdk
    
  2. 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 EmailReceivedEventInput is on Primitive Contract Types Reference; build your input against those types so the compiler tells you what's missing.

  3. 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 EmailReceivedEvent object: the same shape primitive.receive(...) normalizes and validateEmailReceivedEvent accepts.

  4. 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.

Tip

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.

Warning

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#

Was this page helpful?

© Primitive SDKs

Powered by Browzer