.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.
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:
# 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 ReqKeyThe 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:
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:
HttpContext.GetReqKeyDecision() returns the VerificationResult?HttpContext.GetReqKeyRequestId() for log correlationHttpContext.GetReqKeyError() exposes the service error that was bypassedThe decision reads naturally in authorization code:
decision.Allowed— true when the key is valid;decision.Reasoncategorizes a denial.decision.CreditsRemaininganddecision.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:
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 allIn 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:
// 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"]);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:
// 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 actionThe 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:
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"];
});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:
// 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:
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.
ProjectKeystringrequiredYour project credential, sent to ReqKey as the Bearer token. Read it from the REQKEY_PROJECT_KEY environment variable — never hard-code it.
ApiIdstringrequiredThe ReqKey API this application serves — validation and analytics are scoped to it.
ModeReqKeyModeWhat the middleware does per request: guard keys and record analytics (the default), key checks only, or analytics only.
EnabledboolOne switch to bypass ReqKey entirely — handy as a local-development toggle that leaves your pipeline untouched.
RootKeystringBackward-compatible alias for ProjectKey. Never set both.
KeyLocationApiKeyLocationWhere the key travels. Headers are recommended — query strings end up in logs and browser history.
KeyNamestringThe customer-facing header, query parameter, or cookie name. Make it yours: X-Startup-Key.
KeySchemeApiKeySchemeFor headers: the raw key value, or a standard Authorization: Bearer token.
ConsumerKeyResolverresolverFull control — an async delegate that extracts the key from anywhere in the request.
CreditsintThe static cost per successful request. Defaults to 1; use CreditCostResolver to price endpoints individually.
CreditCostResolverresolverCharge 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.
ShouldProtectresolverA 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.
CaptureQueryParametersboolRecord redacted query values with each analytics event. Off by default.
CaptureRequestHeadersboolRecord request headers — auth headers, cookies, and consumer keys are always stripped first.
CaptureResponseHeadersboolRecord response headers, with the same always-on redaction.
CaptureResponseBodyboolRecord textual response bodies, capped at the first 1,000 characters.
CaptureClientIpboolRecord the caller’s IP address as resolved by the framework. Off by default.
CaptureUserAgentboolRecord 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.
ConsumerNameResolverresolverLabel events with a human-readable customer name from a header or another trusted source.
ClientIpResolverresolverBehind a trusted proxy? Tell the SDK exactly which header holds the real client IP.
ResourceResolverresolverNormalize route names before they are recorded — collapse ids so endpoints group cleanly.
RequestIdResolverresolverIn ingest-only mode: correlate events to a validation performed by another middleware.
IngestDeniedRequestsboolRecord denied traffic too — invalid keys, exhausted credits, rate-limit hits — so abuse is visible. On by default.
FailureModeReqKeyFailureModeFail closed (block with a 503, the default) or fail open (let requests through unvalidated). Invalid and exhausted keys are always denied either way.
OnErrorcallbackCalled 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.
TimeoutTimeSpanHow 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.