ReqKey.docs
SDKs

.NET SDK

The official .NET SDK — C# for ASP.NET Core — for API key validation, credit metering, consumer rate limits, and correlated traffic analytics. Two packages target .NET 8: a framework-neutral ReqKey client, and ReqKey.AspNetCore with DI registration, middleware, Minimal API conventions, and MVC attributes — using normal .NET patterns throughout: HttpClient, async methods, cancellation tokens, and strongly typed options.

Prefer it filled in for you?
The dashboard's Setup Guide generates this exact integration from a few questions — with your real API id, key location, and a copy-ready brief for AI coding agents. Explore every option interactively in the SDK playground.

Install

Both packages target .NET 8 and cover Minimal APIs, MVC, Web API controllers, and middleware-based apps. ReqKey.AspNetCore depends on ReqKey, so installing the ASP.NET integration pulls the core client in with it:

terminal
# The ASP.NET Core integration (pulls in the core ReqKey package)
dotnet add package ReqKey.AspNetCore

# Or just the framework-neutral client — for scripts, workers, and custom stacks
dotnet add package ReqKey
Published to NuGet
Packages are published to NuGet. The request contract and middleware behavior track the ReqKey Python SDK, so switching stacks never means relearning ReqKey. Classic ASP.NET (System.Web) is intentionally not included.

The SDK reads your project credential from the REQKEY_PROJECT_KEY environment variable (falling back to the legacy REQKEY_ROOT_KEY). Copy the real key from the dashboard, and keep it out of source control:

.env / shell
export REQKEY_PROJECT_KEY="reqkey_your_project_key"

Quickstart

Register the SDK once with builder.Services.AddReqKey(...), then add app.UseReqKey() after routing. Every routed endpoint it covers is guarded automatically — your handlers stay untouched, and the application never calls /key/validate or /ingest directly. The same options can come from an IConfiguration section instead of the lambda.

using ReqKey.AspNetCore;

var builder = WebApplication.CreateBuilder(args);

builder.Services.AddReqKey(options =>
{
    options.ProjectKey = builder.Configuration["REQKEY_PROJECT_KEY"];
    options.ApiId = "api_payments";
    options.KeyName = "X-Startup-Key";
    options.ExcludePaths = ["/health"];
});

var app = builder.Build();
app.UseRouting();
app.UseReqKey();

app.MapPost("/payments", (HttpContext context) => Results.Ok(new
{
    creditsRemaining = context.GetReqKeyDecision()?.CreditsRemaining,
}))
    .RequireReqKey(credits: 5, apiId: "api_images", resource: "/v1/images");

app.MapGet("/health", () => Results.Ok(new { ok = true })).SkipReqKey();

app.Run();

Minimal API endpoints opt in or out with .RequireReqKey(...) and .SkipReqKey(); MVC and Web API controllers use the equivalent [ReqKey(...)] and [SkipReqKey] attributes. These are endpoint metadata consumed by the shared middleware — they never duplicate validation or analytics logic. For scripts and workers, use the direct ReqKeyClient.

Reading the decision

After successful validation, the full VerificationResult — the consumer, credits remaining, and a traceable request id — is available from the HttpContext:

Any endpoint
HttpContext.GetReqKeyDecision() returns the VerificationResult?
Request id
HttpContext.GetReqKeyRequestId() for log correlation
Fail-open
HttpContext.GetReqKeyError() exposes the service error that was bypassed

The decision reads naturally in authorization code:

  • decision.Allowed — true when the key is valid; decision.Reason categorizes a denial.
  • decision.CreditsRemaining and decision.CreditsLimit — the consumer's balance (long?).
  • decision.RequestId — a traceable id for support and log correlation.

Successful responses also carry X-ReqKey-Request-ID, X-ReqKey-Credits-Limit, X-ReqKey-Credits-Remaining, and X-ReqKey-Validation-Time-Ms headers (plus Retry-After on rate-limited denials) — add any your browser JavaScript reads to the CORS WithExposedHeaders(...) configuration.

Modes

One property decides what the middleware does per request:

mode
options.Mode = ReqKeyMode.Both;       // validate keys AND record analytics (default)
options.Mode = ReqKeyMode.Validate;   // key checks and credit deduction only — nothing recorded
options.Mode = ReqKeyMode.Ingest;     // analytics only — no key checks at all

In ReqKeyMode.Both, denied requests — missing keys, exhausted credits, forbidden access, rate-limit hits — are recorded too, so abuse is visible in your dashboards. Set options.IngestDeniedRequests = false to record successful requests only. In ingest-only mode, another component's validation can be correlated via options.RequestIdResolver. Ingestion runs inside the request lifecycle — the SDK never spawns a background task that can vanish on shutdown.

Where the key comes from

Match however your consumers already send credentials:

key extraction
// Custom header (recommended)
options.KeyLocation = ApiKeyLocation.Header;
options.KeyName = "X-Startup-Key";

// Authorization: Bearer <key>
options.KeyLocation = ApiKeyLocation.Header;
options.KeyName = "Authorization";
options.KeyScheme = ApiKeyScheme.Bearer;

// ?api_key=<key> — easy to test, but URLs end up in logs
options.KeyLocation = ApiKeyLocation.Query;
options.KeyName = "api_key";

// Cookie — sent automatically by browsers
options.KeyLocation = ApiKeyLocation.Cookie;
options.KeyName = "startup_key";

// Or take full control with an async resolver
options.ConsumerKeyResolver = (context, _) =>
    ValueTask.FromResult<string?>(context.Request.Headers["X-Custom-Key"]);
Headers over query parameters
Query and cookie keys must use the raw scheme. Headers are safer — URLs are retained in browser history and access logs. When the key comes from the query string, it is removed from both the captured QueryParameters and the recorded path, while still sent in the dedicated apiKey identity field. Do not read the request body to find a key — it can interfere with model binding.

Paths & credit costs

Excluded traffic is never validated, charged, or recorded — for anyone. Credit costs can be the static options.Credits or a per-request options.CreditCostResolver, and Minimal API / MVC endpoints can override cost, API id, and analytics resource per endpoint:

routing & pricing
// Exact paths or trailing-* prefixes: never validated, charged, or recorded
options.ExcludePaths = ["/health", "/docs/*"];

// Or decide per request with a predicate
options.ShouldProtect = (context, _) =>
    ValueTask.FromResult(context.Request.Path.StartsWithSegments("/api"));

// Charge different endpoints differently
options.CreditCostResolver = (context, _) =>
    ValueTask.FromResult(context.Request.Method == HttpMethods.Post ? 5 : 1);

// Minimal APIs and MVC actions override cost per endpoint
app.MapPost("/images", HandleImage).RequireReqKey(credits: 5, apiId: "api_images");
// [ReqKey(Credits = 2, Resource = "/v1/orders")] on a controller action

The SDK deliberately never auto-retries /key/validate, because validation deducts credits — a safe retry would require server-side idempotency. Enable it only when your deployment has that guarantee via options.Retry.RetryValidationRequests.

Analytics capture

By default each event records the method, normalized endpoint, status, latency, and User-Agent — correlated to the validated consumer. Everything else is opt-in with a plain bool:

capture
builder.Services.AddReqKey(options =>
{
    options.ApiId = "api_payments";
    // All off by default — turn on what your dashboards need
    options.CaptureQueryParameters = true;
    options.CaptureRequestHeaders = true;
    options.CaptureResponseHeaders = true;
    options.CaptureResponseBody = true; // textual, first 1,000 chars
    options.CaptureClientIp = true;

    // User-Agent is captured by default — opt out explicitly
    options.CaptureUserAgent = false;

    // Strip additional headers beyond the built-in redaction list
    options.ExcludedHeaders = ["X-Internal-Trace"];
});
Redaction is always on
Authorization, Cookie, Set-Cookie, proxy authorization, common API-key headers, and your configured key header are always stripped from captured header dictionaries — even with header capture on. Response-body capture streams a prefix only, never buffers the full response, decodes textual content only, and caps the value at 1,000 characters. Automatic request-body capture is intentionally omitted.

Failure behavior

Validation fails closed by default: a timeout, transport failure, invalid project credential, or unexpected ReqKey response returns 503 without running your endpoint. Prefer availability? Set options.FailureMode = ReqKeyFailureMode.Open — service failures let requests through, while explicitly invalid, exhausted, forbidden, and rate-limited keys are still always denied. Customize denials by stable code, or forward failures to alerting:

failure
// Fail closed (default) blocks with a 503; fail open lets requests through
options.FailureMode = ReqKeyFailureMode.Open;

// Rewrite customer-facing messages by stable error code
options.ErrorMessages[ReqKeyErrorCodes.InsufficientCredits] =
    "Your account has no credits remaining.";

// Forward service failures to alerting — no credentials or request content
options.OnError = async (errorEvent, cancellationToken) =>
{
    await alerts.SendAsync(new
    {
        errorEvent.Operation,
        errorEvent.Message,
        errorEvent.Method,
        errorEvent.Path,
    }, cancellationToken);
};

For complete response control, set options.OnDenied — the middleware sets the status code and passes a ReqKeyDenial for the callback to write. On the direct client, decisions are normalized (402/403/429 are data, not exceptions) and real failures surface as typed exceptions deriving from ReqKeyException:

Errors.cs
using ReqKey;

try
{
    // Access denial is data, not an exception — inspect the decision.
    var decision = await client.ValidateAsync("consumer_key_...", apiId: "api_payments");
    Console.WriteLine(decision.Allowed ? $"remaining: {decision.CreditsRemaining}" : $"denied: {decision.Reason}");
}
catch (ReqKeyTimeoutException) { /* an attempt exceeded Timeout */ }
catch (ReqKeyAuthenticationException) { /* ReqKey rejected the project credential */ }
catch (ReqKeyException ex) { /* transport, API, or configuration failure */ }

Configuration reference

Every option below is a property on ReqKeyAspNetCoreOptions, set in the AddReqKey(options => ...) lambda or bound from an appsettings.json section (delegates excepted). The two opt-out captures, CaptureUserAgent and IngestDeniedRequests, default on.

ReqKeyAspNetCoreOptions
ProjectKeystringrequired

Your project credential, sent to ReqKey as the Bearer token. Read it from the REQKEY_PROJECT_KEY environment variable — never hard-code it.

ApiIdstringrequired

The ReqKey API this application serves — validation and analytics are scoped to it.

ModeReqKeyMode

What the middleware does per request: guard keys and record analytics (the default), key checks only, or analytics only.

Enabledbool

One switch to bypass ReqKey entirely — handy as a local-development toggle that leaves your pipeline untouched.

RootKeystring

Backward-compatible alias for ProjectKey. Never set both.

KeyLocationApiKeyLocation

Where the key travels. Headers are recommended — query strings end up in logs and browser history.

KeyNamestring

The customer-facing header, query parameter, or cookie name. Make it yours: X-Startup-Key.

KeySchemeApiKeyScheme

For headers: the raw key value, or a standard Authorization: Bearer token.

ConsumerKeyResolverresolver

Full control — an async delegate that extracts the key from anywhere in the request.

Creditsint

The static cost per successful request. Defaults to 1; use CreditCostResolver to price endpoints individually.

CreditCostResolverresolver

Charge heavy endpoints more than light ones — a per-request delegate returning the cost.

ExcludePathsICollection<string>

Paths that stay public — never validated, charged, or recorded. Exact matches or trailing-* prefixes.

ShouldProtectresolver

A per-request predicate for when patterns are not enough — protect exactly the routes you choose.

SkipMethodsICollection<string>

HTTP methods that bypass ReqKey. Defaults to ["OPTIONS"] so CORS preflights stay free.

CaptureQueryParametersbool

Record redacted query values with each analytics event. Off by default.

CaptureRequestHeadersbool

Record request headers — auth headers, cookies, and consumer keys are always stripped first.

CaptureResponseHeadersbool

Record response headers, with the same always-on redaction.

CaptureResponseBodybool

Record textual response bodies, capped at the first 1,000 characters.

CaptureClientIpbool

Record the caller’s IP address as resolved by the framework. Off by default.

CaptureUserAgentbool

Record the User-Agent header. On by default — set false to opt out.

ExcludedHeadersICollection<string>

Strip additional headers from captured analytics, beyond the built-in redaction list.

ConsumerNameResolverresolver

Label events with a human-readable customer name from a header or another trusted source.

ClientIpResolverresolver

Behind a trusted proxy? Tell the SDK exactly which header holds the real client IP.

ResourceResolverresolver

Normalize route names before they are recorded — collapse ids so endpoints group cleanly.

RequestIdResolverresolver

In ingest-only mode: correlate events to a validation performed by another middleware.

IngestDeniedRequestsbool

Record denied traffic too — invalid keys, exhausted credits, rate-limit hits — so abuse is visible. On by default.

FailureModeReqKeyFailureMode

Fail closed (block with a 503, the default) or fail open (let requests through unvalidated). Invalid and exhausted keys are always denied either way.

OnErrorcallback

Called on ReqKey service failures with a provider-neutral event — wire it to Slack, Discord, or PagerDuty. No request secrets included.

ErrorMessagesIDictionary<string, string>

Rewrite the customer-facing denial messages in your own voice, per error type.

TimeoutTimeSpan

How long each ReqKey HTTP attempt may take before the failure mode kicks in. Defaults to 2 seconds.

Client settings

The project credential, base address, timeout, and retry policy live on the same options object (they are inherited from ReqKeyClientOptions). ReqKeyClientOptions.FromEnvironment() reads REQKEY_PROJECT_KEY for the direct client. Each HTTP attempt has a two-second Timeout by default; automatic retries are off until Retry.MaxRetries is greater than zero, and validation is never retried unless you opt in. Never configure both a project and a root key.