Create an assessment
POST /v1/assessments assesses one signup, login, recovery or profile change and answers within 500 ms. The answer is an assessment: a 0 to 100 score, a band, a recommendation of allow, step_up or review, and the reasons behind it.
The call needs the assess scope and an Idempotency-Key header. Try it live in the interactive reference.
Request
Section titled “Request”import { NoResponse } from "@noresponse/node";
const nr = new NoResponse(); // reads NORESPONSE_API_KEY
const assessment = await nr.assessments.create( { accountId: "usr_8f2k1", policyContext: { flow: "signup", region: "GB" }, }, { idempotencyKey: "signup-usr_8f2k1-01JB3C4D" },);
console.log(assessment.status, assessment.risk?.score, assessment.risk?.recommendation);from noresponse import NoResponse
client = NoResponse() # reads NORESPONSE_API_KEY
assessment = client.assessments.create( account_id="usr_8f2k1", policy_context={"flow": "signup", "region": "GB"}, idempotency_key="signup-usr_8f2k1-01JB3C4D",)
risk = assessment.risk if assessment.status != "pending" else Noneprint(assessment.status, risk.score if risk else None)client := noresponse.NewClient(os.Getenv("NORESPONSE_API_KEY"))
res, err := client.Assessments.Create(ctx, "signup-usr_8f2k1-01JB3C4D", 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"), },})if err != nil { return err}
assessment := res.Resultfmt.Println(assessment.Status, assessment.Risk, http.Header(res.Headers).Get("X-Request-ID"))# The ip below is a placeholder: replace with the confirmed IP.curl https://api.noresponse.org/v1/assessments \ -H "Authorization: Bearer $NORESPONSE_API_KEY" \ -H "Content-Type: application/json" \ -H "Idempotency-Key: signup-usr_8f2k1-01JB3C4D" \ -d '{ "account_id": "usr_8f2k1", "identifiers": { "email": "[email protected]", "name": "Jordan Lee", "ip": "81.2.69.160" }, "policy_context": { "flow": "signup", "region": "GB" } }'The SDKs take the same nested body in each language’s own casing: identifiers and policy_context become identifiers and policyContext in Node, identifiers and policy_context in Python, and Identifiers and PolicyContext in Go. The sections below use the wire names.
Headers
Section titled “Headers”| Header | Required | Value |
|---|---|---|
Authorization |
Yes | Bearer and your API key. The key needs the assess scope, plus detailed when response_mode is detailed. |
Content-Type |
Yes | application/json. Anything else answers 415. |
Idempotency-Key |
Yes | 16 to 128 characters of A-Z, a-z, 0-9, ., _, : and -. A UUID or ULID fits. The key is bound to the body for 24 hours from first use. The same key with the same body replays the latest state of the same assessment; the same key with a different body answers 409. |
The body is a JSON object of at most 32,768 bytes. Unknown members are refused with 400, including any field that names your organisation: the API key identifies it.
| Field | Type | Required | Limits | Example |
|---|---|---|---|---|
account_id |
string | Yes | 1 to 128 characters: a letter or digit, then letters, digits, ., _, :, | and -. Your own opaque id for the account: your internal id, or a hex digest of it. A JWT is refused. The vertical bar is accepted, so an Auth0 id such as auth0|64abc passes unchanged. It travels in the body, not the URL. |
usr_8f2k1 |
identifiers |
object | Yes | At least one of email, username or phone. name and ip sharpen the match, so a body with only those two is refused. |
|
identifiers.email |
string | One of three | 3 to 254 characters: a local part of 1 to 64 characters without whitespace, angle brackets or invisible format characters, then @ and a domain with a letter top-level label. Send an internationalised domain in its punycode form. Nothing is trimmed. The local part keeps its case; only the domain is lower-cased. No alias rewriting: +tag and dots are kept as sent. |
[email protected] |
identifiers.username |
string | One of three | 1 to 64 characters with no whitespace, control or invisible format characters. Echoed as sent; compared case-insensitively, so JordanLee and jordanlee are one username. A value shaped like a password hash, crypt string or JWT is refused. |
jordan_lee |
identifiers.phone |
string | One of three | Strict E.164: +, then 8 to 15 digits, the first not 0, with no separators. Phone sources run after the synchronous answer, so a phone appears in incomplete_sources with reason deferred on the first answer, and a phone-only request answers 202 pending. |
+447700900123 |
identifiers.name |
string | No | 1 to 128 characters as the person typed it, in one string, with no control or invisible format characters. Used only to match names in the evidence. Not a hash, crypt string or JWT. | Jordan Lee |
identifiers.ip |
string | No | The signup’s client address: an IPv4 dotted quad without leading zeros, or an IPv6 address in its canonical lower-case compressed form. No zone, port or prefix. Send an IPv4 client as a dotted quad, not IPv4-mapped. Private, loopback, link-local, multicast, documentation and other reserved addresses answer 400. | 81.2.69.160 |
policy_context |
object | No | At least one member when present. | |
policy_context.flow |
string | No | signup, login, recovery or profile_change. Absent means signup. |
signup |
policy_context.region |
string | No | ISO 3166-1 alpha-2, upper case, as you determine it. Used in the country consistency check. | GB |
request_id |
string | No | 1 to 128 characters: a letter or digit, then letters, digits, ., _, : and -. Your correlation id, echoed back in the assessment. It is not an idempotency key. A UUID is accepted; a bare 40, 64 or 128 character hex digest or a JWT is refused. |
req_01 |
deadline_ms |
integer | No | 50 to 500. The end-to-end budget in milliseconds, measured from the first request byte to the last response byte. Absent means 500. A value above 500 answers 400 and is never clamped. | 300 |
response_mode |
string | No | standard or detailed. Default standard. detailed adds signals and risk.details and needs the detailed scope on the key. There is no raw mode. |
standard |
client_context |
object | No | Hints observed at your edge. At least one member when present. A client can forge them, so they carry low weights and corroborate the identifier evidence. | |
auth_context |
object | No | Facts your backend verified from your auth provider’s session. See the auth_context table below. |
client_context
Section titled “client_context”| Field | Type | Limits | Example |
|---|---|---|---|
timezone |
string | At most 64 characters. An IANA zone name such as Europe/London, or UTC. Fixed numeric offsets are refused. |
Europe/London |
locale |
string | At most 35 characters, BCP 47. A presentation hint, not a nationality. | en-GB |
platform |
string | web, ios, android or other. |
web |
app_version |
string | At most 32 characters: a letter or digit, then letters, digits, ., _, + and -. No build metadata that holds secrets. |
4.12.0 |
user_agent |
string | At most 512 characters, as captured at your edge. Parsed for automation signs. | Mozilla/5.0 ... |
device_id |
string | At most 128 characters: a letter or digit, then letters, digits, ., _, : and -. An opaque id you generate and scope to your organisation. Rotate it when the account or device changes. |
dev_3k9x |
session_id |
string | Same shape as device_id. An opaque signup session id, used for velocity and replay checks within your organisation. |
ses_71qa |
form_duration_ms |
integer | 0 to 86,400,000. Time from form start to submit. Under three seconds is an automation hint. | 18400 |
client_observed_at |
string | RFC 3339 timestamp from the client, at most 64 characters. A large skew from server time is a low-confidence feature. | 2026-10-14T09:00:00Z |
auth_context
Section titled “auth_context”Send auth_context only from your backend, after it has verified the session token’s signature, issuer, audience and expiry. Raw tokens and your auth provider’s secrets are refused. provider, issuer, subject and verified are required when the object is present. The SDK auth adapters build it from verified claims.
| Field | Type | Required | Limits |
|---|---|---|---|
provider |
string | Yes | 2 to 32 lower-case characters: a letter or digit, then letters, digits, ., _ and -. The slug of your auth provider. workos, clerk and auth0 have first-class adapters; anything else uses your own OIDC slug. The slug is metadata and does not change risk by itself. |
issuer |
string | Yes | At most 256 characters, an https:// URL. The verified token issuer. |
subject |
string | Yes | At most 256 characters of letters, digits and . _ : @ / | + = ~ -. The verified sub, opaque to NoResponse. |
verified |
boolean | Yes | Whether your backend completed signature, issuer, audience and expiry validation. false contexts are accepted only as weak hints. |
audience |
string | No | At most 256 characters of letters, digits and . _ : / -. The verified audience your API uses. |
audiences |
array of string | No | At most 8 values of the same shape as audience. All verified audiences when the token carries more than one. |
session_id |
string | No | At most 256 characters, same shape as subject. The verified session id, sid or equivalent. |
organization_id |
string | No | Same shape. The active organisation or workspace id, when your auth provider issued one. |
tenant_id |
string | No | Same shape. Your auth provider’s tenant id, when distinct from organization_id. |
token_id |
string | No | Same shape. The verified jti, used for replay and rotation correlation. |
issued_at |
string | No | RFC 3339, at most 64 characters. The token’s iat. |
expires_at |
string | No | RFC 3339. The token’s exp. |
session_created_at |
string | No | RFC 3339. When the session was created, from your backend’s session record. Separate from token issuance and auth_time. |
auth_time |
string | No | RFC 3339. When the person last authenticated, when present. |
account_created_at |
string | No | RFC 3339. When the account was created, from your verified record. |
email_verified |
boolean | No | Whether your auth provider asserts the email was verified. |
phone_verified |
boolean | No | Whether your auth provider asserts the phone was verified. |
assurance_level |
string | No | unknown, low, medium or high, when you have mapped one. |
impersonated |
boolean | No | Whether the session acts on behalf of another subject. |
transport |
string | No | backend (default) or browser. Browser values are treated as weak hints. |
mfa |
object | No | At least one of present (boolean), methods (at most 8 strings of at most 32 characters) and age_seconds (0 to 315,360,000). |
roles |
array of string | No | At most 32 values of at most 64 characters. Roles do not change risk by themselves. |
scopes |
array of string | No | At most 32 values of at most 64 characters. Scopes do not change risk by themselves. |
Responses
Section titled “Responses”The API answers within deadline_ms with one of two success statuses. Sources that miss the deadline keep running and the assessment is revised later; see Get and query assessments and Webhooks.
| Status | Meaning | Body |
|---|---|---|
200 OK |
A complete or partial assessment with a recommendation. |
The assessment object. |
202 Accepted |
A pending assessment: nothing usable arrived before the deadline, so there is no recommendation yet. Poll Location no faster than Retry-After says, or wait for the assessment.updated webhook. |
The assessment object with status: "pending", confidence: "unavailable" and no risk. |
Apply your own fallback for pending, for a 429 or 503 and for a transport timeout. None of them is an allow.
Response headers
Section titled “Response headers”| Header | When | Value |
|---|---|---|
X-Request-ID |
Every response | The request’s correlation id: 32 lower-case hexadecimal characters. Quote it when you contact NoResponse. The SDKs expose it as meta.requestId in TypeScript, meta.request_id in Python and the X-Request-ID header on res.Headers in Go. |
Cache-Control |
Every response | no-store. |
Location |
202 | The polling path of the assessment, /v1/assessments/{assessment_id}. |
Retry-After |
202, 429 and 503 | Whole seconds to wait before polling or retrying, from 1 to 3600. |
Idempotent-Replayed |
200 and 202 when the answer replays an earlier request | true. The body is the latest state of the assessment that earlier request created, and no new work was started. Absent otherwise. |
WWW-Authenticate |
401 | Bearer realm="noresponse-assessments". |
A 200 answer
Section titled “A 200 answer”{ "schema_version": "1", "assessment_id": "asm_01JB3C4D5E6F7G8H9JKMNPQRST", "account_id": "usr_8f2k1", "status": "complete", "recommendation": "allow", "confidence": "high", "risk": { "score": 0, "band": "low", "recommendation": "allow", "confidence": "high", "policy_version": "risk-2026-10.3", "policy_basis": "heuristic_unvalidated", "reasons": [ { "code": "established_accounts", "weight": -15, "identifier": "email" }, { "code": "aged_identity", "weight": -25, "identifier": "email" } ], "summary": { "email": { "disposable": false, "accounts_found": 6, "breach": { "found": true, "breach_count": 7, "first_year": 2013, "last_year": 2021 } }, "ip": { "country": "GB", "type": "residential", "seen_in_tenant": 1 }, "consistency": { "name": "match", "country": "consistent" } }, "coverage": { "answered": 15, "failed": 0, "pending": 0 } }, "incomplete_sources": [], "policy_version": "risk-2026-10.3", "policy_basis": "heuristic_unvalidated", "assessment_version": 1, "created_at": "2026-10-18T11:00:00.000Z", "updated_at": "2026-10-18T11:00:00.412Z", "expires_at": "2027-10-18T11:00:00.000Z", "timing": { "deadline_ms": 500, "elapsed_ms": 412 }}Every field is described on The assessment object.
A 202 answer
Section titled “A 202 answer”HTTP/1.1 202 AcceptedContent-Type: application/jsonLocation: /v1/assessments/asm_01JA2Z8K6Q4V7W9XBCDEFGHJKMRetry-After: 1X-Request-ID: 0123456789abcdef0123456789abcdefCache-Control: no-store{ "schema_version": "1", "assessment_id": "asm_01JA2Z8K6Q4V7W9XBCDEFGHJKM", "account_id": "usr_8f2k1", "status": "pending", "confidence": "unavailable", "incomplete_sources": [ { "lane": "accounts", "reason": "timed_out" }, { "lane": "breach", "reason": "timed_out" } ], "policy_version": "heuristic-2026-10.1", "policy_basis": "heuristic_unvalidated", "assessment_version": 1, "created_at": "2026-10-14T09:00:00.000Z", "updated_at": "2026-10-14T09:00:00.412Z", "expires_at": "2027-10-14T09:00:00.000Z", "assessment_url": "/v1/assessments/asm_01JA2Z8K6Q4V7W9XBCDEFGHJKM", "timing": { "deadline_ms": 300, "elapsed_ms": 301 }}The SDKs return the same assessment model for 200 and 202. Read status to tell them apart, or call the wait helper (nr.assessments.wait, client.assessments.wait, client.Assessments.Wait) to poll until the assessment is no longer pending.
Errors
Section titled “Errors”| Status | Code | When |
|---|---|---|
| 400 | invalid_request |
The body breaks a rule above. invalid_params names the fields. |
| 400 | invalid_idempotency_key |
The header is missing or malformed. |
| 401 | unauthorized |
No key, or a malformed, unknown, revoked or expired key. |
| 403 | tenant_forbidden |
The key lacks assess, or detailed when asked for. |
| 409 | idempotency_key_reused |
The key was used with a different body within 24 hours. |
| 413 | payload_too_large |
The body is over 32,768 bytes. |
| 415 | unsupported_media_type |
Content-Type is not application/json. |
| 429 | rate_limited |
Your rate or concurrency limit is used up. Retry after Retry-After. |
| 503 | service_unavailable |
The assessment could not be started. Retry after Retry-After. |
Every error is a problem document; see Errors and limits.