Go SDK
The official Go SDK for API key validation, credit metering, consumer rate limits, and correlated traffic analytics. One module ships a concurrency-safe client, standard net/http middleware that also fits Chi and Gorilla/Mux, and native adapters for Gin, Echo v4, and Fiber v3 — with typed decisions, typed errors, and idiomatic pointer options throughout.
Install
Go 1.25 or newer is required, because the current framework adapters target Gin 1.12, Echo 4.15, and Fiber 3.4. The core has no framework dependency, so installing the module alone never pulls in a web framework:
# One module: the concurrency-safe client + net/http middleware, no framework dependency
go get github.com/Req-Key/reqkey-go@latest
# Add the web framework alongside it for a native adapter
go get github.com/Req-Key/reqkey-go@latest github.com/gin-gonic/gin
go get github.com/Req-Key/reqkey-go@latest github.com/labstack/echo/v4
go get github.com/Req-Key/reqkey-go@latest github.com/gofiber/fiber/v3
# Chi uses the standard net/http contract — no separate ReqKey package
go get github.com/Req-Key/reqkey-go@latest github.com/go-chi/chi/v5The 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 middleware once, where your router is created. Every route it serves is guarded automatically — your handlers stay untouched, and the application never calls /key/validate or /ingest directly. Leave ProjectKey unset to read it from the environment.
package main
import (
"encoding/json"
"net/http"
"os"
"github.com/Req-Key/reqkey-go"
)
func main() {
mux := http.NewServeMux()
mux.HandleFunc("GET /protected", func(w http.ResponseWriter, r *http.Request) {
decision, _ := reqkey.DecisionFromContext(r.Context())
_ = json.NewEncoder(w).Encode(map[string]any{"ok": true, "request_id": decision.RequestID})
})
protect := reqkey.MustHTTPMiddleware(reqkey.MiddlewareOptions{
ProjectKey: os.Getenv("REQKEY_PROJECT_KEY"),
APIID: "api_payments",
Mode: reqkey.ModeBoth, // validate keys AND record analytics
KeyName: "X-Startup-Key", // where consumers send their key
ExcludePaths: []string{"/health", "/docs/*"},
})
_ = http.ListenAndServe(":8080", protect(mux))
}Chi and Gorilla/Mux use the standard func(http.Handler) http.Handler contract, so the core MustHTTPMiddleware works directly — there is no separate Chi package. Use HTTPMiddleware for the non-panicking constructor that returns an error. For scripts, workers, and unsupported frameworks, use the direct reqkey.Client.
Reading the decision
After successful validation, the full *reqkey.VerificationResult — the consumer, credits remaining, and a traceable request id — is stored where each framework keeps request-scoped values:
reqkey.DecisionFromContext(r.Context()) returns (*VerificationResult, bool)c.Get(reqkey.ContextDecisionKey), then assert .(*reqkey.VerificationResult)c.Get(reqkey.ContextDecisionKey) with the same type assertionc.Locals(reqkey.ContextDecisionKey) with the same type assertionThe 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 (nullable*int64).decision.RequestID— a traceable id for support and log correlation.
When fail-open is active, a service failure is exposed instead via reqkey.ValidationErrorFromContext(ctx) for net/http/Chi, and under reqkey.ContextErrorKey in Gin, Echo, and Fiber.
Modes
One field decides what the middleware does per request:
Mode: reqkey.ModeBoth // validate keys AND record analytics (default)
Mode: reqkey.ModeValidate // key checks and credit deduction only — nothing recorded
Mode: reqkey.ModeIngest // analytics only — no key checks at allIn reqkey.ModeBoth, denied requests — missing keys, exhausted credits, forbidden access, rate-limit hits — are recorded too, so abuse is visible in your dashboards. Set IngestDeniedRequests: reqkey.Ptr(false) to record successful requests only. In ingest-only mode, another component's validation can be correlated via RequestIDResolver.
Where the key comes from
Match however your consumers already send credentials:
// Custom header (recommended)
KeyLocation: reqkey.KeyInHeader, KeyName: "X-Startup-Key", KeyScheme: reqkey.KeyRaw
// Authorization: Bearer <key>
KeyLocation: reqkey.KeyInHeader, KeyName: "Authorization", KeyScheme: reqkey.KeyBearer
// ?api_key=<key> — easy to test, but URLs end up in logs
KeyLocation: reqkey.KeyInQuery, KeyName: "api_key"
// Cookie — sent automatically by browsers
KeyLocation: reqkey.KeyInCookie, KeyName: "startup_key"
// Or take full control with a resolver (request.Raw holds the native request)
GetConsumerKey: func(ctx context.Context, request *reqkey.MiddlewareRequest) (string, error) {
return request.Headers.Get("X-Custom-Key"), nil
},Paths & credit costs
Excluded traffic is never validated, charged, or recorded — for anyone. Credit costs can be the static Credits field or a per-request CreditsResolver:
// Exact paths or trailing-* prefixes: never validated, charged, or recorded
ExcludePaths: []string{"/health", "/openapi.json", "/docs/*", "/cron/*"},
// Or decide per request with a resolver
ShouldProtect: func(ctx context.Context, request *reqkey.MiddlewareRequest) (bool, error) {
return strings.HasPrefix(request.Path, "/api/"), nil
},
// Charge different endpoints differently (non-negative integers)
CreditsResolver: func(ctx context.Context, request *reqkey.MiddlewareRequest) (int, error) {
if request.Method == http.MethodPost {
return 5, nil
}
return 1, nil
},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 path, status, latency, and User-Agent — correlated to the validated consumer. Everything else is opt-in with a plain bool:
reqkey.MiddlewareOptions{
ProjectKey: os.Getenv("REQKEY_PROJECT_KEY"),
APIID: "api_payments",
Mode: reqkey.ModeBoth,
// All off by default — turn on what your dashboards need
CaptureQueryParams: true,
CaptureRequestHeaders: true,
CaptureResponseHeaders: true,
CaptureResponseBody: true, // textual content, first 1,000 chars
CaptureClientIP: true,
// User-Agent is captured by default — opt out with a pointer
CaptureUserAgent: reqkey.Ptr(false),
// Strip additional headers beyond the built-in redaction list
ExcludedHeaders: []string{"X-Internal-Trace"},
}Authorization, Cookie, Proxy-Authorization, Set-Cookie, X-API-Key, and your configured KeyName are always stripped before anything leaves your server — even with header capture on. Response bodies are captured only when textual, capped at 1,000 characters; binary and compressed bodies are omitted.Failure behavior
Validation fails closed by default: if ReqKey times out or is unreachable, the middleware returns 503 without running your handler. Prefer availability? Set FailureMode: reqkey.FailureOpen — transport failures let requests through, while explicitly invalid, exhausted, forbidden, and rate-limited keys are still always denied. Either way, OnError tells you it happened:
reqkey.MiddlewareOptions{
ProjectKey: os.Getenv("REQKEY_PROJECT_KEY"),
APIID: "api_payments",
FailureMode: reqkey.FailureClosed, // or reqkey.FailureOpen: let requests through
OnError: func(ctx context.Context, event reqkey.MiddlewareErrorEvent) {
// Provider-neutral: post it to Slack, Discord, PagerDuty, ...
log.Printf("ReqKey %s failed on %s %s: %v",
event.Operation, event.Method, event.Path, event.Err)
},
}The event intentionally excludes API keys, project credentials, captured headers, and bodies. Panics inside the callback are isolated, so alerting can never replace your application response. On the client, handle errors with the exported sentinels — ErrConfiguration, ErrTransport, ErrTimeout, ErrAPI, and ErrAuthentication — plus the typed *reqkey.APIError:
decision, err := client.Verify(ctx, key, reqkey.VerifyOptions{APIID: "api_payments"})
if errors.Is(err, reqkey.ErrTimeout) {
// Retry or apply a service-specific fallback.
}
var apiErr *reqkey.APIError
if errors.As(err, &apiErr) {
log.Printf("ReqKey returned %d: %s", apiErr.StatusCode, apiErr.Message)
}Configuration reference
Every field below is set on the reqkey.MiddlewareOptions struct passed to any adapter — MustHTTPMiddleware, reqkeygin.MustMiddleware, reqkeyecho.MustMiddleware, or reqkeyfiber.MustMiddleware. Tri-state settings are pointer booleans — use reqkey.Ptr(false).
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.
ModeMiddlewareModeWhat the middleware does per request: guard keys and record analytics (the default), key checks only, or analytics only.
Enabled*boolOne switch to bypass ReqKey entirely — handy as a local-development toggle that leaves your middleware layout 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.
GetConsumerKeyresolverFull control — a function that extracts the key from anywhere in the request.
Credits*intThe static cost per successful request. Defaults to 1; use CreditsResolver to price endpoints individually.
CreditsResolverresolverCharge heavy endpoints more than light ones — a per-request function returning the cost.
ExcludePaths[]stringPaths 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.
SkipMethods[]stringHTTP methods that bypass ReqKey. Defaults to ["OPTIONS"] so CORS preflights stay free.
CaptureQueryParamsboolRecord redacted query values with each analytics event, appended to the recorded path. 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.
CaptureUserAgent*boolRecord the User-Agent header. On by default — set reqkey.Ptr(false) to opt out.
ExcludedHeaders[]stringStrip 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.
PathResolverresolverNormalize 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.
IngestDeniedRequests*boolRecord 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[ErrorCode]stringRewrite the customer-facing denial messages in your own voice, per error type.
Timeouttime.DurationHow long each ReqKey operation may take before the failure mode kicks in. Defaults to 2s.
Direct client
reqkey.NewClient (or NewClientFromEnv) exposes Verify() and Ingest() for full manual control. The ClientAPI interface can be replaced by a test double via the Client option. VerifyOptions.Credits defaults to 1; use reqkey.Ptr(0) for a deliberate zero-credit check.