{"schema_version":"1.0","publisher":"Primitive SDKs","canonical_url":"https://test.abhinandan.one/node-sdk-request-options","markdown_url":"https://test.abhinandan.one/node-sdk-request-options.md","article":{"id":"7bd33534-3dbb-4563-ba62-865357c28b69","article_slug":"node-sdk-request-options","parent_article_slug":null,"parent_article_title":null,"kind":"guide","published_at":"2026-08-11T18:54:51.967277+00:00","keywords":["RequestOptions","idempotencyKey","AbortSignal.timeout","Idempotency-Key header","client.send request options","per-call timeout Node SDK"],"meta_description":"Pass signal, timeout, headers, and idempotencyKey as the second argument to client.send/reply/forward in @primitivedotdev/sdk for safe retries.","og_image_url":null,"source_file_paths":["sdk-node/README.md","sdk-node/src/api/index.ts"],"recording_id":null,"replayable":false,"task_name":"Request Options and Idempotency","category":"Node.js SDK","summary":null,"description":"Configure per-call timeouts, abort signals, custom headers, and idempotency keys on any @primitivedotdev/sdk client method so slow calls fail fast and retries never double-send.","content_kind":"repo_page","content_markdown":"Every method on the `@primitivedotdev/sdk` client, `client.send`, `client.reply`, `client.forward`, accepts an optional second argument, `RequestOptions`, that controls cancellation, timeouts, headers, and idempotent retries for that one call. Reach for it whenever you need a call to fail fast, carry a tracing header, or survive a network retry without double-sending mail.\n\n## What `RequestOptions` carries\n\n```typescript\ninterface RequestOptions {\n  // Cancel the in-flight request when this signal fires. Surfaces as AbortError.\n  signal?: AbortSignal;\n  // Per-call timeout in milliseconds. Composed with `signal` so either fires.\n  timeout?: number;\n  // Per-call headers merged on top of client-level headers. Last write wins.\n  headers?: Record<string, string>;\n  // Idempotency key for safe retries. Sent as the Idempotency-Key request header.\n  idempotencyKey?: string;\n}\n```\n\nClient-level config, the `fetch` implementation, base URL, and default headers you passed to `primitive.client({...})`, still applies to every call. Per-call `RequestOptions` compose on top: `headers` merge with the per-call value winning on conflict, and `signal`/`timeout` compose so whichever fires first wins.\n\n<Steps>\n\n<Step title=\"Cap a slow call with a per-call timeout\">\n\nPass `timeout` (milliseconds) as the second argument to bound how long a single call waits before it aborts:\n\n```typescript\nimport primitive from \"@primitivedotdev/sdk\";\n\nconst client = primitive.client({\n  apiKey: process.env.PRIMITIVE_API_KEY!,\n});\n\nawait client.send(\n  {\n    from: \"Support <support@example.com>\",\n    to: \"alice@example.com\",\n    subject: \"Hello\",\n    bodyText: \"Hi there\",\n  },\n  { timeout: 15000 },\n);\n```\n\nWhen the timeout fires, the promise rejects with an `AbortError`. Catch it the same way you'd catch any other rejected promise.\n\n</Step>\n\n<Step title=\"Or cancel with your own AbortSignal\">\n\nPass `signal` when you need external cancellation, for example, tying the request to a user action or an upstream request's own abort signal:\n\n```typescript\nconst controller = new AbortController();\n\nconst sendPromise = client.send(\n  {\n    from: \"Support <support@example.com>\",\n    to: \"alice@example.com\",\n    subject: \"Hello\",\n    bodyText: \"Hi there\",\n  },\n  { signal: controller.signal },\n);\n\n// Elsewhere: cancel the request.\ncontroller.abort();\n```\n\nYou can pass both `signal` and `timeout` together. They compose via `AbortSignal.any`, so whichever fires first cancels the call.\n\n</Step>\n\n<Step title=\"Attach custom headers\">\n\nPass `headers` to merge extra headers on top of any client-level defaults. Per-call headers win on a key conflict:\n\n```typescript\nawait client.send(\n  {\n    from: \"Support <support@example.com>\",\n    to: \"alice@example.com\",\n    subject: \"Hello\",\n    bodyText: \"Hi there\",\n  },\n  { headers: { \"X-Trace-Id\": \"trace-abc123\" } },\n);\n```\n\n</Step>\n\n<Step title=\"Make a send retry-safe with an idempotency key\">\n\nPass `idempotencyKey`, a string unique per logical send, as `RequestOptions.idempotencyKey`. It's sent as the `Idempotency-Key` request header:\n\n```typescript\nawait client.send(\n  {\n    from: \"Support <support@example.com>\",\n    to: \"alice@example.com\",\n    subject: \"Hello\",\n    bodyText: \"Hi there\",\n  },\n  { idempotencyKey: \"customer-key-abc123\" },\n);\n```\n\nReusing the same key on a retried call returns the **original** response instead of sending a second email. Use one key per logical send: a network blip that forces you to retry the exact same request should reuse the key; a genuinely new email needs a new key.\n\n<Tip>\n\nCheck `result.deliveryStatus` and the `idempotentReplay` flag documented on the send result to tell a fresh send apart from a replayed one. See [Sending, Replying, and Forwarding Email](node-sdk-sending-email) for the full `SendResult` shape.\n\n</Tip>\n\n</Step>\n\n</Steps>\n\n## Putting it together\n\nCombine timeout and idempotency key on the same call, the common pattern for a retry loop around a flaky network:\n\n```typescript\nimport primitive from \"@primitivedotdev/sdk\";\n\nconst client = primitive.client({\n  apiKey: process.env.PRIMITIVE_API_KEY!,\n});\n\nasync function sendWithRetry() {\n  try {\n    return await client.send(\n      {\n        from: \"Support <support@example.com>\",\n        to: \"alice@example.com\",\n        subject: \"Hello\",\n        bodyText: \"Hi there\",\n      },\n      { timeout: 15000, idempotencyKey: \"customer-key-abc123\" },\n    );\n  } catch (err) {\n    if (err instanceof Error && err.name === \"AbortError\") {\n      // Safe to retry with the SAME idempotencyKey: a replay returns the\n      // original response instead of sending twice.\n      return await client.send(\n        {\n          from: \"Support <support@example.com>\",\n          to: \"alice@example.com\",\n          subject: \"Hello\",\n          bodyText: \"Hi there\",\n        },\n        { timeout: 15000, idempotencyKey: \"customer-key-abc123\" },\n      );\n    }\n    throw err;\n  }\n}\n```\n\n<Warning>\n\nNever generate a fresh `idempotencyKey` inside a retry loop. If the key changes between attempts, a retry after a timeout can produce a duplicate email, the whole point of the key is that it stays fixed across retries of the *same* logical send.\n\n</Warning>\n\n## `wait` is a body field, not a request option\n\n`wait: true` and `waitTimeoutMs` control whether `send`/`reply` hold the response open for the first downstream SMTP delivery outcome. They're passed on the send/reply input itself, not in `RequestOptions`. See the [wait mode concept](email-model) and [Sending, Replying, and Forwarding Email](node-sdk-sending-email) for that behavior; when you combine `wait: true` with a `timeout` here, make the timeout comfortably longer than the 30-second default `waitTimeoutMs` so the request option doesn't cut the call off before the server-side wait resolves.\n\n## Next steps\n\n<CardGroup cols={2}>\n\n<Card title=\"Sending, Replying, and Forwarding Email\" href=\"node-sdk-sending-email\">\n\nSee the full send/reply/forward input and result shapes, including wait mode and delivery status.\n\n</Card>\n\n<Card title=\"Node.js SDK Errors\" href=\"node-sdk-errors\">\n\nLook up PrimitiveApiError and what an aborted or timed-out request surfaces as.\n\n</Card>\n\n<Card title=\"Generated API Client and Primitive Memories\" href=\"node-sdk-api-client\">\n\nUse the lower-level PrimitiveApiClient directly when you need the full generated HTTP surface.\n\n</Card>\n\n<Card title=\"Inbound and Outbound Email Model\" href=\"email-model\">\n\nUnderstand wait mode and delivery status, which live on the request body rather than RequestOptions.\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/README.md","raise_issue_url":"https://github.com/abhi-browzer/primitive-sdks/issues/new?title=Docs+feedback%3A+Request+Options+and+Idempotency&body=Page%3A+https%3A%2F%2Ftest.abhinandan.one%2Fnode-sdk-request-options","page_feedback_enabled":true,"verified_ref":null,"verified_at":"2026-08-11T18:38:45.205849+00:00"}}