Client cloning
withOptions(partial) returns a new client built from the original one’s options. The original client is untouched, and the new one reuses its hooks and retry settings. defaultHeaders is merged with the original headers; any other option you pass replaces the original value.
defaultHeaders is sent on every request, but Nuntly does not read or route on it. Use it for your own tracing and audit chain.
One API key per customer
When each customer of your SaaS has their own Nuntly API key, a newNuntly per request would need the hooks and the retry settings passed again. Clone the root client instead:
One trace ID across services
When a request in your app calls several services, forward your trace ID on every Nuntly call so your APM links the email send to the request that triggered it:Who sent this email
To record which user of your app sent an email, stamp a header on every request. Your gateway, proxy or log pipeline records it; Nuntly ignores it.Lifecycle hooks
Hooks run around each HTTP request, for logging or telemetry. A hook reads the request and cannot change it. Every hook receives a singlectx argument:
For one call, the hooks run in this order:
onRequest, then onRetry and onRequest again for each retry, then onResponse, then onSuccess or onError. The client awaits every hook that returns a Promise. An error thrown by onError is logged and the call still rejects with its own error; an error thrown by any other hook rejects the call with that error.
The Authorization header in ctx.headers reads Bearer [redacted]: no hook argument, log line or error carries the API key, and neither does console.log(nuntly).
Retries
By default the SDK retries a request up to 2 times on a 408, a 429, a 500, a 502, a 503, a 504, a timeout or a network error. The first retry waits 375 to 500 ms and each later one twice as long, up to 8 s. The initial interval is 500 ms, the exponent is 2, and a jitter shortens each delay to 75% to 100% of it.- A 409 is retried only when the API sends
x-should-retry: true. The API does so when the first attempt of a send with an idempotency key is still in progress, and the retry gets the stored response of that attempt. Any other conflict, such as a contact that already exists, does not resolve on its own. - A
POSTor aPATCHis retried only on a 429, so a retry cannot apply it twice. The exception is the eight sending methods listed in the README’s per-request options: the API deduplicates them byIdempotency-Key, and they send your key or one they generate, so they are also retried on a 408, a 5xx, a timeout or a network error. AnIdempotency-Keyheader you add to another method does not change its retries. - The SDK never retries a request you abort through
signal, including withAbortSignal.timeout(...), or an error thrown by a hook. - A failure while reading the response body, such as a connection reset mid-body, is not retried.
- A server-sent
x-should-retry: trueorfalseoverrides these rules. ARetry-Afterheader (seconds, decimals allowed, or an HTTP date) on any response replaces the backoff delay. Above 60 s the SDK does not wait, even withx-should-retry: true: it throws the error for that response at once, withRateLimitError.retryAfterset on a 429. - With
retry: 'none', a per-requestmaxRetriesdoes not turn retries back on.
Safe mode for one call
createSafeNuntly makes every call return { data, error }. To get that shape for some calls only, wrap an existing client with safe():
Response headers without the body
.asResponse() resolves to the raw Response as soon as the headers arrive, without reading the body. Use it for streaming or to read headers only. It does not throw on a 4xx or 5xx, so check response.status:
.asResponse() and .withResponse() resolve to a copy of the response built from that body, so the body stays readable. The copy has no content-encoding or content-length header, since its body is the decoded text, and the call keeps that text in memory for as long as you hold its promise.
Abort a request
APIConnectionError and is not retried. An abort through AbortSignal.timeout(...) rejects with its subclass APIConnectionTimeoutError.
Debug logging
debug: true logs each request and response:
Custom logger
logger accepts any logger compatible with console, such as pino or winston:
Custom fetch
fetch replaces globalThis.fetch with any compatible function, for instrumentation, custom retry or another HTTP client. The SDK calls it with a Request as its first argument:
HTTP and HTTPS proxies
On Node.js, pass anundici ProxyAgent as the dispatcher of the global fetch to send requests through a corporate or debugging proxy:
