Node SDK
@noresponse/node is the server-side client for Node.js. It covers every route in the interactive reference, verifies webhooks and turns your auth provider’s verified claims into authContext. The client is generated from the NoResponse OpenAPI document, so every request and response field is typed with the contract’s descriptions as doc comments.
Install
Section titled “Install”npm install @noresponse/nodeThe package needs Node.js 20 or later and ships ESM and CommonJS builds, so import { NoResponse } from "@noresponse/node" and const { NoResponse } = require("@noresponse/node") both work. It uses the global fetch. Keep it on your backend, where your API key lives.
Client
Section titled “Client”import { NoResponse } from "@noresponse/node";
const nr = new NoResponse({ apiKey: process.env.NORESPONSE_API_KEY, baseUrl: "https://api.noresponse.org", timeoutMs: 10_000,});| Option | Default | Meaning |
|---|---|---|
apiKey |
NORESPONSE_API_KEY |
Your nrk_ key, or a function returning a promise of one. The constructor throws when neither the option nor the variable is set. |
baseUrl |
NORESPONSE_BASE_URL, then https://api.noresponse.org |
The API host. HTTPS is required except for localhost. |
timeoutMs |
10000 |
Timeout per request, in milliseconds. Also settable per call. |
fetch |
global fetch |
A custom fetch, for tests or a proxy agent. |
logger |
console when NORESPONSE_LOG=debug |
Receives one line per request and response. See Logging. |
retryConfig |
See Retries | Overrides the retry policy for every call. |
new NoResponse() with no arguments reads everything from the environment. Every request sends Authorization: Bearer <key> and User-Agent: noresponse-node/2.0.0 (node/<version>).
Every method takes request options as its last argument: { timeoutMs, signal, headers, retries }, plus idempotencyKey on create and recordOutcome.
Methods
Section titled “Methods”| Method | Calls | Returns |
|---|---|---|
nr.assessments.create(request, options?) |
POST /v1/assessments |
Assessment |
nr.assessments.get(id, options?) |
GET /v1/assessments/{id} |
Assessment |
nr.assessments.wait(id, { timeoutMs?, responseMode? }) |
Polls GET /v1/assessments/{id} |
Assessment that is no longer pending |
nr.assessments.query({ accountId, limit?, cursor?, responseMode? }) |
POST /v1/assessments/query |
AssessmentPage, which also iterates every item |
nr.assessments.recordOutcome(id, request, options?) |
POST /v1/assessments/{id}/outcomes |
AssessmentOutcome |
nr.keys.rotate({ overlapSeconds? }) |
POST /v1/keys/rotate |
KeyRotation |
nr.webhooks.constructEvent(payload, headers, secrets, options?) |
No request | WebhookEvent |
nr.webhooks.verificationResponse(event) |
No request | { challenge } |
Requests and responses use the contract’s fields in camelCase: account_id is accountId, assessment_version is assessmentVersion, previous_key_expires_at is previousKeyExpiresAt.
assessments.create
Section titled “assessments.create”const assessment = await nr.assessments.create( { accountId: "usr_8f2k1", policyContext: { flow: "signup", region: "GB" }, requestId: "req_01", deadlineMs: 400, responseMode: "standard", clientContext: { timezone: "Europe/London", locale: "en-GB", platform: "web" }, authContext, // from fromClerk, fromWorkOS, fromAuth0 or fromOIDC }, { idempotencyKey: "signup-usr_8f2k1-01JB3C4D" },);The request mirrors the contract: identifiers holds email, username, phone, name and ip, and policyContext holds flow and region. The limits are on Create an assessment.
idempotencyKey is the Idempotency-Key header. When you omit it the client generates a UUID and sends it again on every retry of this call. Pass your own key, derived from your signup attempt, when a retry from your side must replay the same assessment.
The method returns the assessment for both a 200 and a 202. Read assessment.status: complete and partial carry risk and recommendation; pending carries neither. The response headers are on assessment.meta:
meta field |
Type | From |
|---|---|---|
requestId |
string | X-Request-ID |
retryAfter |
number or null | Retry-After, in seconds |
location |
string or null | Location on a 202 |
idempotentReplayed |
boolean | Idempotent-Replayed |
assessments.get and assessments.wait
Section titled “assessments.get and assessments.wait”const latest = await nr.assessments.get(assessment.assessmentId, { responseMode: "detailed" });
const settled = await nr.assessments.wait(assessment.assessmentId, { timeoutMs: 10_000 });wait polls get until status is not pending, sleeping for Retry-After between polls with a one second floor, and throws APITimeoutError when timeoutMs (30 seconds by default) runs out. Apply your fallback first and let the result arrive by webhook or a background wait, so the signup is never held open.
assessments.query
Section titled “assessments.query”const page = await nr.assessments.query({ accountId: "usr_8f2k1", limit: 50 });page.items; // Assessment[], newest firstpage.nextCursor; // string, absent on the last pagepage.hasMore; // booleanawait page.next(); // the next page, or null
for await (const item of page) { // every unexpired assessment, from this page on, one request per page}The query needs a key with the history scope. See Get and query assessments.
assessments.recordOutcome
Section titled “assessments.recordOutcome”await nr.assessments.recordOutcome( assessment.assessmentId, { outcome: "confirmed_abuse", observedAt: "2026-10-20T05:00:00Z", externalRef: "case-123", notes: "Card testing from this account." }, { idempotencyKey: `outcome-${caseId}` },);outcome is one of confirmed_abuse, account_takeover, chargeback, false_positive, legitimate or unknown. The idempotency key stays bound for as long as the outcome is kept, so derive it from your case.
keys.rotate
Section titled “keys.rotate”const rotation = await nr.keys.rotate({ overlapSeconds: 86_400 });rotation.key; // the new key, shown oncerotation.previousKeyExpiresAt; // when the key that made this call stops workingThe new key has the scopes of the key that made the call. Store it at once and roll it out before previousKeyExpiresAt. See Authentication.
Errors
Section titled “Errors”Every API error is an instance of NoResponseError, with a subclass per status.
import { NoResponseError, RateLimitError, APIConnectionError, APITimeoutError } from "@noresponse/node";
try { await nr.assessments.create(params);} catch (error) { if (error instanceof RateLimitError) { error.retryAfter; // seconds } if (error instanceof NoResponseError) { error.status; // 400, 401, 403, 404, 409, 429, 503 ... error.code; // "rate_limited" error.type; // "https://docs.noresponse.org/problems/rate-limited" error.title; error.detail; error.requestId; // quote it when you contact us error.invalidParams; // [{ field, reason, example? }] on a 400 } if (error instanceof APIConnectionError || error instanceof APITimeoutError) { // no answer at all } // Apply your fallback.}| Class | Thrown for |
|---|---|
BadRequestError |
400 and 422 |
AuthenticationError |
401 |
PermissionDeniedError |
403 |
NotFoundError |
404 |
ConflictError |
409 |
RateLimitError |
429 |
APIStatusError |
Any other 4xx or 5xx, including 503, and a 2xx the client could not read (code invalid_response) |
APIConnectionError |
A failed connection, DNS or TLS |
APITimeoutError |
A request or a wait that timed out |
APIConnectionError and APITimeoutError extend HTTPClientError rather than NoResponseError, so check for them separately. The codes are listed on Errors and limits.
Retries
Section titled “Retries”The client retries connection errors, 408, 429 and every 5xx answer with exponential backoff: up to 1 second before the first retry, then 0.5, 2 and 4.5 seconds plus up to 1 second of jitter, never more than 8 seconds between attempts and 30 seconds in all. When the answer carries Retry-After, the client waits that long instead, up to 8 seconds. A 409 and every other 4xx are never retried. Every attempt of a create or outcome call sends the same Idempotency-Key, so a retried call is recorded once.
Turn retries off, or change them, per client with retryConfig or per call with retries:
await nr.assessments.create(params, { retries: { strategy: "none" } });Webhooks
Section titled “Webhooks”import express from "express";import { NoResponse, isExpiredEvent } from "@noresponse/node";
const nr = new NoResponse();const secrets = { [process.env.NORESPONSE_WEBHOOK_KEY_ID!]: process.env.NORESPONSE_WEBHOOK_SECRET! };const app = express();
app.post("/webhooks/noresponse", express.raw({ type: "application/json" }), (req, res) => { let event; try { event = nr.webhooks.constructEvent(req.body as Buffer, req.headers, secrets, { toleranceSeconds: 300 }); } catch { return res.status(400).end(); } if (event.type === "endpoint.verification") { return res.status(200).json(nr.webhooks.verificationResponse(event)); } if (isExpiredEvent(event)) { forget(event.data.assessmentId); } else if (isNewer(event.id, event.data)) { apply(event.data); // a full Assessment at this assessmentVersion } res.status(200).end();});constructEvent(payload, headers, secrets, { toleranceSeconds = 300 }) takes the raw body as a Buffer, a Uint8Array or the exact text, any headers object (a Fetch Headers, Node’s IncomingHttpHeaders, a plain object or entries) and a map from webhook key id (whk_ and 26 characters) to secret. It checks that each NR-Webhook-* header is present exactly once, that the timestamp is within the tolerance, that the key id names a secret you hold, that the signature over timestamp + "." + raw body matches in constant time, and that the body’s id equals NR-Webhook-Id. A failure throws WebhookVerificationError, whose code says which check failed. The same functions are exported on their own and on NoResponse.webhooks.
The returned event is typed by type. An assessment.updated event’s data is an Assessment, or the expiry notice, which isExpiredEvent(event) narrows to. An endpoint.verification event carries the challenge, and verificationResponse(event) returns the object to answer with.
Pass the raw bytes, never a re-serialised body. Deduplicate on event.id and apply an assessment only when its assessmentVersion is higher than the one you hold; an expiry is always applied. See Webhooks.
Auth adapters
Section titled “Auth adapters”import { fromClerk, fromWorkOS, fromAuth0, fromOIDC } from "@noresponse/node";
const authContext = fromClerk(verifiedClaims, { emailVerified: true, accountCreatedAt: user.createdAt });fromClerk(claims, overrides?), fromWorkOS(claims, overrides?), fromAuth0(claims, overrides?) and fromOIDC(provider, claims, overrides?) take the claims your backend has already verified and return an authContext with verified: true and transport: "backend". Each reads the standard claims (issuer, subject, audience, session and token ids, times), the provider’s organisation, role and scope claims, and multi-factor evidence where the provider exposes it. overrides sets facts the token leaves out, such as emailVerified, accountCreatedAt, sessionCreatedAt or mfa. Every adapter applies the contract’s bounds, dropping over-long values rather than truncating them. Pass verified claims, never the raw token.
Logging
Section titled “Logging”Set NORESPONSE_LOG=debug or pass { logger } to the client. Each request and response logs one line with the method, URL, status and request id. The API key, other headers and bodies stay out of the log.
Standalone functions
Section titled “Standalone functions”Every operation is also a tree-shakeable function that returns a result object instead of throwing:
import { NoResponseCore } from "@noresponse/node";import { assessmentsCreate } from "@noresponse/node/funcs/assessments-create.js";
const core = new NoResponseCore({ apiKey: process.env.NORESPONSE_API_KEY! });const result = await assessmentsCreate(core, { idempotencyKey: "signup-usr_8f2k1-01JB3C4D",});if (result.ok) console.log(result.value.result.status);Every request, response, webhook and error type is exported: Assessment, AssessmentRequest, AssessmentOutcome, KeyRotation, WebhookEvent, InvalidParam and the rest. Enum fields such as status, recommendation and band accept values a newer API version adds, so handle an unrecognised value with your fallback.