---
title: "Node.js SDK Errors"
canonical: "https://test.abhinandan.one/node-sdk-errors"
markdown_url: "https://test.abhinandan.one/node-sdk-errors.md"
publisher: "Primitive SDKs"
kind: "troubleshooting"
content_type: "reference"
category: "Troubleshooting"
description: "Every PrimitiveApiError, WebhookVerificationError, WebhookPayloadError, and X402Error code the Node.js SDK throws, with the exact cause and fix."
keywords: ["PrimitiveApiError", "WebhookVerificationError", "WebhookPayloadError", "WebhookValidationError", "X402Error", "SIGNATURE_MISMATCH"]
last_modified: "2026-08-11T18:54:58.526844+00:00"
published_at: "2026-08-11T18:54:58.368305+00:00"
source_files:
  - "sdk-node/src/webhook/index.ts"
  - "sdk-node/src/api/index.ts"
  - "sdk-node/src/x402/client.ts"
sections:
  - {anchor: "primitiveapierror-inbound_not_repliable-http-422", title: "PrimitiveApiError: `inbound_not_repliable` (HTTP 422)"}
  - {anchor: "primitiveapierror-validation-errors-on-sendforward", title: "PrimitiveApiError: validation errors on `send`/`forward`"}
  - {anchor: "primitiveapierror-from-the-generated-api-and-clientmemories", title: "PrimitiveApiError from the generated API and `client.memories`"}
  - {anchor: "clientmemoriesset-rejects-non-json-values", title: "`client.memories.set` rejects non-JSON values"}
  - {anchor: "clientmemories-rejects-the-generated-operation-shape", title: "`client.memories.*` rejects the generated-operation shape"}
  - {anchor: "webhookverificationerror-missing_secret", title: "WebhookVerificationError: `MISSING_SECRET`"}
  - {anchor: "webhookverificationerror-invalid_signature_header", title: "WebhookVerificationError: `INVALID_SIGNATURE_HEADER`"}
  - {anchor: "webhookverificationerror-timestamp_out_of_range", title: "WebhookVerificationError: `TIMESTAMP_OUT_OF_RANGE`"}
  - {anchor: "webhookverificationerror-signature_mismatch", title: "WebhookVerificationError: `SIGNATURE_MISMATCH`"}
  - {anchor: "webhookpayloaderror-payload_empty_body--payload_null--payload_undefined--payload_is_array--payload_wrong_type", title: "WebhookPayloadError: `PAYLOAD_EMPTY_BODY` / `PAYLOAD_NULL` / `PAYLOAD_UNDEFINED` / `PAYLOAD_IS_ARRAY` / `PAYLOAD_WRONG_TYPE`"}
  - {anchor: "webhookpayloaderror-json_parse_failed", title: "WebhookPayloadError: `JSON_PARSE_FAILED`"}
  - {anchor: "webhookpayloaderror-payload_missing_event", title: "WebhookPayloadError: `PAYLOAD_MISSING_EVENT`"}
  - {anchor: "webhookvalidationerror-schema-validation-failed", title: "WebhookValidationError: schema validation failed"}
  - {anchor: "handlewebhook-throws-on-non-emailreceived-events", title: "`handleWebhook` throws on non-`email.received` events"}
  - {anchor: "x402error-status-0-request-never-reached-the-server", title: "X402Error: `status: 0` (request never reached the server)"}
  - {anchor: "x402error-no-api-key-configured", title: "X402Error: \"no API key configured\""}
  - {anchor: "x402error-chargecreateemailchallenge-amount-errors", title: "X402Error: `charge()`/`createEmailChallenge()` amount errors"}
  - {anchor: "x402error-challengeemail-challenge-validation-failures", title: "X402Error: challenge/email-challenge validation failures"}
  - {anchor: "x402error-server-rejects-the-payment-non-2xx-with-status-set", title: "X402Error: server rejects the payment (non-2xx with `status` set)"}
  - {anchor: "x402error-signing-primitive-errors", title: "X402Error: signing primitive errors"}
  - {anchor: "next-steps", title: "Next steps"}
---

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

# Node.js SDK Errors

Look up what triggers each Node.js SDK error class, PrimitiveApiError, WebhookVerificationError, WebhookValidationError, WebhookPayloadError, and X402Error, and how to fix it.

All `@primitivedotdev/sdk` errors resolve to one of five classes: `PrimitiveApiError` (generated API + high-level `client.send`/`reply`/`forward`/`memories`/`agent` calls), `WebhookVerificationError` and `WebhookPayloadError` and `WebhookValidationError` (webhook parsing, thrown by `handleWebhook`/`handleWebhookEvent`/`receive`), and `X402Error` (the `x402` client). Each carries a stable machine-readable `code` you can branch on instead of matching message text.

## PrimitiveApiError: `inbound_not_repliable` (HTTP 422)

`client.reply(email, ...)` throws when the inbound row isn't in a state Primitive can reply to: the email was rejected at ingestion, its content was discarded, or it has no recipient recorded.

A missing `Message-Id` header does not trigger this; it only omits the threading headers on the reply.

```typescript
import { PrimitiveApiError } from "@primitivedotdev/sdk/api";

try {
  await client.reply(email, "Thanks for your email.");
} catch (err) {
  if (err instanceof PrimitiveApiError && err.code === "inbound_not_repliable") {
    // Fall back to client.send(...) with a fresh subject/thread instead.
  }
}
```

## PrimitiveApiError: validation errors on `send`/`forward`

Before any network call, `client.send` and `client.forward` validate their input locally and throw a `TypeError` (not `PrimitiveApiError`) for malformed fields:

- `from must be at least 3 characters` / `from must be at most 998 characters`
- `to must be at least 3 characters` / `to must be at most 320 characters`
- `to must be a valid email address`
- `subject must be a non-empty string`
- `one of bodyText or bodyHtml is required`
- `thread.references must contain at most 100 values`
- `thread.references header must be at most 8192 characters`
- `waitTimeoutMs must be an integer` / `waitTimeoutMs must be between 1000 and 30000`

Fix the input shape; these never reach the server, so retrying without a change repeats the same throw.

## PrimitiveApiError from the generated API and `client.memories`

Every non-2xx response from the [generated API client](https://test.abhinandan.one/python-generated-api-client.md), including `client.memories.set/get/search/delete` and `client.agent.createAccount`/`claimStart`/`claimVerify`, is unwrapped into a `PrimitiveApiError` with:

- `message`, human-readable description
- `code`, stable error code from the API's `error.code` field
- `status`, the HTTP status
- `gates`, gate-denial details, when the server attached them
- `requestId`, for support correlation
- `retryAfter`, parsed from the `Retry-After` header, when present
- `details`, additional structured context

```typescript
import { PrimitiveApiError } from "@primitivedotdev/sdk/api";

try {
  await client.memories.get("thread:latest");
} catch (err) {
  if (err instanceof PrimitiveApiError) {
    console.error(err.code, err.status, err.requestId);
  }
}
```

A response with no `data` field (an empty-body success) throws `Primitive API returned no <label>`. That means the server returned `200` with a shape the SDK didn't expect, so check you're calling the right operation.

## `client.memories.set` rejects non-JSON values

Calling `client.memories.set({ key, value })` with a value that isn't `string | number | boolean | null | array | plain object` throws a `TypeError`:

```text
client.memories.set value must be a JSON value: string, finite number, boolean, null,
array, or plain object. Undefined, bigint, symbol, function, NaN, Infinity, sparse
arrays, class instances, and cyclic values are not valid memory values.
```

Strip or convert the offending field before calling `set`. This check runs client-side via `isMemoryJsonValue` (see [Memory Value Validation Helper](https://test.abhinandan.one/memory-json-value-helper.md)), so it fails before any network round trip.

## `client.memories.*` rejects the generated-operation shape

Passing `{ client, body }` or `{ client, query }` (the shape the raw generated `setMemory`/`getMemory`/etc. operations expect) into the high-level `client.memories.set/get/delete/search` throws a `TypeError` naming the correct call:

```text
client.memories.set takes the memory fields directly; use client.memories.set({ key, value }),
not the generated operation options shape.
```

Use `client.memories.search({ prefix })`; it's key-prefix search, not free-text. For free-text mail search, use `client.semanticSearch(...)` instead.

## WebhookVerificationError: `MISSING_SECRET`

Thrown when the `secret` argument to `verifyWebhookSignature`, `handleWebhook`, `handleWebhookEvent`, or `receive` is empty, `null`, or omitted. Set `PRIMITIVE_WEBHOOK_SECRET` from your dashboard and pass it explicitly; there is no environment-variable fallback baked into the SDK.

## WebhookVerificationError: `INVALID_SIGNATURE_HEADER`

The `Primitive-Signature` header (or the Standard Webhooks `webhook-signature` header) is missing, empty, or doesn't match the expected format. For the default scheme the expected format is:

```text
Primitive-Signature: t=<unix-seconds>,v1=<hex>
```

Check you're forwarding the raw header value verbatim, not stripping the `t=`/`v1=` parameters.

## WebhookVerificationError: `TIMESTAMP_OUT_OF_RANGE`

The delivery's timestamp is either more than `toleranceSeconds` (default 300s / 5 minutes) old, or more than 60 seconds in the future relative to your server clock. This is the replay-protection window described in [webhook signature verification](https://test.abhinandan.one/node-sdk-webhook-signing.md). If your server clock drifts, sync it with NTP; if you need a wider window for legitimate redelivery delays, pass a larger `toleranceSeconds`.

## WebhookVerificationError: `SIGNATURE_MISMATCH`

The computed HMAC-SHA256 doesn't match any signature in the header. The most common causes, in order of likelihood:

- **Re-serialized body.** You must verify against the *exact* raw request bytes, before any `JSON.parse`/`JSON.stringify` round trip. If the SDK detects a pretty-printed body shape, the error message includes a specific hint: `Request body appears re-serialized (pretty-printed). Use the raw request body before any json.loads() or json.dumps() calls.`
- **Wrong secret.** Fetch the current value from `GET /account/webhook-secret` and use it as a raw UTF-8 string. Do not base64-decode it, even though it looks base64-shaped.
- **Framework middleware already consumed the body** as JSON before your handler saw the raw bytes. Configure your framework to expose the raw body (e.g. `express.raw()`), or use `primitive.receive(request, { secret })` with a standard `Request` object, which reads the raw bytes for you.

## WebhookPayloadError: `PAYLOAD_EMPTY_BODY` / `PAYLOAD_NULL` / `PAYLOAD_UNDEFINED` / `PAYLOAD_IS_ARRAY` / `PAYLOAD_WRONG_TYPE`

Thrown while parsing the JSON body, before signature classification. Each name states the exact problem: an empty string body, a `null`/`undefined` body, an array instead of an object, or some other non-object type. Verify your framework is passing the actual request body through to `handleWebhook`/`handleWebhookEvent`, not an already-transformed value.

## WebhookPayloadError: `JSON_PARSE_FAILED`

The body isn't valid JSON. When the parser can locate the failure position, the message includes it (`Invalid JSON at position 42...`) with a hint that your framework may be truncating the body; otherwise it repeats the underlying JSON parser's message.

## WebhookPayloadError: `PAYLOAD_MISSING_EVENT`

Neither the `X-Webhook-Event` header nor a top-level `event` field in the body could classify the payload. A real Primitive delivery always sends the header, so seeing this means something upstream (a proxy, a test harness) stripped it. Pass the header through, or call `handleWebhookEvent`/`receive`, which reads it for you automatically.

## WebhookValidationError: schema validation failed

Thrown when a payload classified as `email.received` fails the [canonical JSON Schema](https://test.abhinandan.one/webhook-events.md) validation, for example a missing required field or a wrong type on a known property. Compare the payload against `json-schema/email-received-event.schema.json` (or run it through `safeValidateEmailReceivedEvent`, which returns a result object instead of throwing) to find the exact mismatched field.

## `handleWebhook` throws on non-`email.received` events

`handleWebhook` is hard-typed to `email.received` for backward compatibility. Any other event type reaching it (a `payment.*` or `interaction.*` delivery) surfaces as a payload error naming the unsupported event. Switch to `handleWebhookEvent`, which returns a typed union covering every event family plus `UnknownEvent` for forward compatibility. See [handling payment and interaction webhook events](https://test.abhinandan.one/node-sdk-webhook-events.md).

## X402Error: `status: 0` (request never reached the server)

Every x402 client method (`charge`, `pay`, `createEmailChallenge`, `payEmailChallenge`, `registerPayoutAddress`, `setSpendPolicy`,...) throws `X402Error` with `status: 0` for anything that failed before getting an HTTP response: a rejected `fetch` (DNS, connection refused, TLS), a client-side timeout, or an SDK-side validation failure caught before the request was built.

On `pay()` specifically, a `status: 0` error means the payment outcome is indeterminate: the request may or may not have reached the server. Do not blindly retry. Check `getChallenge(id)` or the settlement webhook before resubmitting, to avoid a duplicate authorization attempt.

```typescript
import { X402Error } from "@primitivedotdev/sdk/x402";

try {
  await x402.pay(challenge, { signer: payer });
} catch (err) {
  if (err instanceof X402Error && err.status === 0) {
    // Outcome unknown. Check getChallenge(challenge.id) before retrying.
  }
}
```

## X402Error: "no API key configured"

Thrown by any `X402Client` method when neither `apiKey` was passed to `createX402Client(...)` nor `PRIMITIVE_API_KEY` is set in the environment. Set one of the two before calling `charge`, `pay`, or any other method.

## X402Error: `charge()`/`createEmailChallenge()` amount errors

- **Both `amount` and `amountUsdc` set**: `charge() takes exactly one of amount (base units) or amountUsdc (human USDC), not both`. Pass exactly one.
- **Neither set, or malformed**: `charge() requires amount as a positive integer string in token base units (e.g. "10000"), or amountUsdc as a positive USDC amount with at most 6 decimals (e.g. "0.01")`. USDC has 6 decimals; `amountUsdc` values with more than 6 decimal places, non-positive values, or non-numeric strings are all rejected before any network call.
- **Unknown option key**: `unknown charge() option "<key>"; expected one of: ...` catches typos like `payer_org` instead of `payerOrg` immediately rather than silently dropping the field.

## X402Error: challenge/email-challenge validation failures

`pay()`, `payEmailChallenge()`, and `parseEmailChallengeFromPart()` validate the challenge shape before signing, so a missing field fails with a named error instead of an opaque signing exception:

- `challenge is missing or malformed: <field>`: one of `id`, `network`, `expires_at`, `nonce_binding`, or a `payment_requirements` field (`maxAmountRequired`, `payTo`, `asset`, `extra.name`/`extra.version`) is absent or the wrong shape.
- `email challenge is missing or malformed: <field>`: the same check for the `X402EmailChallenge` shape returned by `createEmailChallenge`/`parseEmailChallengeFromPart`, plus a consistency check that the envelope's `interaction_id` matches `challenge.nonce_binding.interaction_id`.
- `interaction.json part is not a valid x402 challenge: <field>`: `parseEmailChallengeFromPart` rejects a part that isn't the `x402.payment` protocol's `challenge` step, has the wrong `protocol_version`, or has a malformed `challenge_nonce`/`step_id`.

Inspect the challenge object you're passing in; these are almost always the result of hand-constructing a challenge instead of using the object returned by `charge()`/`createEmailChallenge()`/`parseEmailChallengeFromPart()` unmodified.

## X402Error: server rejects the payment (non-2xx with `status` set)

A non-zero `status` on `X402Error` means the request reached the server and it rejected the payment; `message` and `body` carry the server's explanation (e.g. `payment_declined`, spend-policy caps exceeded, expired challenge). Check `err.retryAfter` for a `Retry-After` value if the server attached one, and see [spend policy and payout address management](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-spend-policy.md) if the rejection relates to caps or the allowlist.

## X402Error: signing primitive errors

Calling the low-level primitives directly (see [low-level x402 signing primitives](https://test.abhinandan.one/x402-payments-overview-04a296ff/node-sdk-x402-signing-primitives.md)) throws plain `Error`, not `X402Error`, for malformed inputs:

- `deriveEip3009Nonce`: `challengeNonce must be exactly 64 lowercase hex chars (32 bytes), no 0x prefix`
- `computePaymentValidityWindow`: `invalid validity window: validBefore (...) is below the minimum settlement headroom...` (too close to expiry) or `...exceeds the ... window cap... the authorization window is too wide` (too far in the future), both only thrown when you pin `validBeforeSec`/`validAfterSec` explicitly with `clamp: false`; the default behavior clamps into the accepted band instead of throwing.
- `buildExactEvmPaymentPayload`: `unsupported network <network>`, or a malformed nonce/signature hex string.

These are programming errors in a custom signing flow, not something a caller retries. Fix the input and re-derive.
