Skip to content

REST API

All endpoints accept and return JSON. The base URL depends on your environment:

EnvironmentBase URL
Productionhttps://api.kepca.com
Self-hostedYour configured domain
Local devhttp://localhost:3001

Challenge flow (client-side)

These endpoints are called by the widget automatically. You generally do not call them from your backend.

POST /v1/{site_key}/init

Start a new session and submit initial signals for risk scoring.

Request:

json
{
  "signals": {
    "timestamp": 1712700000000,
    "time_on_page_ms": 5200,
    "submit_time_ms": 12400,
    "user_agent": "Mozilla/5.0 ...",
    "honeypot_filled": false,
    "ip_hash": "sha256_abc...",
    "tls_fingerprint": "ja3_xyz...",
    "request_count": 1
  }
}

Response 200:

json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "pre_score": 0.15,
  "requires_challenge": false
}

If requires_challenge is false, the user passed invisibly. If true, proceed to the challenge endpoint.

Errors:

CodeBodyCause
400{ "error": "invalid_body" }Missing or malformed signals

POST /v1/{site_key}/challenge

Request a challenge for an existing session.

Request:

json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "signals": {
    "timestamp": 1712700005000
  }
}

The signals field is optional. If provided, the risk score is recalculated.

Response 200:

json
{
  "challenge": {
    "type": "pow",
    "difficulty": 15,
    "token": "base64_challenge_token...",
    "expires_at": 1712700300
  },
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "risk_score": 0.45
}

Errors:

CodeBodyCause
400{ "error": "invalid_body" }Missing session_id
400{ "error": "invalid_session" }Session not found or expired

POST /v1/{site_key}/solve

Submit a challenge solution. On success, returns a signed verification token.

Request:

json
{
  "session_id": "550e8400-e29b-41d4-a716-446655440000",
  "solution": "a1b2c3d4..."
}

Response 200:

json
{
  "token": "eyJhbGciOiJIUzI1NiIsInYiOjF9.eyJzayI6Im1wdF9zaXRlX2FiYzEyMyIsInRzIjoxNzEyNzAwMDAwLCJleHAiOjE3MTI3MDAxMjAsInJzIjowLjQ1LCJjaCI6InBvdyIsImlwIjoic2hhMjU2X2FiYy4uLiIsImp0aSI6InVuaXF1ZV9pZCJ9.signature",
  "score": 0.45
}

Errors:

CodeBodyCause
400{ "error": "invalid_body" }Missing fields
400{ "error": "invalid_session" }Session not found
400{ "error": "challenge_expired" }Challenge TTL exceeded (5 min)
400{ "error": "invalid_solution" }Wrong PoW nonce

Verification (server-side)

POST /v1/siteverify

Verify a captcha token from your backend. This is the only endpoint you must call from your server.

Request:

json
{
  "secret": "mpt_secret_your_secret_key",
  "token": "eyJhbGciOiJIUzI1NiIsInYiOjF9...",
  "ip": "203.0.113.42"
}
FieldTypeRequiredDescription
secretstringYesYour site's secret key
tokenstringYesThe token from the widget
ipstringNoClient IP for additional validation

Response 200:

json
{
  "success": true,
  "score": 0.15,
  "challenge": "pow",
  "timestamp": "2026-04-09T12:00:00.000Z"
}

Error response 200:

json
{
  "success": false,
  "score": 0,
  "challenge": "pow",
  "timestamp": "2026-04-09T12:00:00.000Z",
  "error": "token_expired"
}

Possible error codes:

Error codeDescription
invalid_tokenToken cannot be decoded or signature invalid
token_expiredToken exp has passed
invalid_secretSecret key does not match the token's site
ip_mismatchProvided IP does not match token's IP hash
already_usedToken JTI was already verified (replay)

Events

POST /v1/events

Report custom security events for analytics and threat intelligence.

Request:

json
{
  "secret": "mpt_secret_your_secret_key",
  "events": [
    {
      "type": "login_failed",
      "ip": "203.0.113.42",
      "metadata": {
        "email": "attacker@example.com",
        "attempts": 5
      },
      "timestamp": "2026-04-09T12:05:00.000Z"
    }
  ]
}

Response 200:

json
{
  "accepted": 1
}

Analytics

GET /v1/stats/{site_key}

Retrieve verification statistics for a site. Requires authentication via the Authorization header.

Headers:

Authorization: Bearer <jwt_token>

Query parameters:

ParamTypeDefaultDescription
fromstring24h agoStart time (ISO 8601)
tostringnowEnd time (ISO 8601)
granularitystringhourminute, hour, day

Response 200:

json
{
  "site_key": "mpt_site_abc123",
  "period": {
    "from": "2026-04-08T12:00:00.000Z",
    "to": "2026-04-09T12:00:00.000Z"
  },
  "totals": {
    "requests": 12450,
    "passed": 11800,
    "challenged": 580,
    "blocked": 70
  },
  "by_challenge_type": {
    "invisible": 11800,
    "pow": 520,
    "checkbox": 60,
    "grid": 0,
    "slider": 0,
    "interactive": 0,
    "text": 0
  },
  "timeseries": [
    {
      "time": "2026-04-08T12:00:00.000Z",
      "requests": 520,
      "passed": 498,
      "challenged": 20,
      "blocked": 2
    }
  ]
}

Authentication

POST /v1/auth/register

Create a new account.

Request:

json
{
  "email": "admin@example.com",
  "password": "strongpassword123",
  "name": "Ada Lovelace"
}

Response 201:

json
{
  "user_id": "usr_abc123",
  "email": "admin@example.com",
  "name": "Ada Lovelace",
  "created_at": "2026-04-09T12:00:00.000Z"
}

Errors:

CodeErrorCause
400invalid_emailMalformed email address
409email_already_existsAccount already exists

POST /v1/auth/login

Authenticate and receive a JWT token.

Request:

json
{
  "email": "admin@example.com",
  "password": "strongpassword123"
}

Response 200:

json
{
  "token": "eyJhbGciOiJIUzI1NiJ9...",
  "expires_at": "2026-04-10T12:00:00.000Z",
  "user": {
    "user_id": "usr_abc123",
    "email": "admin@example.com",
    "name": "Ada Lovelace"
  }
}

Errors:

CodeErrorCause
401invalid_credentialsWrong email or password

Site management

POST /v1/sites

Create a new site and receive a site key + secret key pair.

Headers:

Authorization: Bearer <jwt_token>

Request:

json
{
  "name": "My Website",
  "domain": "example.com",
  "mode": "adaptive",
  "active_challenges": ["pow", "checkbox"],
  "risk_thresholds": {
    "invisible_max": 0.3,
    "checkbox_max": 0.6,
    "pow_max": 0.8
  }
}

Response 201:

json
{
  "site_key": "mpt_site_abc123",
  "secret_key": "mpt_secret_xyz789",
  "name": "My Website",
  "domain": "example.com",
  "mode": "adaptive",
  "active_challenges": ["pow", "checkbox"],
  "risk_thresholds": {
    "invisible_max": 0.3,
    "checkbox_max": 0.6,
    "pow_max": 0.8
  },
  "created_at": "2026-04-09T12:00:00.000Z"
}

WARNING

Store the secret_key securely. It is shown only once at creation time.

Errors:

CodeErrorCause
400invalid_domainMalformed domain
401unauthorizedMissing or invalid JWT
409domain_already_existsDomain registered to another site

Team management

All team endpoints require JWT authentication.

POST /v1/teams

Create a new team (organization).

Request:

json
{
  "name": "Acme Corp",
  "slug": "acme-corp"
}

Response 201:

json
{
  "team_id": "team_abc123",
  "name": "Acme Corp",
  "slug": "acme-corp",
  "owner_id": "usr_abc123",
  "created_at": "2026-04-09T12:00:00.000Z"
}

GET /v1/teams

List all teams the authenticated user belongs to.

Response 200:

json
{
  "teams": [
    {
      "team_id": "team_abc123",
      "name": "Acme Corp",
      "slug": "acme-corp",
      "role": "owner",
      "member_count": 5
    }
  ]
}

GET /v1/teams/{team_id}/members

List members of a team. Requires admin or owner role.

Response 200:

json
{
  "members": [
    {
      "user_id": "usr_abc123",
      "email": "owner@example.com",
      "name": "Ada Lovelace",
      "role": "owner",
      "joined_at": "2026-04-01T00:00:00.000Z"
    }
  ]
}

POST /v1/teams/{team_id}/invites

Invite a user to a team. Requires admin or owner role.

Request:

json
{
  "email": "colleague@example.com",
  "role": "admin"
}

Response 201:

json
{
  "invite_id": "inv_abc123",
  "email": "colleague@example.com",
  "role": "admin",
  "expires_at": "2026-04-16T12:00:00.000Z"
}

PATCH /v1/teams/{team_id}/members/{user_id}

Update a member's role. Requires owner role to promote to admin.

Request:

json
{
  "role": "member"
}

DELETE /v1/teams/{team_id}/members/{user_id}

Remove a member from the team. Requires admin or owner role.

Response 204: No content.


Domain management

POST /v1/domains

Add a custom domain for a site. Enterprise plan required.

Request:

json
{
  "domain": "captcha.example.com",
  "site_key": "mpt_site_abc123"
}

Response 201:

json
{
  "domain_id": "dom_abc123",
  "domain": "captcha.example.com",
  "status": "pending_verification",
  "cname_target": "cname.kepca.com",
  "txt_record": "_kepca-verify.captcha.example.com",
  "txt_value": "kepca-verify=abc123xyz"
}

GET /v1/domains

List all custom domains for the authenticated user's teams.

Response 200:

json
{
  "domains": [
    {
      "domain_id": "dom_abc123",
      "domain": "captcha.example.com",
      "site_key": "mpt_site_abc123",
      "status": "active",
      "created_at": "2026-04-05T00:00:00.000Z"
    }
  ]
}

DELETE /v1/domains/{domain_id}

Remove a custom domain.

Response 204: No content.


Webhook management

POST /v1/webhooks

Create a webhook endpoint to receive event notifications.

Request:

json
{
  "url": "https://example.com/webhooks/kepca",
  "events": ["verification.failed", "rate_limit.exceeded", "key.rotated"],
  "site_key": "mpt_site_abc123"
}

Response 201:

json
{
  "webhook_id": "wh_abc123",
  "url": "https://example.com/webhooks/kepca",
  "events": ["verification.failed", "rate_limit.exceeded", "key.rotated"],
  "secret": "whsec_abc123xyz",
  "status": "active",
  "created_at": "2026-04-09T12:00:00.000Z"
}

WARNING

Store the webhook secret securely. It is shown only once and is used to verify webhook signatures.

Available webhook events

EventDescription
verification.failedA token verification failed
verification.suspiciousHigh risk score detected (> 0.8)
rate_limit.exceededA rate limit was hit
key.rotatedAn API key was rotated
key.expiredA rotating API key's grace period ended
domain.verifiedA custom domain passed DNS verification
billing.limit_warningUsage reached 80% of plan limit
billing.limit_reachedUsage reached 100% of plan limit

GET /v1/webhooks

List all webhooks for the authenticated user.

Response 200:

json
{
  "webhooks": [
    {
      "webhook_id": "wh_abc123",
      "url": "https://example.com/webhooks/kepca",
      "events": ["verification.failed"],
      "status": "active",
      "created_at": "2026-04-09T12:00:00.000Z"
    }
  ]
}

DELETE /v1/webhooks/{webhook_id}

Delete a webhook endpoint.

Response 204: No content.


API key management

POST /v1/keys/rotate

Rotate a site's secret key. The old key remains active during a grace period.

Request:

json
{
  "site_key": "mpt_site_abc123",
  "key_type": "secret"
}

Response 200:

json
{
  "new_key": "mpt_secret_new_xyz789",
  "old_key_expires_at": "2026-04-16T12:00:00.000Z",
  "grace_period_days": 7
}

GET /v1/keys

List API keys for a site.

Query parameters:

ParamTypeDescription
site_keystringFilter by site key

Response 200:

json
{
  "keys": [
    {
      "key_id": "key_001",
      "prefix": "mpt_secret_***",
      "status": "active",
      "created_at": "2026-04-09T12:00:00.000Z"
    }
  ]
}

DELETE /v1/keys/{key_id}

Immediately revoke an API key.

Response 204: No content.


Billing

GET /v1/billing/plan

Get the current billing plan.

Response 200:

json
{
  "plan": "pro",
  "status": "active",
  "current_period_start": "2026-04-01T00:00:00.000Z",
  "current_period_end": "2026-05-01T00:00:00.000Z"
}

GET /v1/billing/usage

Get current period usage and limits.

Response 200:

json
{
  "period": "2026-04",
  "verifications": {
    "used": 142500,
    "limit": 500000,
    "percentage": 28.5
  },
  "sites": { "used": 5, "limit": 20 },
  "team_members": { "used": 4, "limit": 10 }
}

GET /v1/billing/limits

Get the rate limits for your current plan.

Response 200:

json
{
  "plan": "pro",
  "rate_limits": {
    "init": 1000,
    "challenge": 600,
    "solve": 600,
    "siteverify": 2000,
    "admin": 60
  },
  "unit": "requests_per_minute"
}

Audit log

GET /v1/audit

Query the audit log. Available on Pro and Enterprise plans.

Query parameters:

ParamTypeDefaultDescription
fromstring30d agoStart time (ISO 8601)
tostringnowEnd time (ISO 8601)
actorstring--Filter by user ID
actionstring--Filter by action (e.g., site.*)
limitnumber50Results per page (max 100)
cursorstring--Pagination cursor

Response 200:

json
{
  "entries": [
    {
      "id": "aud_001",
      "timestamp": "2026-04-09T12:00:00.000Z",
      "actor": {
        "user_id": "usr_abc123",
        "email": "admin@example.com",
        "ip": "203.0.113.42"
      },
      "action": "site.settings_updated",
      "resource": { "type": "site", "id": "mpt_site_abc123" },
      "changes": {
        "mode": { "from": "adaptive", "to": "always-challenge" }
      }
    }
  ],
  "has_more": true,
  "cursor": "aud_001"
}

Health check

GET /health

Returns the health status of the API. No authentication required.

Response 200:

json
{
  "status": "healthy",
  "version": "1.2.0",
  "uptime_seconds": 86400,
  "services": {
    "postgresql": "connected",
    "valkey": "connected"
  }
}

Response 503:

json
{
  "status": "degraded",
  "version": "1.2.0",
  "services": {
    "postgresql": "connected",
    "valkey": "disconnected"
  }
}

Metrics

GET /metrics

Prometheus-compatible metrics endpoint. See Security > Prometheus metrics for the full list of available metrics.

No authentication required by default. Restrict access via network policy or METRICS_AUTH_TOKEN environment variable in production.

KVKK/GDPR Uyumlu — Verileriniz yurt icinde kalir.