{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/node-sdk-errors","markdown_url":"https://test.abhinandan.one/node-sdk-errors.md","article":{"id":"9ec14110-e41c-4fdb-8608-c8a7a5c8030c","article_slug":"node-sdk-errors","parent_article_slug":null,"parent_article_title":null,"kind":"troubleshooting","published_at":"2026-08-11T18:54:58.368305+00:00","keywords":["PrimitiveApiError","WebhookVerificationError","WebhookPayloadError","WebhookValidationError","X402Error","SIGNATURE_MISMATCH"],"meta_description":"Every PrimitiveApiError, WebhookVerificationError, WebhookPayloadError, and X402Error code the Node.js SDK throws, with the exact cause and fix.","og_image_url":null,"source_file_paths":["sdk-node/src/webhook/index.ts","sdk-node/src/api/index.ts","sdk-node/src/x402/client.ts"],"recording_id":null,"replayable":false,"task_name":"Node.js SDK Errors","category":"Troubleshooting","summary":null,"description":"Look up what triggers each Node.js SDK error class, PrimitiveApiError, WebhookVerificationError, WebhookValidationError, WebhookPayloadError, and X402Error, and how to fix it.","content_kind":"repo_page","content_markdown":"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.\n\n## PrimitiveApiError: `inbound_not_repliable` (HTTP 422)\n\n`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.\n\nA missing `Message-Id` header does not trigger this; it only omits the threading headers on the reply.\n\n```typescript\nimport { PrimitiveApiError } from \"@primitivedotdev/sdk/api\";\n\ntry {\n  await client.reply(email, \"Thanks for your email.\");\n} catch (err) {\n  if (err instanceof PrimitiveApiError && err.code === \"inbound_not_repliable\") {\n    // Fall back to client.send(...) with a fresh subject/thread instead.\n  }\n}\n```\n\n## PrimitiveApiError: validation errors on `send`/`forward`\n\nBefore any network call, `client.send` and `client.forward` validate their input locally and throw a `TypeError` (not `PrimitiveApiError`) for malformed fields:\n\n- `from must be at least 3 characters` / `from must be at most 998 characters`\n- `to must be at least 3 characters` / `to must be at most 320 characters`\n- `to must be a valid email address`\n- `subject must be a non-empty string`\n- `one of bodyText or bodyHtml is required`\n- `thread.references must contain at most 100 values`\n- `thread.references header must be at most 8192 characters`\n- `waitTimeoutMs must be an integer` / `waitTimeoutMs must be between 1000 and 30000`\n\nFix the input shape; these never reach the server, so retrying without a change repeats the same throw.\n\n## PrimitiveApiError from the generated API and `client.memories`\n\nEvery non-2xx response from the [generated API client](python-generated-api-client), including `client.memories.set/get/search/delete` and `client.agent.createAccount`/`claimStart`/`claimVerify`, is unwrapped into a `PrimitiveApiError` with:\n\n- `message`, human-readable description\n- `code`, stable error code from the API's `error.code` field\n- `status`, the HTTP status\n- `gates`, gate-denial details, when the server attached them\n- `requestId`, for support correlation\n- `retryAfter`, parsed from the `Retry-After` header, when present\n- `details`, additional structured context\n\n```typescript\nimport { PrimitiveApiError } from \"@primitivedotdev/sdk/api\";\n\ntry {\n  await client.memories.get(\"thread:latest\");\n} catch (err) {\n  if (err instanceof PrimitiveApiError) {\n    console.error(err.code, err.status, err.requestId);\n  }\n}\n```\n\nA 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.\n\n## `client.memories.set` rejects non-JSON values\n\nCalling `client.memories.set({ key, value })` with a value that isn't `string | number | boolean | null | array | plain object` throws a `TypeError`:\n\n```text\nclient.memories.set value must be a JSON value: string, finite number, boolean, null,\narray, or plain object. Undefined, bigint, symbol, function, NaN, Infinity, sparse\narrays, class instances, and cyclic values are not valid memory values.\n```\n\nStrip or convert the offending field before calling `set`. This check runs client-side via `isMemoryJsonValue` (see [Memory Value Validation Helper](memory-json-value-helper)), so it fails before any network round trip.\n\n## `client.memories.*` rejects the generated-operation shape\n\nPassing `{ 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:\n\n```text\nclient.memories.set takes the memory fields directly; use client.memories.set({ key, value }),\nnot the generated operation options shape.\n```\n\nUse `client.memories.search({ prefix })`; it's key-prefix search, not free-text. For free-text mail search, use `client.semanticSearch(...)` instead.\n\n## WebhookVerificationError: `MISSING_SECRET`\n\nThrown 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.\n\n## WebhookVerificationError: `INVALID_SIGNATURE_HEADER`\n\nThe `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:\n\n```text\nPrimitive-Signature: t=<unix-seconds>,v1=<hex>\n```\n\nCheck you're forwarding the raw header value verbatim, not stripping the `t=`/`v1=` parameters.\n\n## WebhookVerificationError: `TIMESTAMP_OUT_OF_RANGE`\n\nThe 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](node-sdk-webhook-signing). If your server clock drifts, sync it with NTP; if you need a wider window for legitimate redelivery delays, pass a larger `toleranceSeconds`.\n\n## WebhookVerificationError: `SIGNATURE_MISMATCH`\n\nThe computed HMAC-SHA256 doesn't match any signature in the header. The most common causes, in order of likelihood:\n\n- **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.`\n- **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.\n- **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.\n\n## WebhookPayloadError: `PAYLOAD_EMPTY_BODY` / `PAYLOAD_NULL` / `PAYLOAD_UNDEFINED` / `PAYLOAD_IS_ARRAY` / `PAYLOAD_WRONG_TYPE`\n\nThrown 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.\n\n## WebhookPayloadError: `JSON_PARSE_FAILED`\n\nThe 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.\n\n## WebhookPayloadError: `PAYLOAD_MISSING_EVENT`\n\nNeither 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.\n\n## WebhookValidationError: schema validation failed\n\nThrown when a payload classified as `email.received` fails the [canonical JSON Schema](webhook-events) 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.\n\n## `handleWebhook` throws on non-`email.received` events\n\n`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](node-sdk-webhook-events).\n\n## X402Error: `status: 0` (request never reached the server)\n\nEvery 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.\n\nOn `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.\n\n```typescript\nimport { X402Error } from \"@primitivedotdev/sdk/x402\";\n\ntry {\n  await x402.pay(challenge, { signer: payer });\n} catch (err) {\n  if (err instanceof X402Error && err.status === 0) {\n    // Outcome unknown. Check getChallenge(challenge.id) before retrying.\n  }\n}\n```\n\n## X402Error: \"no API key configured\"\n\nThrown 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.\n\n## X402Error: `charge()`/`createEmailChallenge()` amount errors\n\n- **Both `amount` and `amountUsdc` set**: `charge() takes exactly one of amount (base units) or amountUsdc (human USDC), not both`. Pass exactly one.\n- **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.\n- **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.\n\n## X402Error: challenge/email-challenge validation failures\n\n`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:\n\n- `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.\n- `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`.\n- `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`.\n\nInspect 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.\n\n## X402Error: server rejects the payment (non-2xx with `status` set)\n\nA 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](node-sdk-x402-spend-policy) if the rejection relates to caps or the allowlist.\n\n## X402Error: signing primitive errors\n\nCalling the low-level primitives directly (see [low-level x402 signing primitives](node-sdk-x402-signing-primitives)) throws plain `Error`, not `X402Error`, for malformed inputs:\n\n- `deriveEip3009Nonce`: `challengeNonce must be exactly 64 lowercase hex chars (32 bytes), no 0x prefix`\n- `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.\n- `buildExactEvmPaymentPayload`: `unsupported network <network>`, or a malformed nonce/signature hex string.\n\nThese are programming errors in a custom signing flow, not something a caller retries. Fix the input and re-derive.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Handling Payment and Interaction Webhook Events\" href=\"node-sdk-webhook-events\">\n\nUse handleWebhookEvent and typed guards to branch on email, payment, and interaction deliveries from one endpoint.\n\n</Card>\n\n<Card title=\"Webhook Signature Verification\" href=\"node-sdk-webhook-signing\">\n\nVerify the Primitive-Signature HMAC header manually when your framework doesn't hand you a standard Request.\n\n</Card>\n\n<Card title=\"Paying a Challenge\" href=\"node-sdk-x402-paying\">\n\nSign and submit payment for an x402 challenge with pay() and read the settlement receipt.\n\n</Card>\n\n<Card title=\"Generated API Client and Primitive Memories\" href=\"node-sdk-api-client\">\n\nCall any Primitive HTTP endpoint directly and store, search, and delete durable JSON records.\n\n</Card>\n\n</CardGroup>","canonical_base_url":"https://test.abhinandan.one","seo_indexing_enabled":true,"last_modified":"2026-08-21T18:22:43.359885+00:00","video_url":null,"voiceover_url":null,"tools_used":[],"demonstrated_by":[],"steps":[],"related_links":[],"intro":null,"prerequisites":[],"verification":[],"troubleshooting":[],"suggest_edit_url":"https://github.com/abhi-browzer/primitive-sdks/edit/main/sdk-node/src/webhook/index.ts","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Node.js+SDK+Errors&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-errors","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}