ReqKey.docs
SDKs

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.

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

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
}
Quarkus & Micronaut
Neither has a dedicated module in this first release — their JAX-RS or Servlet support drives the existing adapters without a second copy of the ReqKey lifecycle. The request contract and middleware behavior track the ReqKey Python SDK, so switching stacks never means relearning ReqKey.

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:

.env / shell
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:

Spring Boot · Servlet
(VerificationResult) request.getAttribute(ReqKeyAttributes.DECISION)
JAX-RS
requestContext.getProperty(ReqKeyAttributes.DECISION) on the injected ContainerRequestContext
Direct client
the VerificationResult 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() and decision.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(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 all

In 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:

key extraction
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"));
Headers over query parameters
Query parameters are available for compatibility, but headers are safer — URLs are retained in browser history and access logs. A credential read from the query string is always removed from captured query data and the analytics path, while still sent in the dedicated 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:

routing & 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:

capture
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();
Redaction is always on
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:

failure
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:

Errors.java
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:

timeouts & retries
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.

ReqKeyMiddlewareConfig.builder(apiId)…
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. Passed to ReqKeyMiddlewareConfig.builder.

modeMode

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

enabledboolean

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

rootKeyString

Backward-compatible alias for projectKey. Never set both.

keyLocationKeyLocation

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.

keySchemeKeyScheme

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

apiKeyExtractorresolver

Full control — a function that extracts the key from anywhere in the request.

creditsint | resolver

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

protectWhenresolver

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

captureQueryParametersboolean

Record sanitized query values and the path query string with each analytics event. Off by default.

captureRequestHeadersboolean

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

captureResponseHeadersboolean

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

captureResponseBodyboolean

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

captureClientIpboolean

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

captureUserAgentboolean

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

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.

endpointResolverresolver

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.

ingestDeniedRequestsboolean

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

failureModeFailureMode

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.

errorMessagesMap<String, String>

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

timeoutDuration

How long each ReqKey operation may take before the failure mode kicks in. Defaults to 2 seconds. Set on ReqKeyConfig.

loggingEnabledboolean

SDK diagnostic logging. On by default — set false to silence the SDK’s own logs.

logSuccessfulValidationsboolean

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