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

Agent Guide

A single-page reference for AI coding agents integrating Primitive: install commands, canonical API shapes across all three SDKs and the CLI, codegen rules, and repo conventions to follow before opening a PR.

This page is a dense reference for AI coding agents integrating Primitive. It gives you install commands, canonical API shapes across all three SDKs and the CLI, codegen rules, and repo conventions. Read it instead of crawling every language-specific page.

Fastest successful path#

SituationApproachFirst command
First-time integration, no stack constraintNode.js SDK (flagship path)npm install @primitivedotdev/sdk
Existing Python codebasePython SDKpip install primitivedotdev
Existing Go codebaseGo SDKgo get github.com/primitivedotdev/sdks/sdk-go@latest
Terminal / CI / deploy workflows, not app codeCLInpm install -g primitive
You changed openapi/primitive-api.yaml or the webhook schemaRegenerate all three SDKsmake node-generate python-generate go-generate
You need to verify a webhook without a framework Request objectLow-level signature helperSee Webhook Signature Verification
You need payments over email instead of an out-of-band challenge idEmail-native x402 flowSee Email-Native x402 Payments

Full product model: What is Primitive?. Full onboarding walkthrough: Quickstart.

Install and authenticate#

npm install @primitivedotdev/sdk
export PRIMITIVE_API_KEY=prim_test
export PRIMITIVE_WEBHOOK_SECRET=whsec_...

Requires Node.js 22+. This package no longer ships a primitive bin; install primitive separately for the CLI.

Canonical API shapes#

Receive, reply, send, forward: every SDK#

SDKReceiveReplySendForward
Nodeprimitive.receive(req, { secret })client.reply(email, input)client.send(input)client.forward(email, input)
Pythonprimitive.receive(body=, headers=, secret=)client.reply(email, text_or_dict)client.send(**kwargs)client.forward(email, to=, body_text=)
Goprimitive.Receive(HandleWebhookOptions{...})client.Reply(ctx, email, ReplyParams{...})client.Send(ctx, SendParams{...})client.Forward(ctx, email, ForwardParams{...})

Full explanation of the normalized ReceivedEmail, wait mode, and delivery statuses: Inbound and Outbound Email Model. SDK-specific detail: Sending, Replying, and Forwarding Email (Node), Sending Email / Replying and Forwarding (Python), Sending Emails / Replying to Emails / Forwarding Emails (Go).

Node canonical snippet (the shape every quickstart converges on):

import primitive from "@primitivedotdev/sdk";

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 });
}

Rules for every send/reply call:

  • subject is never accepted on reply/Reply; a custom subject breaks Gmail's Conversation View threading. Use send/Send for full subject control.
  • Default (no wait): returns as soon as Primitive accepts the message. Pass wait: true (Node), wait=True (Python), or a *bool pointing to true on Wait (Go) only when you need the terminal delivery status (delivered / bounced / deferred / wait_timeout) before responding. In that mode set a request timeout long enough for SMTP delivery, typically 30-60 seconds (waitTimeoutMs / wait_timeout_ms / WaitTimeoutMs defaults to 30000 ms and must be between 1000 and 30000).
  • inbound_not_repliable (HTTP 422) means the inbound row cannot be replied to (rejected at ingestion, discarded content, or no recipient recorded).
  • Idempotency: pass idempotencyKey / idempotency_key / IdempotencyKey on send/forward. Reusing a key replays the original response.

Webhook verification and event dispatch#

TaskNodePythonGo
Verify + normalize inbound email in one callprimitive.receive(req, { secret })primitive.receive(body=, headers=, secret=)primitive.Receive(HandleWebhookOptions{...})
Verify + parse full event union (email/payment/interaction)handleWebhookEvent({ body, headers, secret })handle_webhook_event(body=, headers=, secret=)primitive.HandleWebhookEvent(HandleWebhookOptions{...})
Legacy, hard-typed to email.received onlyhandleWebhook(...)handle_webhook(...)primitive.HandleWebhook(...)
Manual signature check (no standard Request)verifyWebhookSignature({ rawBody, signatureHeader, secret })verify_webhook_signature(raw_body=, signature_header=, secret=)primitive.VerifyWebhookSignature(VerifyOptions{...})
Standard Webhooks alt schemeverifyStandardWebhooksSignature(...)verify_standard_webhooks_signature(...)primitive.VerifyStandardWebhooksSignature(...)

Signature wire format: header Primitive-Signature: t=<unix-seconds>,v1=<hex> (legacy MyMX-Signature mirrors the same value); signed string is ${timestamp}.${rawBody}; HMAC-SHA256 hex; secret from GET /account/webhook-secret used as raw UTF-8 (never base64-decode it); default tolerance 300 seconds. Event family discriminator is the X-Webhook-Event header, not a body field, full contract at Webhook Events Overview.

Event type catalog (identical across all SDKs):

email.received, email.bounced, email.tls_report, email.dmarc_report, email.dmarc_failure
payment.settled, payment.failed
interaction.x402.challenge, interaction.x402.payment, interaction.x402.settled,
interaction.x402.rejected, interaction.x402.declined, interaction.x402.expired,
interaction.x402.verify_timeout
interaction.ack.received, interaction.ack.requested, interaction.ack.acked,
interaction.ack.canceled, interaction.ack.expired

Email authenticity and sender trust#

TaskNodePythonGo
Compute bare verdict (legit/suspicious/unknown)validateEmailAuth(event.email.auth)validate_email_auth(event.email.auth)primitive.ValidateEmailAuth(input)
Anchor verdict to an expected From domainisTrustedSender(email.raw, { domain })is_trusted_sender(email.raw, domain=)primitive.IsTrustedSender(email.Raw, TrustedSenderOptions{Domain:})

validateEmailAuth's legit verdict does not say which domain authenticated: a fully authenticated email from any domain (including an attacker's) returns legit. Use the domain-anchored trust check for authorization decisions. Never gate on email.replyTarget / email.reply_target / email.ReplyTarget or the raw SMTP envelope sender; both are sender-controlled. Details: Verifying Inbound Email Authenticity.

x402 payments: every SDK#

StepNodePythonGo
Construct clientcreateX402Client({ apiKey })create_x402_client(api_key=)primitive.NewX402Client(X402ClientOptions{...})
Register payout address (payee, once)x402.registerPayoutAddress({ network, label }, { signer })x402.register_payout_address(signer=, network=, label=)client.RegisterPayoutAddress(ctx, input, signer)
Create challenge (payee)x402.charge({ amountUsdc, network })x402.charge(amount_usdc=, network=)client.Charge(ctx, X402ChargeInput{AmountUsdc:, Network:})
Pay challenge (payer)x402.pay(challenge, { signer })x402.pay(challenge, signer=)client.Pay(ctx, challenge, signer)
Email-native: issuex402.createEmailChallenge({ from, to, amountUsdc, network })x402.create_email_challenge(from_=, to=, amount_usdc=, network=)client.CreateEmailChallenge(ctx, X402EmailChargeInput{...})
Email-native: extract from inbound partparseEmailChallengeFromPart(part)extract_email_challenge(part)primitive.ExtractEmailChallenge(part)
Email-native: sign (no send)x402.payEmailChallenge(issued, { signer })x402.pay_email_challenge(issued, signer=)client.PayEmailChallenge(issued, signer)
Spend policy read/updatex402.getSpendPolicy() / setSpendPolicy(...)x402.get_spend_policy() / set_spend_policy(...)client.SetSpendPolicy(ctx, update)

Amounts: pass amountUsdc/amount_usdc/AmountUsdc as a human decimal string ("0.01"), the documented easy path. Set exactly one of that or amount/Amount in raw base units ("10000"; USDC has 6 decimals). Networks: base-sepolia (testnet, used in examples) or base (mainnet). Every method throws or returns X402Error carrying status (0 = the request may never have reached the server, so on pay() the outcome is indeterminate), body, and retryAfter. Full model: x402 Payments Overview.

CLI command surface#

TaskCommand
Authprimitive signin / login / signup / logout --force / whoami
Send / replyprimitive send --to ... --body ... / primitive reply --id <email-id> --body ...
List mailprimitive emails list / primitive emails latest --limit 5 / primitive emails get --id <inbound-email-id>
Deploy a functionprimitive functions init my-fn && primitive functions deploy --name my-fn --file ./dist/handler.js
Recipient routingprimitive routes add alice@acme.com --function <id>
Memoriesprimitive memories set thread:latest '{"email_id":"em_123"}' / get / search thread: / delete
x402 paymentsprimitive payments charge --network base-sepolia --amount-usdc 0.01 / primitive payments pay --challenge-file challenge.json
Direct API accessprimitive <tag>:<operation>, e.g. primitive emails:list-emails

Full command reference: What is the Primitive CLI?.

Generated API client (advanced surface)#

Use the high-level send/reply/receive surface for app code; drop to the generated client only for operations it doesn't cover (Memories, semantic search, account/domain management).

SDKConstructExample call
Nodenew PrimitiveApiClient({ apiKey }) from @primitivedotdev/sdk/apigetAccount({ client: api.client })
Pythoncreate_client("prim_test") from primitive.apiget_account(client=client)
Goprimitiveapi.NewAPIClient("prim_test") from sdk-go/apiclient.SetMemory(ctx, ...)

Reference: Generated API Client and Primitive Memories (Node), Generated API Client (Python).

Verification commands#

Run these after wiring an integration to confirm it works end to end.

# Node: typecheck + test suite
cd sdk-node && pnpm typecheck && pnpm test

# Python: dependency sync + tests + lint + typecheck
cd sdk-python && uv sync --dev && uv run pytest && uv run ruff check . && uv run basedpyright

# Go: unit tests + the cross-SDK shared compatibility fixtures
cd sdk-go && go test ./... && go test -run TestSharedCompatibilityFixtures ./...

# Repo root: run every SDK's checks through the shared task interface
make check
make shared-check

# CLI: confirm the packed build works and lists operations
primitive list-operations

The shared fixtures in test-fixtures/ pin behavioral parity across all three SDKs: schema validation outcomes, signature verification, auth classification, domain-anchored sender trust, raw-content helpers, and parseWebhookEvent/handleWebhook behavior. make shared-check runs the cross-SDK compatibility checks; in Go the suite is go test -run TestSharedCompatibilityFixtures ./.... If you touch webhook or auth logic in one SDK, run all three suites before committing.

Codegen rules (read before touching generated files)#

RuleDetail
Never edit generated files directlyopenapi/primitive-api.codegen.json, sdk-node/src/api/** (openapi-ts output), sdk-python/src/primitive/api/**, sdk-go/api/** are all build artifacts
Author spec as OpenAPI 3.1Edit openapi/primitive-api.yaml only; it is normalized to 3.0.3 JSON for the code generators, see OpenAPI Spec Normalization and Codegen Artifacts
After any spec or JSON Schema changeRun make node-generate python-generate go-generate from repo root, then commit the regenerated files alongside the source change
Single-language iterationpnpm --dir sdk-node generate (or the Python/Go equivalents) is fine while iterating, but the final commit must include all three regenerated outputs
Webhook schema source of truthjson-schema/email-received-event.schema.json, propagates to TypeScript types, an AJV validator, and Go/Python model modules; see Webhook Schema Codegen
api-core is never published@primitivedotdev/api-core is workspace-internal; sdk-node and cli-node bundle it inline, see What is API Core?

Change-strategy checklist when shared webhook behavior changes (from docs/architecture.md):

  1. Update the canonical schema if the payload contract changed.
  2. Regenerate language-specific artifacts.
  3. Update test-fixtures/ when expected behavior changes.
  4. Run make check.
  5. Review each SDK for language-specific helper implications.

Release conventions (if your PR bumps a version)#

PackageVersion fileTrigger
Node SDK (@primitivedotdev/sdk)sdk-node/package.jsonMerge to main with a version bump publishes via OIDC
CLI (primitive, mirrored as primcli / @primitivedotdev/cli)cli-node/package.jsonSame as above; mirrors publish in lockstep
Python (primitivedotdev)sdk-python/pyproject.tomlSame as above
Go (github.com/primitivedotdev/sdks/sdk-go)sdk-go/VERSIONCreates a subdirectory-prefixed tag sdk-go/vX.Y.Z

Do not hand-edit npm tokens into workflows; npm publishing uses trusted publishing/OIDC per RELEASE.md.

Pitfalls and do-not-use list#

  • Do not base64-decode the webhook secret from GET /account/webhook-secret. It looks base64-shaped but must be used as a raw UTF-8 string for the HMAC key.
  • Do not pass a custom subject to reply/Reply. The field is intentionally rejected or absent; use send/Send.
  • Do not authorize actions based on email.sender/email.Sender, email.replyTarget/ReplyTarget, or the SMTP envelope mail_from. None are safe anchors; use the domain-anchored trust check.
  • Do not treat an unknown auth verdict as always retryable. Check retryable explicitly: a domain with no DMARC record is a permanent unknown, not a transient one.
  • Do not install @primitivedotdev/sdk expecting a primitive CLI bin. It no longer ships one; install the primitive package.
  • Do not edit openapi/primitive-api.codegen.json or any file under a generated api/ directory. Edit the OpenAPI 3.1 source and regenerate.
  • Do not use handleWebhook/handle_webhook/HandleWebhook if you need payment.* or interaction.* events. It is hard-typed to email.received; use the *WebhookEvent variant.
  • Do not hand-set validBefore/valid_before/ValidBefore on an x402 signing call unless you have a specific reason. computePaymentValidityWindow clamps into the platform-accepted band (60s minimum settlement headroom, 24h maximum window) automatically.
  • Do not send a private key over the network. Every x402 signer (PrivateKeySigner, viem LocalAccount) signs locally; only the signature and address ever leave the process.

Was this page helpful?

© Primitive SDKs

Powered by Browzer