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

Request Options and Idempotency

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.

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.

What RequestOptions carries#

interface RequestOptions {
  // Cancel the in-flight request when this signal fires. Surfaces as AbortError.
  signal?: AbortSignal;
  // Per-call timeout in milliseconds. Composed with `signal` so either fires.
  timeout?: number;
  // Per-call headers merged on top of client-level headers. Last write wins.
  headers?: Record<string, string>;
  // Idempotency key for safe retries. Sent as the Idempotency-Key request header.
  idempotencyKey?: string;
}

Client-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.

  1. 1

    Cap a slow call with a per-call timeout#

    Pass timeout (milliseconds) as the second argument to bound how long a single call waits before it aborts:

    import primitive from "@primitivedotdev/sdk";
    
    const client = primitive.client({
      apiKey: process.env.PRIMITIVE_API_KEY!,
    });
    
    await client.send(
      {
        from: "Support <support@example.com>",
        to: "alice@example.com",
        subject: "Hello",
        bodyText: "Hi there",
      },
      { timeout: 15000 },
    );
    

    When the timeout fires, the promise rejects with an AbortError. Catch it the same way you'd catch any other rejected promise.

  2. 2

    Or cancel with your own AbortSignal#

    Pass signal when you need external cancellation, for example, tying the request to a user action or an upstream request's own abort signal:

    const controller = new AbortController();
    
    const sendPromise = client.send(
      {
        from: "Support <support@example.com>",
        to: "alice@example.com",
        subject: "Hello",
        bodyText: "Hi there",
      },
      { signal: controller.signal },
    );
    
    // Elsewhere: cancel the request.
    controller.abort();
    

    You can pass both signal and timeout together. They compose via AbortSignal.any, so whichever fires first cancels the call.

  3. 3

    Attach custom headers#

    Pass headers to merge extra headers on top of any client-level defaults. Per-call headers win on a key conflict:

    await client.send(
      {
        from: "Support <support@example.com>",
        to: "alice@example.com",
        subject: "Hello",
        bodyText: "Hi there",
      },
      { headers: { "X-Trace-Id": "trace-abc123" } },
    );
    
  4. 4

    Make a send retry-safe with an idempotency key#

    Pass idempotencyKey, a string unique per logical send, as RequestOptions.idempotencyKey. It's sent as the Idempotency-Key request header:

    await client.send(
      {
        from: "Support <support@example.com>",
        to: "alice@example.com",
        subject: "Hello",
        bodyText: "Hi there",
      },
      { idempotencyKey: "customer-key-abc123" },
    );
    

    Reusing 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.

    Tip

    Check 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 for the full SendResult shape.

Putting it together#

Combine timeout and idempotency key on the same call, the common pattern for a retry loop around a flaky network:

import primitive from "@primitivedotdev/sdk";

const client = primitive.client({
  apiKey: process.env.PRIMITIVE_API_KEY!,
});

async function sendWithRetry() {
  try {
    return await client.send(
      {
        from: "Support <support@example.com>",
        to: "alice@example.com",
        subject: "Hello",
        bodyText: "Hi there",
      },
      { timeout: 15000, idempotencyKey: "customer-key-abc123" },
    );
  } catch (err) {
    if (err instanceof Error && err.name === "AbortError") {
      // Safe to retry with the SAME idempotencyKey: a replay returns the
      // original response instead of sending twice.
      return await client.send(
        {
          from: "Support <support@example.com>",
          to: "alice@example.com",
          subject: "Hello",
          bodyText: "Hi there",
        },
        { timeout: 15000, idempotencyKey: "customer-key-abc123" },
      );
    }
    throw err;
  }
}
Warning

Never 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.

wait is a body field, not a request option#

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 and Sending, Replying, and Forwarding 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.

Next steps#

Was this page helpful?

© Primitive SDKs

Powered by Browzer