Skip to content

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

EndpointFree planPro planEnterprise
POST /v1/{sk}/init100/min1,000/min10,000/min
POST /v1/{sk}/challenge60/min600/min6,000/min
POST /v1/{sk}/solve60/min600/min6,000/min
POST /v1/siteverify200/min2,000/min20,000/min
Admin API30/min60/min120/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
HeaderDescription
X-RateLimit-LimitMaximum requests allowed in the window
X-RateLimit-RemainingRequests remaining in the current window
X-RateLimit-ResetUnix timestamp when the window resets
Retry-AfterSeconds 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:

bash
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

  1. Generate a new key -- the old key remains active.
bash
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:

json
{
  "new_key": "mpt_secret_new_xyz789",
  "old_key_expires_at": "2026-04-16T12:00:00.000Z",
  "grace_period_days": 7
}
  1. Update your backend -- deploy the new secret key to your servers.

  2. Old key expires -- after the grace period (default 7 days), the old key is deactivated automatically.

Listing active keys

bash
curl https://api.kepca.com/v1/keys?site_key=mpt_site_abc123 \
  -H "Authorization: Bearer <jwt>"
json
{
  "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

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

Send the token to the KEPCA API:

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

js
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(),
  };
}
python
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:

HeaderDescription
X-Kepca-SignatureHMAC-SHA256 hex digest of the request body
X-Kepca-TimestampUnix timestamp when the webhook was sent
X-Kepca-Webhook-IdUnique delivery ID for idempotency

Verification steps

  1. Read the X-Kepca-Timestamp and X-Kepca-Signature headers.
  2. Concatenate the timestamp and the raw request body: timestamp.body.
  3. Compute HMAC-SHA256 using your webhook secret.
  4. Compare the result with the signature header (constant-time comparison).
  5. Reject requests older than 5 minutes to prevent replay attacks.
js
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 /metrics

No authentication required by default. In production, restrict access via network policy or the METRICS_AUTH_TOKEN environment variable.

Available metrics

MetricTypeDescription
kepca_requests_totalCounterTotal requests by endpoint and status
kepca_challenges_issued_totalCounterChallenges issued by type
kepca_challenges_solved_totalCounterChallenges solved by type
kepca_risk_score_histogramHistogramDistribution of risk scores
kepca_pow_difficulty_histogramHistogramDistribution of PoW difficulty levels
kepca_verification_duration_secondsHistogramToken verification latency
kepca_rate_limit_hits_totalCounterRate limit rejections by endpoint
kepca_active_sessionsGaugeCurrently 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

KVKK/GDPR Uyumlu — Verileriniz yurt icinde kalir.