.NET SDK
The NoResponse .NET SDK is the server-side client for .NET 8 and later. It covers every route in the interactive reference, verifies webhooks and turns your auth provider’s verified claims into AuthContext. Every call is asynchronous, takes a CancellationToken and returns an envelope: Result holds the typed object and Headers holds the response headers (X-Request-ID, Retry-After, Idempotent-Replayed).
Keep the client on your backend, where your API key lives.
Install
Section titled “Install”There is no NuGet package yet. Clone the tag you want from github.com/noresponsehq/dotnet beside your solution and reference the project. Keep the clone outside your project folder, or the SDK’s source files compile twice.
# from the folder that holds your project foldergit clone --branch v2.0.0 https://github.com/noresponsehq/dotnet.git noresponse-dotnetdotnet add MyApp reference noresponse-dotnet/src/NoResponse/NoResponse.csprojIn a repository of your own, add it as a submodule and check out the tag before you commit:
git submodule add https://github.com/noresponsehq/dotnet.git third_party/noresponse-dotnetgit -C third_party/noresponse-dotnet checkout v2.0.0git add third_party/noresponse-dotnetUse the latest tag from the repository’s tags page.
Client
Section titled “Client”using NoResponse;using NoResponse.Models.Shared;
var client = new NoResponseClient( apiKey: Environment.GetEnvironmentVariable("NORESPONSE_API_KEY")!);| Option | Meaning |
|---|---|
apiKey |
Your nrk_ key. Read it from NORESPONSE_API_KEY as above. |
apiKeySource |
A Func<string> that supplies the key on each request, for a secret that rotates. |
serverUrl |
The API host. The default is https://api.noresponse.org. |
client |
An INoResponseHttpClient, for timeouts, proxies and logging. |
retryConfig |
Overrides the retry policy for every call. See Retries. |
NoResponseClient.Builder().WithApiKey(key).Build() does the same. The default NoResponseHttpClient uses a shared HttpClient; derive from it or implement INoResponseHttpClient to set a timeout.
Methods
Section titled “Methods”| Call | Does |
|---|---|
client.Assessments.CreateAsync(body, idempotencyKey?) |
POST /v1/assessments. Answers 200 (complete or partial) or 202 (pending). |
client.Assessments.GetAsync(assessmentId, responseMode?) |
GET /v1/assessments/{id}. |
client.Assessments.WaitAsync(assessmentId, timeout?) |
Polls GetAsync until the assessment is no longer pending. |
client.Assessments.QueryAsync(query) |
POST /v1/assessments/query. One page of an account’s history. |
client.Assessments.RecordOutcomeAsync(assessmentId, body, idempotencyKey?) |
POST /v1/assessments/{id}/outcomes. |
client.Keys.RotateAsync(request) |
POST /v1/keys/rotate. |
Webhook.ConstructEvent(payload, headers, secrets) |
Verifies a webhook delivery. No request is made. |
Every request method also takes retryConfig and cancellationToken.
Create an assessment
Section titled “Create an assessment”using NoResponse;using NoResponse.Models.Shared;
var client = new NoResponseClient( apiKey: Environment.GetEnvironmentVariable("NORESPONSE_API_KEY")!);
var response = await client.Assessments.CreateAsync( new AssessmentRequest { AccountId = "usr_8f2k1", Identifiers = new AssessmentRequestIdentifiers { Name = "Jordan Lee", Ip = "81.2.69.160", }, PolicyContext = new AssessmentRequestPolicyContext { Flow = Flow.Signup, Region = "GB", }, RequestId = "req_01", DeadlineMs = 400, ResponseMode = ResponseMode.Standard, ClientContext = new AssessmentRequestClientContext { Timezone = "Europe/London", Locale = "en-GB", }, }, idempotencyKey: "signup-usr_8f2k1-01JB3C4D");
var assessment = response.Result;if (Status.Pending.Equals(assessment.Status)){ // No recommendation yet. Apply your fallback, then wait or take the webhook.}else{ Console.WriteLine(assessment.Recommendation); // allow, step_up or review}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 advice: your application decides what to do.
The method returns the assessment for both a 200 and a 202. complete and partial carry Risk and Recommendation; pending carries neither.
Pending and wait
Section titled “Pending and wait”var settled = await client.Assessments.WaitAsync( assessment.AssessmentId, timeout: TimeSpan.FromSeconds(10));WaitAsync polls GetAsync until the status is no longer pending, sleeping for Retry-After between polls with a one second floor. It throws WaitTimeoutException, which carries AssessmentId and the last pending assessment as Last, when the timeout (30 seconds by default) passes. Apply your fallback first and let the result arrive by webhook or a background wait, so the signup is never held open.
var latest = await client.Assessments.GetAsync( assessment.AssessmentId, responseMode: ResponseMode.Detailed);Detailed responses need the detailed scope.
Query and paging
Section titled “Query and paging”var page = await client.Assessments.QueryAsync( new AssessmentQuery { AccountId = "usr_8f2k1", Limit = 50 });
while (page != null){ foreach (var item in page.Result.Items) { // newest first } page = await page.Next!();}page.Next fetches the following page and returns null after the last one. The query needs a key with the history scope. See Get and query assessments.
Record an outcome
Section titled “Record an outcome”await client.Assessments.RecordOutcomeAsync( assessment.AssessmentId, new AssessmentOutcomeRequest { Outcome = Outcome.ConfirmedAbuse, ObservedAt = "2026-10-20T05:00:00Z", ExternalRef = "case-123", Notes = "Card testing from this account.", }, idempotencyKey: "outcome-case-123");Outcome has ConfirmedAbuse, AccountTakeover, Chargeback, FalsePositive, 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”var rotation = await client.Keys.RotateAsync( new KeyRotationRequest { OverlapSeconds = 86_400 });
rotation.Result.Key; // the new key, shown oncerotation.Result.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”CreateAsync and RecordOutcomeAsync send a fresh UUID as the Idempotency-Key when you pass none, and the same key on every retry of that call, so a retried request is recorded once. Pass your own key, derived from your signup attempt, to make a retry from your side replay the same assessment: the API answers the latest version with Idempotent-Replayed: true. The same key with a different body answers 409 (ConflictException).
Errors
Section titled “Errors”Every API error derives from NoResponseException and carries StatusCode, Headers, Body and, for problem documents, a typed Payload with Type, Title, Detail, Code, RequestId and InvalidParams.
using System.Net.Http;
try{ await client.Assessments.CreateAsync(request);}catch (RateLimitException e){ Schedule(e.Headers.GetValues("Retry-After"));}catch (NoResponseException e){ Log(e.StatusCode, e is BadRequestException b ? b.Payload.RequestId : null);}catch (HttpRequestException){ // DNS, TLS or connection failure: no answer at all.}// Apply your fallback.| Exception | When |
|---|---|
BadRequestException |
400: the body or a header is malformed. See Payload.InvalidParams. |
AuthenticationException |
401: a missing, malformed, revoked or expired key. |
PermissionDeniedException |
403: the key lacks the operation’s scope. |
NotFoundException |
404: an unknown, expired or foreign assessment. |
ConflictException |
409: an idempotency key reused with a different body, or a key already rotating. |
RateLimitException |
429: see the Retry-After header. |
APIStatusException |
Any other 4xx or 5xx, including 503. |
ResponseValidationException |
A 2xx the client could not read. |
Quote RequestId 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: 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 of a create or outcome call sends the same Idempotency-Key, so a retried call is recorded once.
Override the policy for the client with retryConfig: or for one call with retryConfig: on the method:
await client.Assessments.CreateAsync( request, retryConfig: new RetryConfig(RetryConfig.RetryStrategy.NONE));Webhooks
Section titled “Webhooks”Verify the exact bytes you received, then act on the typed event. secrets maps each whk_ key id to its secret; hold both during a rotation.
using NoResponse;using NoResponse.Models.Shared;using NoResponse.Webhooks;
app.MapPost("/webhooks/noresponse", async (HttpRequest request) =>{ using var buffer = new MemoryStream(); await request.Body.CopyToAsync(buffer); try { var secrets = new Dictionary<string, string> { [Environment.GetEnvironmentVariable("NORESPONSE_WEBHOOK_KEY_ID")!] = Environment.GetEnvironmentVariable("NORESPONSE_WEBHOOK_SECRET")!, }; var webhookEvent = Webhook.ConstructEvent(buffer.ToArray(), request.Headers, secrets);
if (webhookEvent.Type == "endpoint.verification") { return Results.Json(Webhook.VerificationResponse(webhookEvent)); } if (webhookEvent.IsExpired) { // Forget the assessment: GET now answers 404. } else if (webhookEvent.Data is Assessment assessment) { // Keep the highest AssessmentVersion. } return Results.NoContent(); } catch (WebhookVerificationException) { return Results.BadRequest(); }});ConstructEvent(payload, headers, secrets, tolerance?, now?) takes the raw body as byte[], any collection of header key and value pairs (ASP.NET Core’s request.Headers and HttpHeaders both work) 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 the tolerance (300 seconds by default), 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 WebhookVerificationException, whose Code is a WebhookErrorCode: MissingHeader, DuplicateHeader, InvalidTimestamp, UnknownKey, InvalidSignature or InvalidPayload.
The event has Id, Type, CreatedAt and Data. Data is an Assessment, an AssessmentExpired notice (IsExpired is true) or an EndpointVerification. Deliveries arrive at least once: deduplicate on webhookEvent.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”using NoResponse;
var authContext = Auth.FromClerk( verifiedClaims, new Dictionary<string, object> { ["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 Dictionary<string, object> or a JObject, and returns an AuthContext with Verified true and Transport set to backend. Set it on the request as AuthContext = 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 overrides set facts the token leaves out, using the contract’s 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, not turned into a stronger assurance, and an adapter throws ArgumentException when the claims have no valid issuer and subject. Pass verified claims, never the raw token.