
Security News
npm Tooling Bug Incorrectly Marks One-Character Packages as Security Holders
npm confirmed a tooling bug incorrectly marked several one-character packages as security holders and said it was working on a rollback.
@primitivedotdev/sdk
Advanced tools
Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser runtime modules.
@primitivedotdev/sdkThe 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.
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.
npm install @primitivedotdev/sdk
Requires Node.js 22 or newer.
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_...
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";
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.
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.
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.
wait modeWhen wait: true, the call returns the first downstream SMTP outcome (or waitTimeoutMs, default 30000). Possible terminal deliveryStatus values:
delivered accepted by the receiving MTAbounced rejected by the receiving MTA (the response is still 200 OK)deferred temporary failure, the receiving MTA may retrywait_timeout no outcome was observed in time. Treat as "outcome unknown." The send may still complete after the response returns.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",
});
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.
await client.forward(email, {
to: "ops@example.com",
bodyText: "Can you take this one?",
});
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.
receive formIf 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!,
});
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 });
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:
Primitive-Signature: t=<unix-seconds>,v1=<hex>. A legacy MyMX-Signature header carries the same value for back-compat.${timestamp}.${rawBody} where rawBody is the exact request bytes before any JSON decoding.GET /account/webhook-secret. Use as a UTF-8 string; do not base64-decode despite the base64-shaped output.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.
@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.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.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
FAQs
Official Primitive Node.js SDK: webhook, api, openapi, contract, and parser runtime modules.
We found that @primitivedotdev/sdk demonstrated a healthy version release cadence and project activity because the last version was released less than a year ago. It has 2 open source maintainers collaborating on the project.
Did you know?

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.

Security News
npm confirmed a tooling bug incorrectly marked several one-character packages as security holders and said it was working on a rollback.

Research
/Security News
Newer packages in this compromise use native extensions and .pth loaders to execute JavaScript stealers in developer environments.

Research
Socket found 37 malicious PyPI wheels that abuse Python startup hooks to launch a Bun-powered credential stealer tied to Mini Shai-Hulud/Miasma.