Big News: Socket raises $60M Series C at a $1B valuation to secure software supply chains for AI-driven development.Announcement
Sign In

@primitivedotdev/sdk

Package Overview
Dependencies
Maintainers
2
Versions
63
Alerts
File Explorer

Advanced tools

Socket logo

Install Socket

Detect and block malicious and high-risk dependencies

Install

@primitivedotdev/sdk

Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser runtime modules.

Source
npmnpm
Version
1.1.0
Version published
Weekly downloads
1.1K
-59.71%
Maintainers
2
Weekly downloads
 
Created
Source

@primitivedotdev/sdk

The official Node.js library for Primitive, an email API for sending and receiving programmatic mail. Typed client for receiving and verifying inbound webhooks, sending mail, parsing raw MIME, and calling the full HTTP API.

Looking for the CLI?

The primitive CLI ships as a separate package, @primitivedotdev/cli. Install it with:

npm install -g @primitivedotdev/cli
# or, no-install:
npx @primitivedotdev/cli@latest <command>

This package no longer ships a primitive bin. Install @primitivedotdev/cli to get the CLI.

Install

npm install @primitivedotdev/sdk

Requires Node.js 22 or newer.

Set your API key

Get a key from your dashboard and export it. The library defaults to reading PRIMITIVE_API_KEY from the environment.

export PRIMITIVE_API_KEY=prim_...

Library

The default root import is intentionally small and centered on the two most common app-code use cases: receiving inbound webhook deliveries and sending mail.

import primitive from "@primitivedotdev/sdk";

Receive and reply in a Next.js route

import primitive from "@primitivedotdev/sdk";

export const runtime = "nodejs";
export const maxDuration = 300;

const client = primitive.client({
  apiKey: process.env.PRIMITIVE_API_KEY!,
});

export async function POST(req: Request) {
  const email = await primitive.receive(req, {
    secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
  });

  await client.reply(email, "Thank you for your email.");

  return Response.json({ ok: true });
}

primitive.receive(...) reads the request body, verifies the HMAC-SHA256 signature against your account secret (rejecting expired or tampered deliveries), and returns a normalized email object. client.reply(email, ...) derives threading and the Re: subject from the parent message server-side.

Send a new email

import primitive from "@primitivedotdev/sdk";

const client = primitive.client({
  apiKey: process.env.PRIMITIVE_API_KEY!,
});

const result = await client.send({
  from: "Support <support@example.com>",
  to: "alice@example.com",
  subject: "Hello",
  bodyText: "Hi there",
  wait: true,
  waitTimeoutMs: 5000,
});

console.log(result.id, result.status, result.queueId, result.deliveryStatus);

send, reply, and forward keep the HTTP request open until Primitive's downstream SMTP transaction completes. In production, configure your runtime or transport with a request timeout long enough for SMTP delivery, typically 30 to 60 seconds.

Per-call request options

Every client method accepts an optional second argument with cancellation, timeout, header, and idempotency controls:

interface RequestOptions {
  // Cancel the in-flight request when this signal fires. Surfaces as AbortError.
  signal?: AbortSignal;
  // Per-call timeout in milliseconds. Composed with `signal` so either fires.
  timeout?: number;
  // Per-call headers merged on top of client-level headers. Last write wins.
  headers?: Record<string, string>;
  // Idempotency key for safe retries. Sent as the Idempotency-Key request header.
  idempotencyKey?: string;
}

Cap a single send at 15 seconds:

await client.send(
  { from, to, subject, bodyText },
  { signal: AbortSignal.timeout(15000) },
);

Idempotency key for safe retries (reusing a key returns the original response, so a retried network call deduplicates against the first send):

await client.send(
  { from, to, subject, bodyText },
  { idempotencyKey: "customer-key-abc123" },
);

Client-level config (default fetch, base URL, default headers passed to primitive.client({...})) still applies to every call. Per-call RequestOptions overrides or merges on top: headers merge (per-call wins on conflict), signal and timeout compose so the first to fire wins.

About wait mode

When wait: true, the call returns the first downstream SMTP outcome (or waitTimeoutMs, default 30000). Possible terminal deliveryStatus values:

  • delivered accepted by the receiving MTA
  • bounced rejected by the receiving MTA (the response is still 200 OK)
  • deferred temporary failure, the receiving MTA may retry
  • wait_timeout no outcome was observed in time. Treat as "outcome unknown." The send may still complete after the response returns.

Reply from a different address

reply() defaults the From address to the inbound recipient (the address that received the email). When your verified outbound domain differs from your inbound domain, pass from explicitly:

await client.reply(email, {
  text: "Thanks for your email.",
  from: "notifications@outbound.example.com",
});

HTML replies and waiting on the delivery outcome

reply() accepts html as a sibling of text, plus the same wait flag the top-level send() takes:

await client.reply(email, {
  text: "Thanks for your email.",
  html: "<p>Thanks for your email.</p>",
  attachments: [
    {
      filename: "report.txt",
      content_base64: Buffer.from("hello").toString("base64"),
    },
  ],
  wait: true,
});

subject is intentionally not accepted on reply(). Gmail's Conversation View needs both a References match and a normalized-subject match to thread, so a custom subject silently breaks the thread for half the recipient population. Use client.send(...) if you need full subject control.

If the inbound row is not in a state we can reply to (no Message-Id recorded, or content was discarded), the API returns inbound_not_repliable (HTTP 422) and the SDK throws.

Forward an inbound email

await client.forward(email, {
  to: "ops@example.com",
  bodyText: "Can you take this one?",
});

The normalized email object

primitive.receive(...) returns a normalized inbound email object that keeps the common case clean:

email.sender.address;
email.sender.name;

email.receivedBy;
email.receivedByAll;

email.replyTarget.address;
email.replySubject;
email.forwardSubject;

email.subject;
email.text;

email.thread.messageId;
email.thread.references;

email.raw;

Use email.raw when you need the original validated webhook event shape.

Lower-level surfaces

Explicit receive form

If your framework does not expose a standard Request, use the lower-level form:

const email = primitive.receive({
  body: req.body,
  headers: req.headers,
  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
});

Generated API client

The full HTTP API is exposed as a generated client. Use it when the high-level helpers don't cover what you need:

import { PrimitiveApiClient, getAccount } from "@primitivedotdev/sdk/api";

const api = new PrimitiveApiClient({ apiKey: process.env.PRIMITIVE_API_KEY });
const result = await getAccount({ client: api.client });

Webhook signature verification

primitive.receive(...) handles verification automatically. If you need to verify a delivery yourself (a different language reverse-proxying through Node, a one-off audit, etc.), the wire format is:

  • Header: Primitive-Signature: t=<unix-seconds>,v1=<hex>. A legacy MyMX-Signature header carries the same value for back-compat.
  • Signed string: ${timestamp}.${rawBody} where rawBody is the exact request bytes before any JSON decoding.
  • Signature: HMAC-SHA256, hex-encoded.
  • Secret: returned by GET /account/webhook-secret. Use as a UTF-8 string; do not base64-decode despite the base64-shaped output.
  • Tolerance: reject deliveries with a timestamp more than 5 minutes off your wall-clock.

The Node helper:

import { verifyWebhookSignature } from "@primitivedotdev/sdk/webhook";

verifyWebhookSignature({
  rawBody: rawBodyString,
  signatureHeader: req.headers["primitive-signature"] as string,
  secret: process.env.PRIMITIVE_WEBHOOK_SECRET!,
});

rawBody must be the exact bytes of the HTTP body (string or Buffer) before any JSON parsing. signatureHeader is the value of the Primitive-Signature header verbatim. Throws WebhookVerificationError on mismatch, expired timestamp, or malformed input. Pass toleranceSeconds to override the default 300-second replay window.

For most app-code callers, primitive.receive(...) from the root import handles both the body extraction and verification in one call (see "Receive and reply in a Next.js route" above). Reach for verifyWebhookSignature directly when your framework doesn't expose a standard Request and you've already pulled the raw body and header value yourself.

For the full reference (response codes, replay protection details), see the API-level "Webhook signing" section in the OpenAPI spec.

Other subpath imports

  • @primitivedotdev/sdk/openapi exports the OpenAPI document and the operation manifest as JSON. Useful for tools that want the spec inline.
  • @primitivedotdev/sdk/contract builds and signs webhook payloads. Useful for tests or replaying inbound events through your own handler.
  • @primitivedotdev/sdk/parser parses raw .eml files and bundles attachments. Useful when you receive inbound mail through a different path (forwarded .eml files, archived storage) and want the same normalization the webhook receiver applies.

Going further

  • primitive.dev/docs for product docs (quickstart, webhook payload reference, FAQ).
  • api.primitive.dev/v1/openapi for the machine-readable OpenAPI spec.
  • primitive list-operations for the same spec as a JSON manifest, fetched from the bundled SDK.
  • primitive describe <command> for the inlined request/response schema of a single operation, including per-field descriptions.

Development

From sdks/sdk-node:

pnpm install
pnpm generate
pnpm typecheck
pnpm test
pnpm build

Or from repo root sdks/:

make node-generate
make node-check
make node-build

Keywords

primitive

FAQs

Package last updated on 08 Jun 2026

Did you know?

Socket

Socket for GitHub automatically highlights issues in each pull request and monitors the health of all your open source dependencies. Discover the contents of your packages and block harmful activity before you install or update your dependencies.

Install

Related posts