Skip to content

Pending, retries and idempotency

An assessment is answered within 500 ms whatever the sources did. This page covers the answers that are not final yet, the key that makes a retry safe, and the retry policy built into the SDKs.

When no source answers before the deadline, POST /v1/assessments answers 202 with status: "pending". The body has no recommendation and no risk, confidence is unavailable, and incomplete_sources names the lanes still running. Two headers tell you what to do next.

Header Meaning
Location The polling URL, /v1/assessments/{assessment_id}. The same value is in the body as assessment_url.
Retry-After Whole seconds to wait before you poll. Never poll faster.
HTTP/1.1 202 Accepted
Location: /v1/assessments/asm_01M5FNFQWDRBZZMWBPN0WEC40C
Retry-After: 1

A 200 partial answer carries Retry-After too while a lane is still running, and GET /v1/assessments/{assessment_id} keeps carrying it until the assessment is complete. The GET always returns the highest assessment_version. An unknown, expired or malformed id answers 404, as does another platform’s id.

The SDKs have a wait helper that polls the assessment, sleeps for Retry-After or at least one second between polls, and returns the first answer that is not pending. It gives up with a timeout error (APITimeoutError in TypeScript and Python, a *noresponse.WaitTimeoutError in Go) after 30 seconds unless you set a shorter limit. Apply your fallback first and let the wait run beside the signup, or move it to a worker. A webhook is the better long-term path, because it also delivers the revisions after a partial answer.

const settled = await nr.assessments.wait("asm_01M5FNFQWDRBZZMWBPN0WEC40C", { timeoutMs: 10_000 });
console.log(settled.status, settled.assessmentVersion, settled.risk?.score);

POST /v1/assessments and POST /v1/assessments/{assessment_id}/outcomes require an Idempotency-Key header. The key is 16 to 128 characters of letters, digits, ., _, : and -, so a UUID with a prefix fits. It is scoped to your platform, so it never collides with another customer’s key.

Before any source work starts, NoResponse binds the key to the request body and to a new assessment_id, and keeps that binding for 24 hours from first use. The body is compared exactly as sent, before any normalisation, so [email protected] and [email protected] are different bodies.

You send You get
A new key A new assessment.
The same key and the same body, within 24 hours The latest revision of the same assessment, with the header Idempotent-Replayed: true. No new work starts, even while the first request is still in flight. The status is 202 while it is pending and 200 once it has a recommendation.
The same key and a different body 409 idempotency_key_reused.

Derive the key from your own attempt id, such as signup- followed by the UUID you assign when the person submits the form. Reuse it for every retry of that attempt. Use a new key when the person submits again with different details.

When you leave the key out, the SDK generates a UUID v4 and reuses it across the retries of that one call. The SDK exposes the headers that matter on the returned assessment: meta.requestId, meta.retryAfter, meta.location and meta.idempotentReplayed in TypeScript, the same names in snake_case in Python, and the X-Request-ID, Retry-After, Location and Idempotent-Replayed response headers on res.Headers in Go.

const assessment = await nr.assessments.create(params, { idempotencyKey: "signup-7c1f0a2b-3d4e-4f5a-9b6c-1d2e3f4a5b6c" });
if (assessment.meta.idempotentReplayed) console.log("replayed", assessment.meta.requestId);

An outcome’s key stays bound for as long as the outcome is kept, rather than 24 hours. GET /v1/assessments/{assessment_id} and POST /v1/assessments/query are read-only and take no key. POST /v1/keys/rotate takes none either, and a retry mints another key.

Every SDK retries the same failures and keeps the same Idempotency-Key across attempts.

  • A retry happens on a connection error, a timeout, a 408, a 429 and a 5xx.
  • A 409 and every other 4xx are not retried. They mean the request itself is wrong.
  • The same Idempotency-Key is sent on every attempt of a create or outcome call, so a retry replays the first attempt rather than starting a second assessment.

The timing differs by SDK.

SDK Attempts Between attempts Retry-After
Python 3 by default; set max_retries A random time up to the smaller of 8 seconds and 0.5 seconds doubled per attempt Slept up to 60 seconds. A longer value raises RateLimitError for a 429 or APIStatusError for a 503 at once, so your fallback runs.
Go As many as fit in 30 seconds; set WithRetryConfig or operations.WithRetries Exponential backoff with jitter from 500 ms, at most 8 seconds Honoured.
Node As many as fit in 30 seconds; set retryConfig or retries Up to 1 second, then 0.5, 2 and 4.5 seconds plus up to 1 second of jitter, at most 8 seconds Slept up to 8 seconds.