Skip to content

Assess signups and logins

POST /v1/assessments is the one call your platform makes at signup or login. This page covers every request field, the answers, and the fallback you choose before you go live.

const assessment = await nr.assessments.create(
{
accountId: "usr_8f2k1",
identifiers: { email: "[email protected]", name: "Jordan Lee", ip: "81.2.69.160" },
policyContext: { flow: "signup", region: "GB" },
requestId: "req-2026-10-21-0001",
deadlineMs: 400,
clientContext: {
timezone: "Europe/London",
locale: "en-GB",
platform: "web",
userAgent: req.headers["user-agent"],
sessionId: signupSession.id,
formDurationMs: 8400,
},
authContext,
},
{ idempotencyKey: `signup-${signupAttempt.id}` },
);

The body is capped at 32,768 bytes; a larger one answers 413. A Content-Type other than application/json answers 415.

Your platform’s own opaque id for the account, 1 to 128 characters of letters, digits, ., _, :, | and -. The vertical bar is allowed so an Auth0 id such as auth0|64abc passes unchanged. Use your internal id or a hash of it, not an email or phone.

At least one of email, username or phone. name and ip sharpen the match.

Field Rules
email One address, 3 to 254 characters, with a local part of at most 64. Nothing is trimmed. The local part keeps its case, so [email protected] and [email protected] are different identifiers. Only the domain is lower-cased, and no +tag or dot is stripped.
username 1 to 64 characters with no whitespace. Compared case-insensitively, so JaneDoe and janedoe are one username. A value shaped like a password hash or JWT is refused.
phone Strict E.164: +, then 8 to 15 digits, no separators. Phone sources run after the synchronous answer, so a phone-only request answers 202 pending.
name The full name as typed, 1 to 128 characters in one string. Matched against the names found in breach and caller-ID records.
ip The client’s public address, as an IPv4 dotted quad or an RFC 5952 IPv6 address. Private, loopback and documentation addresses answer 400.

flow names the event the assessment gates: signup, login, recovery or profile_change. Absent means signup. region is the ISO 3166-1 alpha-2 country your platform believes the signup is in. NoResponse compares it with the IP, phone and breach countries in risk.summary.consistency.country.

deadline_ms lowers the 500 ms budget, from 50 to 500, measured from the first byte NoResponse reads to the last it writes. A value above 500 answers 400. Sources that miss the budget appear in incomplete_sources and keep running.

response_mode is standard by default. detailed adds signals[] and risk.details and needs the detailed scope.

Optional hints from the signup client. A client can forge them, so they carry low weights and work as corroboration for the identifier evidence.

Field Rules
timezone An IANA name such as Europe/London. Numeric offsets are refused.
locale A BCP 47 tag such as en-GB.
platform web, ios, android or other.
app_version Up to 32 characters.
user_agent The raw header, up to 512 characters. Parsed for automation signs.
device_id, session_id Opaque ids you generate, up to 128 characters, used for velocity and replay checks within your organisation.
form_duration_ms Milliseconds from form start to submit. Under three seconds is a weak automation hint.
client_observed_at The client’s RFC 3339 timestamp.

request_id is your own correlation id, up to 128 characters, echoed back. auth_context carries facts from the session your auth provider verified; see Pass your auth session.

Answer Meaning
200 complete Every source settled. risk is final.
200 partial risk is present and incomplete_sources names a lane still running or failed.
202 pending No usable answer before the deadline, so no risk. Location is the polling URL and Retry-After the wait.
429 rate_limited Your platform’s rate limit is used up. Retry-After says when to retry.
503 service_unavailable The assessment could not be admitted. Retry-After is present.

A 400 names the offending field in invalid_params, and a 409 means the Idempotency-Key was reused with a different body. Pending, retries and idempotency covers 202 and 409 in full.

Four cases give you no recommendation: a pending answer, a 429, a 503 and a transport timeout. Pick one of two fallbacks before you go live.

The first is fail-open. Your platform creates the account but marks it provisional: no free credits, a low rate limit, or no API key until the final assessment arrives by webhook or polling. Your code then runs its rules and lifts or keeps the limits. Signup stays fast for real customers.

The second is step-up. Your platform asks for a second factor, such as an email link or an SMS code, before it creates the account or hands over anything of value. This costs the person a step and keeps abusive signups out from the start.

Many platforms use fail-open for login and step-up for signup. Whichever you choose, record the case as your fallback rather than allow, so you can see later which accounts were never scored.

import { APIConnectionError, APIStatusError, NoResponse, RateLimitError } from "@noresponse/node";
async function assess(params: Parameters<typeof nr.assessments.create>[0], idempotencyKey: string) {
try {
const assessment = await nr.assessments.create(params, { idempotencyKey });
if (assessment.status === "pending") return { decision: fallback(), assessment };
return { decision: decide(assessment), assessment };
} catch (error) {
if (error instanceof RateLimitError || error instanceof APIConnectionError) return { decision: fallback() };
if (error instanceof APIStatusError && error.status === 503) return { decision: fallback() };
throw error; // a 4xx is a bug in the request, so fix it rather than fall back
}
}