Reference

API key guard

A valid key can still be stolen. Check how it is being used.

Report key use

Add an opaque apiKeyId to usage events. Never send the API key itself. IDs are nonempty strings of at most 128 characters and stored as per-site HMACs. The dashboard shows a hash identifier instead of the original ID. Optional country is a two-letter country code supplied by your backend; Nolgard does not infer it from the IP.

POST /v1/usage
{"secret":"sk_…","events":[{"accountId":"user_42","apiKeyId":"key_7","ip":"198.51.100.8","userAgent":"my-sdk/2","country":"US"}]}

{"accepted":1,"flagged":[],"keys":[]}

Findings appear as {"apiKeyId":"key_7","reasons":["key_network_spread"],"action":"rotate"} in keys. Existing account findings remain in flagged. Detection uses server receipt time rather than the optional usage timestamp.

Gate before inference

const result = await nolgard.keyCheck({
  apiKeyId: authenticatedKey.id,
  ip: trustedClientIp,
  userAgent: request.headers.get('user-agent') ?? undefined,
  country: trustedCountry
});
if (result.decision !== 'allow') {
  return new Response('Key requires review', { status: 403 });
}
return runInference();

POST /v1/key-check accepts {secret, apiKeyId, ip?, userAgent?, country?} and returns {decision: 'allow'|'review'|'block', reasons: string[]}. It counts this use. Do not report the same request again through usage unless you intend to count it twice. Keep your site secret on the server; fail closed on transport errors.

Pass apiKeyId with a spend reserve call too. A blocked key returns {"allowed":false,"reason":"key_blocked"}. Commit and release still settle existing reservations.

Detectors and defaults

ReasonDefault trigger
key_network_spreadMore than 10 distinct IP prefixes in an hour
key_country_spreadAt least 3 supplied countries in an hour
key_concurrencyMore than 5 prefixes in 60 seconds
key_tool_shiftKey age at least 7 days and 200 baseline requests before the last hour; an unseen family exceeds 50% of last-hour requests
key_new_network_burstKey age at least 7 days; more than 80% of last-hour requests use prefixes first seen in that hour
key_canaryAny use of a registered decoy

Country spread and concurrency alone mean review. Network spread, tool shift, new-network burst and canary use mean block with action rotate. Findings persist until cleared or aged out. New reasons emit a signed key.anomaly webhook with {apiKeyId,reasons,action}.

Register a canary

In the site's API keys panel, register the ID of a decoy key and a label such as “Public repo”. Plant that decoy in a repository, documentation example or client bundle where no legitimate caller should use it. Route attempted use through key-check before rejecting it. Do not plant a real production key. Only its ID hash and your label are stored.

Use Clear to acknowledge a finding. A registered canary stays blocked even after Clear; remove the canary registration to stop blocking it. Rotate or revoke a compromised key in your own key service. Nolgard cannot revoke credentials for you.

GET /api/sites/:siteKey/keyguard
POST /api/sites/:siteKey/keyguard/canaries
{"apiKeyId":"decoy_repo","label":"Public repo"}
DELETE /api/sites/:siteKey/keyguard/canaries/:keyHash
POST /api/sites/:siteKey/keyguard/:keyHash/clear
PUT /api/sites/:siteKey/keyguard/thresholds
{"networkSpread":10,"countrySpread":3,"concurrency":5,"baselineDays":7,"baselineRequests":200,"toolShiftShare":0.5,"newNetworkShare":0.8}

Portal endpoints require the site owner's session and same-origin requests. Threshold saves replace overrides; omitted fields use defaults. Counts and days must be positive integers, at most 100,000; shares must be greater than zero and at most one.

Errors and test keys

Invalid input: 400. Invalid secret: 401. Demo writes: 403. Key-check shares usage's 120-call-per-minute site limit; excess calls return 429. All four test secrets are stateless: IDs starting canary_ block with key_canary, flag_ review with test, other IDs allow. Test spend reserves with canary_ IDs return key_blocked.

Limits

These defaults are logical starting points rather than measured or tuned thresholds. Shared NAT and corporate egress can hide many callers behind one prefix. CI fleets and mobile carriers can legitimately change networks and countries. Missing IP, user-agent or country fields skip the corresponding evidence. A reverse proxy that hides every caller behind one stable network can evade spread checks. SDK-family recognition is heuristic. An hour is a rolling window; concurrency counts distinct prefixes rather than open sockets. Per-request history is pruned after 30 days. IDs are hashed, but labels and webhook payloads are owner-visible: keep secrets out of both.