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.
The request
Section titled “The request”const assessment = await nr.assessments.create( { accountId: "usr_8f2k1", 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}` },);assessment = client.assessments.create( account_id="usr_8f2k1", policy_context={"flow": "signup", "region": "GB"}, request_id="req-2026-10-21-0001", deadline_ms=400, client_context={ "timezone": "Europe/London", "locale": "en-GB", "platform": "web", "user_agent": request.headers["user-agent"], "session_id": signup_session.id, "form_duration_ms": 8400, }, auth_context=auth_context, idempotency_key=f"signup-{signup_attempt.id}",)res, err := client.Assessments.Create(ctx, "signup-"+signupAttempt.ID, components.AssessmentRequest{ AccountID: "usr_8f2k1", Identifiers: components.AssessmentRequestIdentifiers{ Name: noresponse.Pointer("Jordan Lee"), IP: noresponse.Pointer("81.2.69.160"), }, PolicyContext: &components.AssessmentRequestPolicyContext{ Flow: noresponse.Pointer(components.FlowSignup), Region: noresponse.Pointer("GB"), }, RequestID: noresponse.Pointer("req-2026-10-21-0001"), DeadlineMs: noresponse.Pointer(int64(400)), ClientContext: &components.AssessmentRequestClientContext{ Timezone: noresponse.Pointer("Europe/London"), Locale: noresponse.Pointer("en-GB"), Platform: noresponse.Pointer(components.PlatformWeb), UserAgent: noresponse.Pointer(r.UserAgent()), SessionID: noresponse.Pointer(signupSession.ID), FormDurationMs: noresponse.Pointer(int64(8400)), }, AuthContext: authContext,})curl https://api.noresponse.org/v1/assessments \ -H "Authorization: Bearer $NORESPONSE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: signup-7c1f0a2b-3d4e-4f5a-9b6c-1d2e3f4a5b6c" \ -d '{ "account_id": "usr_8f2k1", "identifiers": { "email": "[email protected]", "name": "Jordan Lee", "ip": "81.2.69.160" }, "policy_context": { "flow": "signup", "region": "GB" }, "request_id": "req-2026-10-21-0001", "deadline_ms": 400, "client_context": { "timezone": "Europe/London", "locale": "en-GB", "platform": "web", "form_duration_ms": 8400 } }'The body is capped at 32,768 bytes; a larger one answers 413. A Content-Type other than application/json answers 415.
account_id
Section titled “account_id”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.
identifiers
Section titled “identifiers”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. |
policy_context
Section titled “policy_context”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 and response_mode
Section titled “deadline_ms and response_mode”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.
client_context
Section titled “client_context”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 and auth_context
Section titled “request_id and auth_context”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.
The answers
Section titled “The answers”| 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.
Choose your fallback
Section titled “Choose your fallback”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 }}from typing import Any
from noresponse import APIConnectionError, APIStatusError, Assessment, RateLimitError
def assess(idempotency_key: str, **params: Any) -> tuple[str, Assessment | None]: try: assessment = client.assessments.create(idempotency_key=idempotency_key, **params) except (RateLimitError, APIConnectionError): return fallback(), None except APIStatusError as error: if error.status == 503: return fallback(), None raise # a 4xx is a bug in the request, so fix it rather than fall back if assessment.status == "pending": return fallback(), assessment return decide(assessment), assessmentfunc assess(ctx context.Context, request components.AssessmentRequest, key string) (string, *components.Assessment, error) { res, err := client.Assessments.Create(ctx, key, request) if err != nil { var limited *apierrors.RateLimitError var status *apierrors.APIStatusError var badRequest *apierrors.BadRequestError var unauthorised *apierrors.AuthenticationError var denied *apierrors.PermissionDeniedError var conflict *apierrors.ConflictError switch { case errors.As(err, &limited): return fallback(), nil, nil case errors.As(err, &status): if status.StatusCode >= 500 { return fallback(), nil, nil // a 503, for example } return "", nil, err case errors.As(err, &badRequest), errors.As(err, &unauthorised), errors.As(err, &denied), errors.As(err, &conflict): return "", nil, err // a 4xx is a bug in the request, so fix it rather than fall back } return fallback(), nil, nil // a failed connection or a timeout } assessment := res.Result if assessment.Status == components.StatusPending { return fallback(), &assessment, nil } return decide(&assessment), &assessment, nil}# Send the request with a 1 second client timeout. A non-zero exit, a 202, a 429# or a 503 means your fallback applies.curl --max-time 1 -s -o /tmp/assessment.json -w '%{http_code}' \ https://api.noresponse.org/v1/assessments \ -H "Authorization: Bearer $NORESPONSE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: signup-7c1f0a2b-3d4e-4f5a-9b6c-1d2e3f4a5b6c" \