---
title: "Building Webhook Payloads (Contract Module)"
canonical: "https://test.abhinandan.one/node-sdk-contract-module"
markdown_url: "https://test.abhinandan.one/node-sdk-contract-module.md"
publisher: "Primitive SDKs"
kind: "guide"
content_type: "reference"
category: "Node.js SDK"
description: "buildEmailReceivedEvent and buildEventFromParsedData construct schema-valid email.received webhook payloads from @primitivedotdev/sdk/contract."
keywords: ["buildEmailReceivedEvent", "buildEventFromParsedData", "@primitivedotdev/sdk/contract", "EmailReceivedEventInput", "email.received fixture", "ParsedInput"]
last_modified: "2026-08-11T18:55:00.051193+00:00"
published_at: "2026-08-11T18:54:59.877929+00:00"
sections:
  - {anchor: "what-the-contract-module-builds", title: "What the contract module builds"}
  - {anchor: "build-a-fixture-from-a-hand-written-input", title: "Build a fixture from a hand-written input"}
  - {anchor: "step-install-the-sdk", title: "Install the SDK"}
  - {anchor: "step-import-the-builder-and-its-input-type", title: "Import the builder and its input type"}
  - {anchor: "step-build-the-event", title: "Build the event"}
  - {anchor: "step-serialize-it-as-a-webhook-body", title: "Serialize it as a webhook body"}
  - {anchor: "build-from-an-intermediate-parsedinput-shape", title: "Build from an intermediate ParsedInput shape"}
  - {anchor: "validate-what-you-built", title: "Validate what you built"}
  - {anchor: "next-steps", title: "Next steps"}
---

> Documentation index: https://test.abhinandan.one/llms.txt

# 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](https://test.abhinandan.one/node-sdk-receiving-email.md), and see the [Inbound and Outbound Email Model](https://test.abhinandan.one/email-model.md) 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](https://test.abhinandan.one/webhook-events.md).

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](https://test.abhinandan.one/node-sdk-contract-module/node-sdk-contract-types.md) (`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.

```bash
npm install @primitivedotdev/sdk
```

### 2. Import the builder and its input type

```typescript
// 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](https://test.abhinandan.one/node-sdk-contract-module/node-sdk-contract-types.md); build your input against those types so the compiler tells you what's missing.

### 3. Build the event

```typescript
// 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. Serialize it as a webhook body

```typescript
// 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.

```typescript
// 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](https://test.abhinandan.one/node-sdk-contract-module/node-sdk-contract-types.md).

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

```typescript
// 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.
