Java SDK
The NoResponse Java SDK is the server-side client for Java 11 and later. It covers every route in the interactive reference, verifies webhooks and turns your auth provider’s verified claims into authContext. Requests and responses are typed, and every method has a blocking form, a builder form and an asynchronous twin that returns CompletableFuture.
Keep the client on your backend, where your API key lives.
Install
Section titled “Install”JitPack builds the SDK from the tagged release at github.com/noresponsehq/java. With Gradle:
repositories { mavenCentral() maven { url 'https://jitpack.io' }}
dependencies { implementation 'com.github.noresponsehq:java:v2.0.0'}With Maven:
<repositories> <repository> <id>jitpack.io</id> <url>https://jitpack.io</url> </repository></repositories>
<dependency> <groupId>com.github.noresponsehq</groupId> <artifactId>java</artifactId> <version>v2.0.0</version></dependency>Use the latest tag from the repository’s tags page.
Client
Section titled “Client”import org.noresponse.NoResponse;
NoResponse client = NoResponse.builder() .apiKey(System.getenv("NORESPONSE_API_KEY")) .serverURL("https://api.noresponse.org") .build();| Builder method | Meaning |
|---|---|
apiKey(String) |
Your nrk_ key. Read it from NORESPONSE_API_KEY as above. |
securitySource(SecuritySource) |
Supplies the key on each request, for a secret that rotates. |
serverURL(String) |
The API host. The default is https://api.noresponse.org. |
retryConfig(RetryConfig) |
Overrides the retry policy for every call. See Retries. |
client(HTTPClient) |
Your own HTTP client, for a proxy or custom timeouts. |
enableHTTPDebugLogging(boolean) |
Logs requests and responses while you debug. |
Connections time out after 10 seconds and each request after 60 seconds.
Methods
Section titled “Methods”| Call | Does |
|---|---|
client.assessments().create(idempotencyKey, request) |
POST /v1/assessments. Answers 200 (complete or partial) or 202 (pending). |
client.assessments().get(assessmentId) |
GET /v1/assessments/{id}. |
Wait.wait(client, assessmentId) |
Polls get until the assessment is no longer pending. |
client.assessments().query(query) |
POST /v1/assessments/query. One page of an account’s history. |
AllAssessments.stream(client, query) |
A lazy Stream<Assessment> over every page. |
client.assessments().recordOutcome(assessmentId, idempotencyKey, request) |
POST /v1/assessments/{id}/outcomes. |
client.keys().rotate(request) |
POST /v1/keys/rotate. |
Webhook.constructEvent(payload, headers, secrets) |
Verifies a webhook delivery. No request is made. |
Each method also has a builder form, for example client.assessments().create().idempotencyKey(key).body(request).call(), and an asynchronous twin from client.async(). Every response has statusCode(), headers() (including X-Request-ID, Retry-After and Idempotent-Replayed) and the typed body.
Create an assessment
Section titled “Create an assessment”import org.noresponse.IdempotencyKey;import org.noresponse.NoResponse;import org.noresponse.models.operations.CreateAssessmentResponse;import org.noresponse.models.shared.Assessment;import org.noresponse.models.shared.AssessmentRequest;import org.noresponse.models.shared.AssessmentRequestClientContext;import org.noresponse.models.shared.AssessmentRequestIdentifiers;import org.noresponse.models.shared.AssessmentRequestPolicyContext;import org.noresponse.models.shared.Flow;import org.noresponse.models.shared.ResponseMode;import org.noresponse.models.shared.Status;
NoResponse client = NoResponse.builder() .apiKey(System.getenv("NORESPONSE_API_KEY")) .build();
CreateAssessmentResponse res = client.assessments().create( "signup-usr_8f2k1-01JB3C4D", AssessmentRequest.builder() .accountId("usr_8f2k1") .identifiers(AssessmentRequestIdentifiers.builder() .name("Jordan Lee") .ip("81.2.69.160") .build()) .policyContext(AssessmentRequestPolicyContext.builder() .flow(Flow.SIGNUP) .region("GB") .build()) .requestId("req_01") .deadlineMs(400L) .responseMode(ResponseMode.STANDARD) .clientContext(AssessmentRequestClientContext.builder() .timezone("Europe/London") .locale("en-GB") .build()) .build());
Assessment assessment = res.assessment().orElseThrow();if (Status.PENDING.equals(assessment.status())) { // No recommendation yet. Apply your fallback, then wait or take the webhook.} else { System.out.println(assessment.recommendation().get() + " " + assessment.risk().get().score());}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. The recommendation is allow, step_up or review, and it is advice: your application decides what to do.
The method returns the assessment for both a 200 and a 202, and res.statusCode() tells them apart. complete and partial carry risk() and recommendation(); pending carries neither.
Pending and wait
Section titled “Pending and wait”Assessment settled = Wait.wait(client, assessment.assessmentId());Wait.wait polls get until status() is not pending, sleeping for Retry-After between polls with a one second floor. It throws WaitTimeoutException when 30 seconds pass, and the exception carries assessmentId() and the last pending assessment as last(). Pass a Duration as the third argument to change the limit. It also throws InterruptedException if the thread is interrupted while sleeping. Apply your fallback first and let the result arrive by webhook or a background wait, so the signup is never held open.
Assessment latest = client.assessments().get() .assessmentId(assessment.assessmentId()) .responseMode(ResponseMode.DETAILED) .call() .assessment() .orElseThrow();Detailed responses need the detailed scope.
Query and paging
Section titled “Query and paging”import org.noresponse.AllAssessments;import org.noresponse.models.shared.AssessmentList;import org.noresponse.models.shared.AssessmentQuery;
AssessmentQuery query = AssessmentQuery.builder().accountId("usr_8f2k1").limit(50L).build();
AssessmentList page = client.assessments().query(query).assessmentList().orElseThrow();page.items(); // List<Assessment>, newest firstpage.nextCursor(); // Optional<String>, empty on the last page
AllAssessments.stream(client, query) .forEach(a -> System.out.println(a.assessmentId() + " " + a.status()));AllAssessments.stream follows the cursor lazily, one request per page. To page by hand, pass nextCursor() to AssessmentQuery.builder().cursor(...). The query needs a key with the history scope. See Get and query assessments.
Record an outcome
Section titled “Record an outcome”import org.noresponse.models.shared.AssessmentOutcomeRequest;import org.noresponse.models.shared.Outcome;
client.assessments().recordOutcome( assessment.assessmentId(), "outcome-case-123", AssessmentOutcomeRequest.builder() .outcome(Outcome.CONFIRMED_ABUSE) .observedAt("2026-10-20T05:00:00Z") .externalRef("case-123") .notes("Card testing from this account.") .build());Outcome has CONFIRMED_ABUSE, ACCOUNT_TAKEOVER, CHARGEBACK, FALSE_POSITIVE, LEGITIMATE and UNKNOWN. The idempotency key stays bound for as long as the outcome is kept, so derive it from your case.
Key rotation
Section titled “Key rotation”import org.noresponse.models.shared.KeyRotation;import org.noresponse.models.shared.KeyRotationRequest;
KeyRotation rotation = client.keys() .rotate(KeyRotationRequest.builder().overlapSeconds(86_400L).build()) .keyRotation() .orElseThrow();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.
Idempotency keys
Section titled “Idempotency keys”create and recordOutcome require an idempotency key. The same key with the same body replays the stored answer and starts no new work. The same key with a different body is a 409. Use IdempotencyKey.generate() for a fresh key, and store it beside the signup so a retry after a crash sends the same one. Pass a key derived from your signup attempt, as above, when a retry from your side must replay the same assessment.
Errors
Section titled “Errors”Every API failure is a NoResponseException, which is a RuntimeException. Problem documents are typed and carry the request id.
import java.io.UncheckedIOException;import org.noresponse.models.errors.NoResponseException;import org.noresponse.models.errors.NotFoundException;import org.noresponse.models.errors.RateLimitException;
try { client.assessments().get("asm_01JA2Z8K6Q4V7W9XBCDEFGHJKM");} catch (NotFoundException e) { System.out.println("no such assessment, request " + e.requestId().orElse("unknown"));} catch (RateLimitException e) { System.out.println("rate limited, request " + e.requestId().orElse("unknown"));} catch (NoResponseException e) { System.out.println("HTTP " + e.code() + " " + e.bodyAsString().orElse(""));} catch (UncheckedIOException e) { // no answer at all: network failure or timeout}// Apply your fallback.| Class | Thrown for |
|---|---|
BadRequestException |
400 |
AuthenticationException |
401 |
PermissionDeniedException |
403 |
NotFoundException |
404 |
ConflictException |
409 |
RateLimitException |
429 |
APIStatusException |
Any other 4xx or 5xx, including 503 |
All of them are in org.noresponse.models.errors, and each exposes code(), headers(), body() and rawResponse(). Quote the request id when you contact us. 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 and jitter: 500 ms first, 8 seconds at most between attempts and 30 seconds in all. A 409 and every other 4xx are not retried. Every attempt sends the same idempotency key, so a retried call is recorded once.
Change the policy for the client, or for one call:
import java.util.concurrent.TimeUnit;import org.noresponse.utils.BackoffStrategy;import org.noresponse.utils.RetryConfig;
NoResponse client = NoResponse.builder() .apiKey(System.getenv("NORESPONSE_API_KEY")) .retryConfig(RetryConfig.builder() .backoff(BackoffStrategy.builder() .initialInterval(200, TimeUnit.MILLISECONDS) .maxInterval(2, TimeUnit.SECONDS) .maxElapsedTime(5, TimeUnit.SECONDS) .baseFactor(2.0) .jitterFactor(0.15) .retryConnectError(true) .build()) .build()) .build();
client.assessments().get().assessmentId(id).retryConfig(RetryConfig.noRetries()).call();Webhooks
Section titled “Webhooks”Pass the raw request body. Re-serialising parsed JSON breaks the signature.
import java.util.Map;import org.noresponse.webhooks.VerificationException;import org.noresponse.webhooks.Webhook;import org.noresponse.webhooks.WebhookEvent;
Map<String, String> secrets = Map.of( System.getenv("NORESPONSE_WEBHOOK_KEY_ID"), System.getenv("NORESPONSE_WEBHOOK_SECRET"));
try { // rawBody is a byte[]; headers is a Map<String, List<String>> of the request headers. WebhookEvent event = Webhook.constructEvent(rawBody, headers, secrets); event.verification().ifPresent(v -> { /* answer with Webhook.verificationResponse(event) */ }); event.expired().ifPresent(e -> { /* delete what you hold for e.assessmentId() */ }); event.assessment().ifPresent(a -> { /* the full assessment, as get returns it */ });} catch (VerificationException e) { // answer 400 and do not process the delivery}constructEvent(payload, headers, secrets) takes the raw body as byte[], a map of header names to values in any letter case, 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 300 seconds, 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 VerificationException, whose code() is one of MISSING_HEADER, DUPLICATE_HEADER, INVALID_TIMESTAMP, UNKNOWN_KEY, INVALID_SIGNATURE or INVALID_PAYLOAD. A second overload takes an explicit tolerance in seconds and a clock.
WebhookEvent exposes id(), type(), createdAt() and isExpired(), plus the typed payload as assessment(), expired() or verification(). Deliveries arrive at least once and unordered. Deduplicate on event.id(), apply an assessment only when its assessmentVersion() is higher than the one you hold, and always apply an expiry. See Webhooks.
Auth adapters
Section titled “Auth adapters”import java.util.Map;import org.noresponse.Auth;import org.noresponse.models.shared.AuthContext;
AuthContext authContext = Auth.fromClerk(verifiedClaims, Map.of("email_verified", true));Auth has fromWorkOS, fromClerk, fromAuth0, fromSupabase, fromFirebase and fromOIDC(provider, claims, overrides). Each takes the claims your backend has already verified, as a Map<String, ?>, and returns an AuthContext with verified true and transport set to backend. Pass the result to AssessmentRequest.builder().authContext(...).
Each adapter 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. The optional second argument sets facts the token leaves out, using the field names email_verified, phone_verified, account_created_at, session_created_at and the like. For fromOIDC, provider is a lowercase slug of 2 to 32 characters. A claim that is missing or out of range is left out, never turned into a stronger assurance, and an adapter throws IllegalArgumentException when the claims have no valid issuer and subject. Pass verified claims, never the raw token.