Java SDK
The official Java SDK for API key validation, credit metering, consumer rate limits, and correlated traffic analytics. One framework-neutral core — the reusable JDK HTTP client and shared middleware engine — plus thin adapters for Spring Boot, Jakarta Servlet, and JAX-RS, with typed enums, typed exceptions, and a fluent builder throughout. Java 17 or later is required.
Install
Every module is published to Maven Central under the com.reqkey group. Add the one artifact for your framework — each pulls the framework-neutral core (com.reqkey:reqkey) transitively, and no framework API leaks into that core:
// build.gradle.kts — add the one module for your framework
repositories { mavenCentral() }
dependencies {
implementation("com.reqkey:reqkey-spring-boot-starter:1.0.0") // Spring Boot 3.5
// implementation("com.reqkey:reqkey-servlet:1.0.0") // Jakarta Servlet 6
// implementation("com.reqkey:reqkey-jaxrs:1.0.0") // Jakarta REST 3.1
// implementation("com.reqkey:reqkey:1.0.0") // core client, no framework
}The core reads your project credential from the REQKEY_PROJECT_KEY environment variable (falling back to the legacy REQKEY_ROOT_KEY) via ReqKeyConfig.fromEnvironment(). Spring Boot reads the same value from reqkey.project-key. Keep it out of source control:
export REQKEY_PROJECT_KEY="reqkey_your_project_key"Quickstart
Every adapter shares the same ReqKeyMiddlewareConfig, wired to a ReqKeyConfig that carries the project credential. Spring Boot builds both from application.yml; Servlet and JAX-RS use the fluent builder directly. Every route the adapter covers is guarded automatically — your handlers stay untouched, and the application never calls /key/validate or /ingest itself.
# src/main/resources/application.yml
reqkey:
project-key: ${REQKEY_PROJECT_KEY}
api-id: api_payments
key-name: X-Startup-Key
exclude-paths:
- /health
// ── PaymentsController.java ───────────────────────────────
// The starter registers one filter; the decision is a request attribute.
@PostMapping("/payments")
Map<String, Object> create(HttpServletRequest request) {
VerificationResult decision = (VerificationResult)
request.getAttribute(ReqKeyAttributes.DECISION);
return Map.of("creditsRemaining", decision.getCreditsRemaining());
}Spring Boot configures capture, failure, key-location, and retry options declaratively; for dynamic credit costs or resolver lambdas, expose a ReqKeyMiddlewareConfig bean and the starter backs off from its own. For scripts, workers, and custom servers, the thread-safe HttpReqKeyClient validates and ingests without any middleware.
Reading the decision
After successful validation, the full VerificationResult — the consumer, credits remaining, and a traceable request id — is exposed where each framework keeps request-scoped values, under the portable ReqKeyAttributes names:
(VerificationResult) request.getAttribute(ReqKeyAttributes.DECISION)requestContext.getProperty(ReqKeyAttributes.DECISION) on the injected ContainerRequestContextVerificationResult returned by reqKey.verify(...)The decision reads naturally in authorization code:
decision.isAllowed()— true when the key is valid;decision.getReason()categorizes a denial (VALID,INVALID_KEY,INSUFFICIENT_CREDITS,FORBIDDEN,RATE_LIMITED,DENIED).decision.getCreditsRemaining()anddecision.getCreditsLimit()— the consumer's balance.decision.getRequestId()— a traceable id for support and log correlation.
Successful framework responses also carry X-ReqKey-Request-ID, X-ReqKey-Credits-Limit, X-ReqKey-Credits-Remaining, and X-ReqKey-Validation-Time-Ms headers; rate-limited responses preserve Retry-After.
Modes
One builder method decides what the middleware does per request:
.mode(Mode.BOTH) // validate keys AND record analytics (default)
.mode(Mode.VALIDATE) // key checks and credit deduction only — nothing recorded
.mode(Mode.INGEST) // analytics only — no key checks at allIn Mode.BOTH, denied requests — missing keys, exhausted credits, forbidden access, rate-limit hits — are recorded too, so abuse is visible in your dashboards. Set .ingestDeniedRequests(false) to record successful requests only. In ingest-only mode, another component's validation can be correlated via a .requestIdResolver(...). Ingestion runs inside the request lifecycle — a failed analytics send is logged, never surfaced to your consumer.
Where the key comes from
Match however your consumers already send credentials:
import com.reqkey.middleware.KeyLocation;
import com.reqkey.middleware.KeyScheme;
// Custom header (recommended)
ReqKeyMiddlewareConfig.builder("api_payments")
.keyLocation(KeyLocation.HEADER)
.keyName("X-Startup-Key");
// Authorization: Bearer <key>
ReqKeyMiddlewareConfig.builder("api_payments")
.keyName("Authorization")
.keyScheme(KeyScheme.BEARER);
// ?api_key=<key> — easy to test, but URLs end up in logs
ReqKeyMiddlewareConfig.builder("api_payments")
.keyLocation(KeyLocation.QUERY)
.keyName("api_key");
// Or take full control with a custom extractor
ReqKeyMiddlewareConfig.builder("api_payments")
.apiKeyExtractor(request -> request.firstHeader("X-Custom-Key"));apiKey identity field. Query and cookie credentials must use KeyScheme.RAW.Paths & credit costs
Excluded traffic is never validated, charged, or recorded — for anyone. The credits builder method is overloaded: a non-negative int, or a CreditResolver for per-request pricing:
ReqKeyMiddlewareConfig.builder("api_images")
// Exact paths or trailing-* prefixes: never validated, charged, or recorded
.excludePath("/health")
.excludePath("/docs/*")
// Or decide per request with a predicate
.protectWhen(request -> request.getPath().startsWith("/api/"))
// Charge endpoints differently — a CreditResolver instead of an int
.credits(request -> request.getMethod().equals("POST") ? 5 : 1)
.build();Every resolver receives a framework-neutral RequestMetadata — method, path, headers, query parameters, and cookies, with helpers like firstHeader(...) and userAgent(). The SDK deliberately never auto-retries /key/validate, because validation deducts credits — a safe retry would require server-side idempotency.
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 boolean:
ReqKeyMiddlewareConfig.builder("api_payments")
// All off by default — turn on what your dashboards need
.captureQueryParameters(true)
.captureRequestHeaders(true)
.captureResponseHeaders(true)
.captureResponseBody(true) // textual, first 1,000 chars, where supported
.captureClientIp(true)
// User-Agent is captured by default — opt out explicitly
.captureUserAgent(false)
// Strip additional headers beyond the built-in redaction list
.excludedHeader("X-Internal-Trace")
.build();Authorization, Cookie, Set-Cookie, Proxy-Authorization, X-API-Key, and your configured key name are always stripped before anything leaves your server — even with header capture on. Response bodies are captured only for textual content and truncated to 1,000 characters; compressed bodies are never captured, and on portable JAX-RS body capture applies when the response entity is already a String.Failure behavior
Validation fails closed by default: if ReqKey times out or is unreachable, the adapter returns 503 without running your handler. Prefer availability? Set .failureMode(FailureMode.OPEN) — transport, timeout, and service failures let requests through, while explicitly invalid, exhausted, forbidden, and rate-limited keys are still always denied. Either way, an onError handler tells you it happened:
import com.reqkey.middleware.FailureMode;
ReqKeyMiddlewareConfig.builder("api_payments")
.failureMode(FailureMode.CLOSED) // or FailureMode.OPEN: let requests through
.onError(event ->
// Provider-neutral: post it to Slack, Discord, PagerDuty, ...
log.warn("ReqKey {} failed on {} {}: {}",
event.operation(), event.method(), event.path(), event.message()))
.errorMessage("insufficient_credits", "Out of credits — top up in your dashboard.")
.build();The MiddlewareErrorEvent intentionally excludes credentials, consumer keys, query values, headers, and bodies; exceptions thrown inside the handler are isolated and never change the HTTP response. On the direct client, access decisions are normalized (denials are data, not errors) and real failures surface as typed unchecked exceptions under com.reqkey.exception — ReqKeyConfigurationException, ReqKeyTransportException, ReqKeyTimeoutException, ReqKeyApiException, and ReqKeyAuthenticationException:
import com.reqkey.exception.ReqKeyApiException;
import com.reqkey.exception.ReqKeyTimeoutException;
import com.reqkey.exception.ReqKeyTransportException;
try {
VerificationResult decision = reqKey.verify(
ValidationRequest.builder(key).apiId("api_payments").build());
// Access denial is data, not an exception — inspect the decision.
if (!decision.isAllowed()) {
// decision.getReason(), getCreditsRemaining(), getRequestId() are available.
}
} catch (ReqKeyTimeoutException e) {
// Validation deducts credits, so it is never retried by default.
} catch (ReqKeyTransportException | ReqKeyApiException e) {
log.warn("ReqKey unavailable: {}", e.getMessage());
}HTTP timeouts and bounded, exponential-backoff retries live on ReqKeyConfig via a RetryPolicy. The default is a single attempt; ingestion may retry safely, while validation retries stay off unless you can guarantee server-side idempotency:
RetryPolicy retry = RetryPolicy.builder()
.maxAttempts(3)
.retryIngestion(true) // safe — ingestion is idempotent
.retryValidation(false) // off by default: a replay could double-charge credits
.build();
ReqKeyConfig config = ReqKeyConfig.builder(projectKey)
.timeout(Duration.ofSeconds(2))
.connectTimeout(Duration.ofSeconds(2))
.retryPolicy(retry)
.build();Configuration reference
Every option below is a method on the ReqKeyMiddlewareConfig builder. Spring Boot exposes the same options as kebab-case keys under reqkey in application.yml (resolver lambdas excepted). Booleans are plain — 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. Passed to ReqKeyMiddlewareConfig.builder.
modeModeWhat the middleware does per request: guard keys and record analytics (the default), key checks only, or analytics only.
enabledbooleanOne switch to bypass ReqKey entirely — handy as a local-development toggle that leaves your filter chain untouched.
rootKeyStringBackward-compatible alias for projectKey. Never set both.
keyLocationKeyLocationWhere 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.
keySchemeKeySchemeFor headers: the raw key value, or a standard Authorization: Bearer token.
apiKeyExtractorresolverFull control — a function that extracts the key from anywhere in the request.
creditsint | resolverThe cost per successful request. Pass a CreditResolver instead of a number to charge heavy endpoints more than light ones.
excludePathsList<String>Paths that stay public — never validated, charged, or recorded. Exact matches or trailing-* prefixes.
protectWhenresolverA per-request predicate for when patterns are not enough — protect exactly the routes you choose.
skipMethodsCollection<String>HTTP methods that bypass ReqKey. Defaults to ["OPTIONS"] so CORS preflights stay free.
captureQueryParametersbooleanRecord sanitized query values and the path query string with each analytics event. Off by default.
captureRequestHeadersbooleanRecord request headers — auth headers, cookies, and consumer keys are always stripped first.
captureResponseHeadersbooleanRecord response headers, with the same always-on redaction.
captureResponseBodybooleanRecord textual response bodies, capped at the first 1,000 characters where supported.
captureClientIpbooleanRecord the caller’s IP address as resolved by the framework. Off by default.
captureUserAgentbooleanRecord the User-Agent header. On by default — set false to opt out.
excludedHeadersCollection<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.
endpointResolverresolverNormalize 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.
ingestDeniedRequestsbooleanRecord denied traffic too — invalid keys, exhausted credits, rate-limit hits — so abuse is visible. On by default.
failureModeFailureModeFail 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.
errorMessagesMap<String, String>Rewrite the customer-facing denial messages in your own voice, per error type.
timeoutDurationHow long each ReqKey operation may take before the failure mode kicks in. Defaults to 2 seconds. Set on ReqKeyConfig.
loggingEnabledbooleanSDK diagnostic logging. On by default — set false to silence the SDK’s own logs.
logSuccessfulValidationsbooleanEmit a debug-level log line for each allowed decision. Off by default.
Client settings
The project credential and timeouts live on ReqKeyConfig, not the middleware config. ReqKeyConfig.fromEnvironment() reads REQKEY_PROJECT_KEY (falling back to REQKEY_ROOT_KEY); or build one with ReqKeyConfig.builder(projectKey) and set baseUrl, timeout (default 2 seconds), connectTimeout, and a retryPolicy. Spring Boot maps these to reqkey.timeout, reqkey.connect-timeout, and reqkey.retry.*. Never set both a project and a root key.