Skip to content

.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.

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.

Terminal window
# from the folder that holds your project folder
git clone --branch v2.0.0 https://github.com/noresponsehq/dotnet.git noresponse-dotnet
dotnet add MyApp reference noresponse-dotnet/src/NoResponse/NoResponse.csproj

In a repository of your own, add it as a submodule and check out the tag before you commit:

Terminal window
git submodule add https://github.com/noresponsehq/dotnet.git third_party/noresponse-dotnet
git -C third_party/noresponse-dotnet checkout v2.0.0
git add third_party/noresponse-dotnet

Use the latest tag from the repository’s tags page.

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.

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.

Signup.cs
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
{
Email = "[email protected]",
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.

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.

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.

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.

var rotation = await client.Keys.RotateAsync(
new KeyRotationRequest { OverlapSeconds = 86_400 });
rotation.Result.Key; // the new key, shown once
rotation.Result.PreviousKeyExpiresAt; // when the key that made this call stops working

The new key has the scopes of the key that made the call. Store it at once and roll it out before PreviousKeyExpiresAt. See Authentication.

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).

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.

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));

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.

Program.cs
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.

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.