Security
This page covers KEPCA's security features for protecting your integration: rate limiting, API key management, token verification, and webhook security.
Rate limiting
All API endpoints are rate-limited to prevent abuse. Limits are applied per IP address and per API key.
Default limits
| Endpoint | Free plan | Pro plan | Enterprise |
|---|---|---|---|
POST /v1/{sk}/init | 100/min | 1,000/min | 10,000/min |
POST /v1/{sk}/challenge | 60/min | 600/min | 6,000/min |
POST /v1/{sk}/solve | 60/min | 600/min | 6,000/min |
POST /v1/siteverify | 200/min | 2,000/min | 20,000/min |
| Admin API | 30/min | 60/min | 120/min |
Rate limit headers
Every response includes standard rate limit headers:
X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 997
X-RateLimit-Reset: 1712700060
Retry-After: 12| Header | Description |
|---|---|
X-RateLimit-Limit | Maximum requests allowed in the window |
X-RateLimit-Remaining | Requests remaining in the current window |
X-RateLimit-Reset | Unix timestamp when the window resets |
Retry-After | Seconds until the client can retry (on 429 only) |
When the limit is exceeded, the API returns 429 Too Many Requests.
Custom limits
Enterprise plans can configure custom rate limits per site via the dashboard or API:
curl -X PATCH https://api.kepca.com/v1/sites/mpt_site_abc123 \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"rate_limits": {
"init": 5000,
"challenge": 3000,
"solve": 3000,
"siteverify": 10000
}
}'API key rotation
API keys should be rotated regularly. KEPCA supports zero-downtime rotation by allowing two active keys simultaneously during a transition period.
Rotation workflow
- Generate a new key -- the old key remains active.
curl -X POST https://api.kepca.com/v1/keys/rotate \
-H "Authorization: Bearer <jwt>" \
-H "Content-Type: application/json" \
-d '{
"site_key": "mpt_site_abc123",
"key_type": "secret"
}'Response:
{
"new_key": "mpt_secret_new_xyz789",
"old_key_expires_at": "2026-04-16T12:00:00.000Z",
"grace_period_days": 7
}Update your backend -- deploy the new secret key to your servers.
Old key expires -- after the grace period (default 7 days), the old key is deactivated automatically.
Listing active keys
curl https://api.kepca.com/v1/keys?site_key=mpt_site_abc123 \
-H "Authorization: Bearer <jwt>"{
"keys": [
{
"key_id": "key_001",
"prefix": "mpt_secret_new_***",
"status": "active",
"created_at": "2026-04-09T12:00:00.000Z"
},
{
"key_id": "key_000",
"prefix": "mpt_secret_old_***",
"status": "rotating",
"expires_at": "2026-04-16T12:00:00.000Z"
}
]
}Revoking a key immediately
curl -X DELETE https://api.kepca.com/v1/keys/key_000 \
-H "Authorization: Bearer <jwt>"HMAC token verification
KEPCA tokens are HMAC-signed (HS256) and self-contained. You can verify them in two ways.
Online verification (recommended)
Send the token to the KEPCA API:
curl -X POST https://api.kepca.com/v1/siteverify \
-H "Content-Type: application/json" \
-d '{
"secret": "mpt_secret_xyz789",
"token": "TOKEN_FROM_WIDGET",
"ip": "203.0.113.42"
}'This verifies the signature, checks expiry, validates IP, prevents replay, and logs analytics.
Offline verification
For latency-sensitive or air-gapped deployments, verify the token locally using your secret key:
import { createHmac } from 'node:crypto';
function verifyToken(token, secretKey) {
const [headerB64, payloadB64, signatureB64] = token.split('.');
// Recompute the signature
const data = `${headerB64}.${payloadB64}`;
const expected = createHmac('sha256', secretKey)
.update(data)
.digest('base64url');
if (expected !== signatureB64) {
return { success: false, error: 'invalid_signature' };
}
// Decode and validate payload
const payload = JSON.parse(
Buffer.from(payloadB64, 'base64url').toString()
);
const now = Math.floor(Date.now() / 1000);
if (payload.exp < now) {
return { success: false, error: 'token_expired' };
}
return {
success: true,
score: payload.rs,
challenge: payload.ch,
timestamp: new Date(payload.ts * 1000).toISOString(),
};
}import hmac, hashlib, base64, json, time
def verify_token(token: str, secret_key: str) -> dict:
header_b64, payload_b64, signature_b64 = token.split(".")
# Recompute signature
data = f"{header_b64}.{payload_b64}".encode()
expected = base64.urlsafe_b64encode(
hmac.new(secret_key.encode(), data, hashlib.sha256).digest()
).rstrip(b"=").decode()
if expected != signature_b64:
return {"success": False, "error": "invalid_signature"}
payload = json.loads(base64.urlsafe_b64decode(payload_b64 + "=="))
if payload["exp"] < time.time():
return {"success": False, "error": "token_expired"}
return {"success": True, "score": payload["rs"], "challenge": payload["ch"]}WARNING
Offline verification does not check for token replay. If replay prevention is important, use the online /v1/siteverify endpoint or maintain your own JTI cache.
Webhook signature verification
When you configure webhooks (for events like verification failures or rate limit alerts), KEPCA signs each webhook payload with your webhook secret using HMAC-SHA256.
Verifying webhook signatures
Each webhook request includes these headers:
| Header | Description |
|---|---|
X-Kepca-Signature | HMAC-SHA256 hex digest of the request body |
X-Kepca-Timestamp | Unix timestamp when the webhook was sent |
X-Kepca-Webhook-Id | Unique delivery ID for idempotency |
Verification steps
- Read the
X-Kepca-TimestampandX-Kepca-Signatureheaders. - Concatenate the timestamp and the raw request body:
timestamp.body. - Compute HMAC-SHA256 using your webhook secret.
- Compare the result with the signature header (constant-time comparison).
- Reject requests older than 5 minutes to prevent replay attacks.
import { createHmac, timingSafeEqual } from 'node:crypto';
function verifyWebhook(req, webhookSecret) {
const timestamp = req.headers['x-kepca-timestamp'];
const signature = req.headers['x-kepca-signature'];
const body = req.rawBody;
// Check timestamp freshness (5 minute window)
const age = Math.floor(Date.now() / 1000) - parseInt(timestamp);
if (age > 300) {
throw new Error('Webhook timestamp too old');
}
const expected = createHmac('sha256', webhookSecret)
.update(`${timestamp}.${body}`)
.digest('hex');
const valid = timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
if (!valid) {
throw new Error('Invalid webhook signature');
}
return JSON.parse(body);
}Prometheus metrics
KEPCA exposes a Prometheus-compatible metrics endpoint for monitoring.
Endpoint
GET /metricsNo authentication required by default. In production, restrict access via network policy or the METRICS_AUTH_TOKEN environment variable.
Available metrics
| Metric | Type | Description |
|---|---|---|
kepca_requests_total | Counter | Total requests by endpoint and status |
kepca_challenges_issued_total | Counter | Challenges issued by type |
kepca_challenges_solved_total | Counter | Challenges solved by type |
kepca_risk_score_histogram | Histogram | Distribution of risk scores |
kepca_pow_difficulty_histogram | Histogram | Distribution of PoW difficulty levels |
kepca_verification_duration_seconds | Histogram | Token verification latency |
kepca_rate_limit_hits_total | Counter | Rate limit rejections by endpoint |
kepca_active_sessions | Gauge | Currently active challenge sessions |
Grafana dashboard
A pre-built Grafana dashboard JSON is included in the repository at infra/grafana/kepca-dashboard.json. Import it into your Grafana instance for real-time monitoring.
Next steps
- Enterprise -- team management, custom domains, and billing
- Challenge Types -- all six challenge types with configuration
- REST API -- full endpoint reference
