ReqKey.docs
SDKs

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.

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

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:

terminal
# 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/v5
No central registry
Go modules are published from Git tags — there is no upload step like npm or PyPI. Once a version tag is fetched through the Go proxy, its reference docs appear on pkg.go.dev.

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

net/http · Chi
reqkey.DecisionFromContext(r.Context()) returns (*VerificationResult, bool)
Gin
c.Get(reqkey.ContextDecisionKey), then assert .(*reqkey.VerificationResult)
Echo
c.Get(reqkey.ContextDecisionKey) with the same type assertion
Fiber
c.Locals(reqkey.ContextDecisionKey) with the same type assertion

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 (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
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 all

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

key extraction
// 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
},
Headers over query parameters
Query parameters are available for compatibility, but headers are safer — URLs are retained in browser history and access logs. When query capture is enabled, the configured consumer-key parameter is always redacted from both the captured map and the recorded path.

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:

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

capture
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"},
}
Redaction is always on
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:

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

errors.go
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).

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

ModeMiddlewareMode

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

Enabled*bool

One switch to bypass ReqKey entirely — handy as a local-development toggle that leaves your middleware layout 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.

GetConsumerKeyresolver

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

Credits*int

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

CreditsResolverresolver

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

ExcludePaths[]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.

SkipMethods[]string

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

CaptureQueryParamsbool

Record redacted query values with each analytics event, appended to the recorded path. 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.

CaptureUserAgent*bool

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

ExcludedHeaders[]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.

PathResolverresolver

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.

IngestDeniedRequests*bool

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[ErrorCode]string

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

Timeouttime.Duration

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