Cookbook
Primitive Cookbook: Inbound/Outbound Email & CLI Recipes
Copy-paste TypeScript and CLI recipes for Primitive: receive inbound email, reply/forward synchronously, and explore the primitive/prim CLI and x402 payments.
Primitive is an inbound and outbound email platform: you receive an email, inspect a normalized email object, and send, reply, or forward it synchronously in the same request. This cookbook covers the default primitive workflow in TypeScript, installing the package, wiring a webhook handler that replies and forwards automatically, and using the primitive/prim CLI to discover commands and the non-custodial x402 payments flow.
Every recipe below uses only the API shape and CLI surface documented for this package: primitive.receive(...), primitive.client(...), client.reply(...), client.forward(...), and the primitive/prim binaries with their payments command group.
How to install primitive and set your API credentials#
Before you can receive or send mail with Primitive, you need the package installed in your project and your API key and webhook secret available as environment variables.
Prerequisites
- Node.js project with npm
- A Primitive API key from your dashboard
- A Primitive webhook secret for the inbound endpoint you'll create
npm install primitive
# .env (local project) — used by primitive.client() and primitive.receive()
export PRIMITIVE_API_KEY="sk_live_your_key_here"
export PRIMITIVE_WEBHOOK_SECRET="whsec_your_secret_here"
# to get the CLI binaries (primitive and prim) on your PATH:
npm install -g primitive
npm install primitive adds the package to your project so you can import primitive from "primitive" in code. This local install does not put the primitive or prim binaries on your shell PATH, for that you need a global install (npm install -g primitive), which is how you'll run the CLI recipes later in this cookbook.
PRIMITIVE_API_KEY authenticates the outbound client you create with primitive.client(...). PRIMITIVE_WEBHOOK_SECRET is checked by primitive.receive(...) to verify that an inbound payload actually came from Primitive.
Expected output
`npm ls primitive` prints the installed version in your project's dependency tree. After the global install, `primitive --version` and `prim --version` both print a version string from your shell.
Gotchas
- A local install (
npm install primitive) gives you the importable module, not the CLI on your PATH, you need the-gflag for that. - Both PRIMITIVE_API_KEY and PRIMITIVE_WEBHOOK_SECRET must be set before you call client() or receive(), or the non-null assertions in the webhook handler will throw at runtime.
How to receive an inbound email and reply to it automatically#
You want a webhook endpoint that verifies an incoming Primitive email payload and sends an automatic reply in the same request, without a separate polling step.
Prerequisites
- primitive installed (see previous recipe)
- PRIMITIVE_API_KEY and PRIMITIVE_WEBHOOK_SECRET set
- An inbound webhook configured in your Primitive dashboard pointing at this route
import primitive from "primitive";
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(req, { secret }) reads and verifies the incoming webhook Request, checking it against PRIMITIVE_WEBHOOK_SECRET, and resolves to a normalized email object. primitive.client({ apiKey }) creates a reusable outbound client, construct it once at module scope, not per-request, so it can be shared across invocations.
client.reply(email, "Thank you for your email.") sends a reply into the same thread as the inbound message, addressed back to the original sender. The handler returns a JSON acknowledgement so Primitive knows the webhook was processed.
Expected output
The route responds with `{"ok":true}` and HTTP 200. The original sender receives a reply email in the same thread with the body "Thank you for your email."
Gotchas
- If the secret passed to receive() doesn't match the webhook secret configured in your Primitive dashboard, verification fails, double check you're reading the same PRIMITIVE_WEBHOOK_SECRET value the dashboard has on file.
- Construct the client with primitive.client({ apiKey }) once outside the handler; recreating it on every request works but wastes setup on each invocation.
- This handler assumes a Web-standard Request/Response API (e.g. Next.js route handlers), adapt the function signature if your framework passes a different request object.
How to forward an inbound email to another address#
You want to hand off a received email to a different recipient, for example escalating it to a support or ops address, without manually re-composing the message.
Prerequisites
- primitive installed
- PRIMITIVE_API_KEY and PRIMITIVE_WEBHOOK_SECRET set
- An inbound webhook route already receiving mail (see previous recipe)
import primitive from "primitive";
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.forward(email, "escalations@example.com");
return Response.json({ ok: true });
}
client.forward(email, "escalations@example.com") takes the normalized email object you got from primitive.receive(...) and forwards it on to the given address, the same way client.reply(email, text) replies into the original thread but sends to a new recipient instead of back to the sender.
Because forward operates on the object produced by receive, you must call it inside the same request that received the webhook, there's no separate lookup-by-ID step shown in this flow.
Expected output
The route responds with `{"ok":true}` and HTTP 200. The address escalations@example.com receives the original message forwarded from Primitive.
Gotchas
- forward() needs the email object from this request's receive() call, you can't forward an arbitrary message ID with this API surface.
- Verify the destination address is correct before deploying; a bad forward target silently drops the escalation with no bounce visible in your own logs.
How to discover primitive CLI commands with --help and --version#
You've installed the CLI globally and want to confirm it's working and see what command groups are available before scripting anything against it.
Prerequisites
- Global install: npm install -g primitive
primitive --version
primitive --help
prim --help
primitive and prim are the two binary names shipped by the package, they're equivalent, so pick whichever is shorter to type in your shell. --version confirms the CLI is installed and on your PATH. --help at the root level lists the available command topics, which includes the payments group used for the x402 flow covered in the next recipe.
Expected output
`primitive --version` prints a version string. `primitive --help` and `prim --help` print usage text and a list of topics/commands, including `payments`.
Gotchas
- If
primitiveorprimis not found, the package was installed locally instead of globally, rerunnpm install -g primitive. - Run --help again after any CLI upgrade; command groups and flags can change between versions, and --help always reflects what's actually installed.
How to explore the x402 payments command group before scripting it#
You want to send or receive a non-custodial USDC payment between two agents from the terminal, and need to see the exact subcommands and flags your installed CLI version exposes before wiring it into a script.
Prerequisites
- Global install: npm install -g primitive
- A funded wallet on base or base-sepolia for the paying agent
- A payout address for the receiving agent
primitive payments --help
The primitive payments command group implements Primitive's x402 payments flow from the terminal: one agent registers a payout address and requests a USDC payment, and the paying agent signs the transaction locally with its own key and settles it. Keys never leave the machine that holds them, Primitive's servers never see the paying agent's private key.
Two networks are supported: base and base-sepolia. Amounts are specified in the token's base units rather than decimal notation, USDC has 6 decimals, so "10000" means 0.01 USDC. Run primitive payments --help first on your installed version to see the exact subcommands and flags for registering a payout address, requesting a payment, and settling one, then drill into each with its own --help.
Expected output
Output lists the payments subcommands available in your installed CLI version, with a one-line description of each. Running `primitive payments <subcommand> --help` after that shows the flags for network selection and amount for that specific subcommand.
Gotchas
- Always pass amounts as base-unit strings (e.g. "10000" for 0.01 USDC), not decimal floats, the SDKs and CLI treat amount as raw token base units.
- network must be exactly "base" or "base-sepolia"; mixing testnet and mainnet networks between the requesting and paying agent will fail to settle.
- Because signing happens locally with the paying agent's own key, losing that key means losing the ability to settle, Primitive has no custodial recovery path for it.
FAQ#
What does primitive.receive(req, { secret }) actually verify?#
It reads the inbound webhook request and checks it against the PRIMITIVE_WEBHOOK_SECRET you pass as secret, confirming the payload came from Primitive before resolving to a normalized email object you can act on.
Should I create a new primitive.client() on every request?#
No. Create it once with primitive.client({ apiKey }) at module scope and reuse it across requests; the client instance is what you call reply, send, and forward on.
Why doesn't npm install primitive give me the primitive command in my terminal?#
A plain npm install adds the package as a project dependency for imports only. The primitive and prim binaries are only put on your shell PATH with a global install: npm install -g primitive.
Which blockchain networks does the x402 payments flow support, and how are amounts formatted?#
Only base and base-sepolia are supported. Amounts are strings in the token's base units, not decimals, since USDC has 6 decimals, "10000" represents 0.01 USDC.
Does Primitive ever see the paying agent's private key during an x402 payment?#
No. The payments flow is non-custodial: the paying agent signs the transaction locally with its own key and only the signed, settled transaction leaves that agent's machine.
Key takeaways#
- The default Primitive workflow is receive → inspect → send/reply/forward, all synchronous within one webhook request.
- primitive.client({ apiKey }) is created once and reused; primitive.receive(req, { secret }) verifies and normalizes each inbound payload.
- client.reply(email, text) replies into the original thread; client.forward(email, address) hands the same message to a new recipient.
- npm install primitive gives you the importable module; npm install -g primitive puts the primitive and prim binaries on your PATH.
- x402 payments are non-custodial on base or base-sepolia, with amounts expressed in token base units and keys that never leave the caller.
- Always run --help on the installed CLI version before scripting a command group like payments, since exact subcommands can change between releases.