---
title: "Agent Guide"
canonical: "https://test.abhinandan.one/agent-guide-e8fc0998"
markdown_url: "https://test.abhinandan.one/agent-guide-e8fc0998.md"
publisher: "Primitive SDKs"
kind: "agent_guide"
content_type: "reference"
category: "Getting Started"
description: "Lists install commands, canonical send/reply/receive and x402 API shapes, codegen rules, and verification commands for agents integrating Primitive."
keywords: ["primitive.receive", "client.reply", "x402 charge pay", "make node-generate python-generate go-generate", "PRIMITIVE_API_KEY", "handleWebhookEvent"]
last_modified: "2026-08-11T18:55:04.302305+00:00"
published_at: "2026-08-11T18:55:03.940307+00:00"
source_files:
  - "docs/architecture.md"
  - "RELEASE.md"
sections:
  - {anchor: "fastest-successful-path", title: "Fastest successful path"}
  - {anchor: "install-and-authenticate", title: "Install and authenticate"}
  - {anchor: "canonical-api-shapes", title: "Canonical API shapes"}
  - {anchor: "receive-reply-send-forward-every-sdk", title: "Receive, reply, send, forward: every SDK"}
  - {anchor: "webhook-verification-and-event-dispatch", title: "Webhook verification and event dispatch"}
  - {anchor: "email-authenticity-and-sender-trust", title: "Email authenticity and sender trust"}
  - {anchor: "x402-payments-every-sdk", title: "x402 payments: every SDK"}
  - {anchor: "cli-command-surface", title: "CLI command surface"}
  - {anchor: "generated-api-client-advanced-surface", title: "Generated API client (advanced surface)"}
  - {anchor: "verification-commands", title: "Verification commands"}
  - {anchor: "codegen-rules-read-before-touching-generated-files", title: "Codegen rules (read before touching generated files)"}
  - {anchor: "release-conventions-if-your-pr-bumps-a-version", title: "Release conventions (if your PR bumps a version)"}
  - {anchor: "pitfalls-and-do-not-use-list", title: "Pitfalls and do-not-use list"}
---

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

# 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

| Situation | Approach | First command |
|---|---|---|
| First-time integration, no stack constraint | Node.js SDK (flagship path) | `npm install @primitivedotdev/sdk` |
| Existing Python codebase | Python SDK | `pip install primitivedotdev` |
| Existing Go codebase | Go SDK | `go get github.com/primitivedotdev/sdks/sdk-go@latest` |
| Terminal / CI / deploy workflows, not app code | CLI | `npm install -g primitive` |
| You changed `openapi/primitive-api.yaml` or the webhook schema | Regenerate all three SDKs | `make node-generate python-generate go-generate` |
| You need to verify a webhook without a framework `Request` object | Low-level signature helper | See [Webhook Signature Verification](https://test.abhinandan.one/node-sdk-webhook-signing.md) |
| You need payments over email instead of an out-of-band challenge id | Email-native x402 flow | See [Email-Native x402 Payments](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-email.md) |

Full product model: [What is Primitive?](https://test.abhinandan.one/what-is-primitive.md). Full onboarding walkthrough: [Quickstart](https://test.abhinandan.one/quickstart.md).

## Install and authenticate

**Choose one of the following:**

**Node.js**

```bash
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.

**Python**

```bash
pip install primitivedotdev
export PRIMITIVE_API_KEY=prim_test
```

Requires Python 3.10+. Import name is `primitive`.

**Go**

```bash
go get github.com/primitivedotdev/sdks/sdk-go@latest
export PRIMITIVE_API_KEY=prim_test
```

Requires Go 1.25+. Module path: `github.com/primitivedotdev/sdks/sdk-go`.

**CLI**

```bash
npm install -g primitive
# or: npx primitive@latest <command>
primitive signin
primitive whoami
```

Also published as `primcli` and the legacy `@primitivedotdev/cli`; all three track the same version.

## Canonical API shapes

### Receive, reply, send, forward: every SDK

| SDK | Receive | Reply | Send | Forward |
|---|---|---|---|---|
| Node | `primitive.receive(req, { secret })` | `client.reply(email, input)` | `client.send(input)` | `client.forward(email, input)` |
| Python | `primitive.receive(body=, headers=, secret=)` | `client.reply(email, text_or_dict)` | `client.send(**kwargs)` | `client.forward(email, to=, body_text=)` |
| Go | `primitive.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](https://test.abhinandan.one/email-model.md). SDK-specific detail: [Sending, Replying, and Forwarding Email](https://test.abhinandan.one/node-sdk-sending-email.md) (Node), [Sending Email](https://test.abhinandan.one/python-send-email.md) / [Replying and Forwarding](https://test.abhinandan.one/python-reply-forward.md) (Python), [Sending Emails](https://test.abhinandan.one/go-sending-emails.md) / [Replying to Emails](https://test.abhinandan.one/go-replying-to-emails.md) / [Forwarding Emails](https://test.abhinandan.one/go-forwarding-emails.md) (Go).

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

```ts
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

| Task | Node | Python | Go |
|---|---|---|---|
| Verify + normalize inbound email in one call | `primitive.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` only | `handleWebhook(...)` | `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 scheme | `verifyStandardWebhooksSignature(...)` | `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](https://test.abhinandan.one/webhook-events.md).

Event type catalog (identical across all SDKs):

```text
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

| Task | Node | Python | Go |
|---|---|---|---|
| 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 domain | `isTrustedSender(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](https://test.abhinandan.one/node-sdk-email-authenticity.md).

### x402 payments: every SDK

| Step | Node | Python | Go |
|---|---|---|---|
| Construct client | `createX402Client({ 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: issue | `x402.createEmailChallenge({ from, to, amountUsdc, network })` | `x402.create_email_challenge(from_=, to=, amount_usdc=, network=)` | `client.CreateEmailChallenge(ctx, X402EmailChargeInput{...})` |
| Email-native: extract from inbound part | `parseEmailChallengeFromPart(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/update | `x402.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](https://test.abhinandan.one/x402-payments-overview.md).

### CLI command surface

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

Full command reference: [What is the Primitive CLI?](https://test.abhinandan.one/cli-overview.md).

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

| SDK | Construct | Example call |
|---|---|---|
| Node | `new PrimitiveApiClient({ apiKey })` from `@primitivedotdev/sdk/api` | `getAccount({ client: api.client })` |
| Python | `create_client("prim_test")` from `primitive.api` | `get_account(client=client)` |
| Go | `primitiveapi.NewAPIClient("prim_test")` from `sdk-go/api` | `client.SetMemory(ctx, ...)` |

Reference: [Generated API Client and Primitive Memories](https://test.abhinandan.one/node-sdk-api-client.md) (Node), [Generated API Client](https://test.abhinandan.one/python-generated-api-client.md) (Python).

## Verification commands

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

```bash
# 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)

| Rule | Detail |
|---|---|
| Never edit generated files directly | `openapi/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.1 | Edit `openapi/primitive-api.yaml` only; it is normalized to 3.0.3 JSON for the code generators, see [OpenAPI Spec Normalization and Codegen Artifacts](https://test.abhinandan.one/openapi-spec-normalization.md) |
| After any spec or JSON Schema change | Run `make node-generate python-generate go-generate` from repo root, then commit the regenerated files alongside the source change |
| Single-language iteration | `pnpm --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 truth | `json-schema/email-received-event.schema.json`, propagates to TypeScript types, an AJV validator, and Go/Python model modules; see [Webhook Schema Codegen](https://test.abhinandan.one/webhook-schema-codegen.md) |
| api-core is never published | `@primitivedotdev/api-core` is workspace-internal; `sdk-node` and `cli-node` bundle it inline, see [What is API Core?](https://test.abhinandan.one/api-core-overview.md) |

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)

| Package | Version file | Trigger |
|---|---|---|
| Node SDK (`@primitivedotdev/sdk`) | `sdk-node/package.json` | Merge to `main` with a version bump publishes via OIDC |
| CLI (`primitive`, mirrored as `primcli` / `@primitivedotdev/cli`) | `cli-node/package.json` | Same as above; mirrors publish in lockstep |
| Python (`primitivedotdev`) | `sdk-python/pyproject.toml` | Same as above |
| Go (`github.com/primitivedotdev/sdks/sdk-go`) | `sdk-go/VERSION` | Creates 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.
