Checkpoint Docs

Public API Reference

Complete reference for the Checkpoint REST API

Overview

The Checkpoint Public API provides programmatic access to detection services, project data, analytics, governance, and the platform's supporting surfaces. Endpoints are RESTful and use JSON. Most endpoints require API key authentication; the public client-ingestion endpoints (/event, /pixel, /pixel/track) authenticate by project/pixel ID in the request body, and the gateway, Molti, and audit families each authenticate by their own flow instead: see Authentication.

Scope of this reference. It documents the public /api/v1 surface end to end: core detection, events, pixels, projects, Shopify, enforcement, policy, webhooks, Bouncer (Govern), audit, gateway, managed-agent (Molti), OpenClaw, vault, fingerprint, and the supporting infrastructure endpoints. Deliberately not covered, because they authenticate with a dashboard session or admin rights (or are internal edge plumbing): the gateway connection, domain, install, and .mcpb routes; the Molti deploy, restart, stop, and API-key rotation routes; the Wink identity routes (wink/session, wink/verify); the edge-signed pixel/analytics route; and everything under /api/internal/*, which backs the dashboard itself. If an endpoint isn't documented here, treat it as unsupported rather than undocumented.

Base URL

All API requests should be made to:

https://kya.vouched.id/api/v1

For local development:

http://localhost:3000/api/v1

Authentication

Checkpoint uses API keys to authenticate requests. See Credentials for where to find your Project ID and API key.

Include your API key in the request header:

curl -X POST https://kya.vouched.id/api/v1/detect \
  -H "X-API-Key: your_api_key_here" \
  -H "Content-Type: application/json" \
  -d '{"userAgent": "Mozilla/5.0..."}'
const response = await fetch('https://kya.vouched.id/api/v1/detect', {
  method: 'POST',
  headers: {
    'X-API-Key': 'your_api_key_here',
    'Content-Type': 'application/json',
  },
  body: JSON.stringify({ userAgent: 'Mozilla/5.0...' }),
});
import requests

response = requests.post(
    'https://kya.vouched.id/api/v1/detect',
    headers={
        'X-API-Key': 'your_api_key_here',
        'Content-Type': 'application/json',
    },
    json={'userAgent': 'Mozilla/5.0...'},
)

Which endpoints need a key?

EndpointsAuth
/detect, /batch, /log-detection, /enforce, /policy, /middleware/policy, /projects/*, /shopify/projects/*, /webhooks/*, /kya-os/events, /openclaw/config, /openclaw/approve, most /bouncer/*API key (X-API-Key)
/event, /pixel, /pixel/trackPublic: authenticated by the project/pixel ID in the body
/status, /search, /demo/generate, most /fingerprint/*Public: no authentication
/gateway/*Flow-authenticated (OAuth code + PKCE, browser session, state token, or gateway assertion JWT); never an API key
/molti/*Per-deployment heartbeat token (X-Heartbeat-Token); API key for BYOK config-bundle reads
/vault/{projectId}/resolveAPI key plus an Ed25519 assertion JWT in the body
/audit/entriesAudit ingest credential (x-kya-audit-key or Authorization: Bearer)
/llm/anthropic/v1/messagesPer-deployment gateway token (x-api-key header)

Exceptions inside /bouncer/* (each authenticated by its flow rather than an API key, by design): the user-facing consent link (GET /bouncer/authorize) and delegation status polling (GET /bouncer/delegations/status/{requestId}) are public; delegation token retrieval authenticates with Authorization: Bearer <delegation_token>; delegation creation (POST /bouncer/projects/{projectId}/delegations) is part of the browser-based consent flow; the OAuth token endpoint (POST /bouncer/oauth/token) authenticates with the single-use authorization code it exchanges; and the OAuth callback (GET /bouncer/oauth/callback) is the provider's redirect target, validated by its state parameter. The OpenClaw consent endpoint (/openclaw/consent/{requestId}) similarly authenticates with the consent token minted by its approval webhook.

Core Detection APIs

Detect AI Agent

Analyzes a request to determine if it originates from an AI agent, bot, or human.

Endpoint: POST /api/v1/detect

Auth: API key (X-API-Key), read permission.

Request:

{
  "userAgent": "Mozilla/5.0 AppleWebKit/537.36 (compatible; GPTBot/1.2; +https://openai.com/gptbot)",
  "ipAddress": "203.0.113.7",
  "headers": {
    "accept": "*/*"
  },
  "url": "https://example.com/pricing",
  "method": "GET"
}

Only userAgent is required. Optional fields: ipAddress (falls back to the connecting IP when omitted), headers, url, method, behavioralData, requestId, timestamp, and options (mode: strict | balanced | lenient, includeMetadata, includeBreakdown).

Response:

{
  "success": true,
  "data": {
    "result": {
      "isAgent": true,
      "confidence": 92,
      "detectionClass": { "type": "AiAgent", "agentType": "chatgpt" },
      "reasons": ["User agent matched known AI agent pattern"],
      "signals": [],
      "detectedAgent": { "type": "chatgpt", "name": "ChatGPT-User" },
      "verificationMethod": "pattern",
      "timestamp": "2026-07-27T12:00:00.000Z"
    },
    "metadata": {
      "processingTime": 12,
      "wasmUsed": true,
      "cacheHit": false
    }
  },
  "metadata": {
    "requestId": "req_abc123",
    "timestamp": "2026-07-27T12:00:00.000Z"
  }
}

data.result is the detection detail. data.breakdown (a per-method confidence breakdown) is included when options.includeBreakdown is set, and data.behavioralAnalysis (isHumanLike, confidence, patterns, anomalies) is included when the request carried behavioralData.

Detection Classes:

result.detectionClass is a tagged object — match on its type:

typeDescription
HumanRegular browser traffic
AiAgentAI assistants and crawlers (ChatGPT, Claude, Perplexity) — may carry agentType, vendor, model
BotTraditional bots (search engines, scrapers, monitoring) — may carry botType, legitimacy
IncompleteDataInsufficient signals for classification

The wire schema also declares Automation and Unknown, but the detection engine never emits either — treat them as dead values, not classes to branch on.

Confidence Scores:

Scores range from 0–100. See Confidence Distribution for guidance on threshold selection.

Errors: 401 AUTH_MISSING_CREDENTIALS / AUTH_INVALID_API_KEY / AUTH_EXPIRED_API_KEY (see Common Error Codes for the shared shape), 429 RATE_LIMIT_EXCEEDED once your plan's per-minute limit is exceeded, 400 VALIDATION_INVALID_REQUEST when the body fails schema validation (e.g. missing userAgent).

Event Tracking

Send Event

Records a tracking event for analytics.

Endpoint: POST /api/v1/event

Auth: Public — authenticated by the pixelId in the body (no API key required).

Request:

{
  "pixelId": "b1c2d3e4-5678-90ab-cdef-1234567890ab",
  "sessionId": "session_abc",
  "url": "https://example.com/page",
  "userAgent": "Mozilla/5.0...",
  "referrer": "https://google.com",
  "metadata": { "custom": "data" }
}

Required fields: pixelId (UUID), sessionId, url, userAgent.

Response:

{
  "success": true,
  "eventId": "0b6e2c9a-4a6f-4d5e-9a1b-2c3d4e5f6a7b",
  "sessionId": "session_abc",
  "detection": {
    "isAgent": false,
    "confidence": 88,
    "detectionClass": { "type": "Human" },
    "reasons": [],
    "signals": [],
    "verificationMethod": "pattern",
    "timestamp": "2026-07-27T12:00:00.000Z"
  },
  "rateLimit": { "limit": 60, "remaining": 59, "reset": 1234567890 }
}

detection is the shared detection-result object (isAgent, confidence, detectionClass, signals, reasons, verificationMethod, timestamp); additional fields may be present (agentType, detectedAgent, riskLevel, metadata). A debug object is added in development builds only. The status is 200, or 403 with this same body (not the standard error envelope) when the project's blockOnHighConfidence setting is on and this event's confidence meets the configured threshold.

Errors: 404 PIXEL_NOT_FOUND for an invalid pixelId, 403 PIXEL_DISABLED for a disabled pixel, 403 PIXEL_DOMAIN_NOT_ALLOWED when the pixel has an allowedDomains list configured and the event's URL doesn't match, 429 RATE_LIMIT_EXCEEDED.

Batch Events

Send multiple events in a single request.

Endpoint: POST /api/v1/batch

Auth: API key (X-API-Key), write permission.

Each event uses the same shape as Send Event — every event must include its own pixelId. Maximum 100 events per batch.

Request:

{
  "events": [
    {
      "pixelId": "b1c2d3e4-...",
      "sessionId": "s1",
      "url": "https://example.com/p1",
      "userAgent": "Mozilla/5.0..."
    },
    {
      "pixelId": "b1c2d3e4-...",
      "sessionId": "s2",
      "url": "https://example.com/p2",
      "userAgent": "Mozilla/5.0..."
    }
  ]
}

Response: always 200 for a well-formed batch — per-event failures (invalid or disabled pixel) are reported inside results[] as { success: false, error: { code, message } } rather than failing the whole request.

Errors: 413 PIXEL_BATCH_TOO_LARGE when events.length exceeds 100, 429 RATE_LIMIT_EXCEEDED, 400 VALIDATION_INVALID_REQUEST for a malformed body. Per-item PIXEL_NOT_FOUND / PIXEL_DISABLED surface inside results[], not as the top-level error.

Pixel Tracking

Pixel Endpoint

Ingestion endpoint for the Marketing Pixel — the endpoint the pixel JS beacon calls on every page load.

Endpoint: POST /api/v1/pixel

Auth: Public — authenticated by the project/pixel ID (pixelId or projectId) in the body.

Request: a JSON pixel-tracking request — pixelId/projectId, sessionId, url, userAgent, plus optional behavioral and fingerprint signals. This endpoint's request schema (PixelTrackingRequestSchema) is large and internal to the beacon package; it is not itemized field-by-field here. Build against the Marketing Pixel beacon rather than this raw endpoint.

Response: a JSON detection result (classification + confidence). This endpoint is POST/JSON — it does not serve a 1×1 tracking GIF.

Errors: 400 MISSING_PIXEL_ID when pixelId is empty, 400 PROJECT_NOT_FOUND for an unrecognized project/pixel ID (this endpoint returns 400, not 404, for that case — it's the one exception among the project-scoped endpoints on this page), 403 INVALID_EDGE_SIGNATURE on a failed internal Edge-redirect signature check, 429 RATE_LIMIT_EXCEEDED.

Shopify Pixel Track

Ingestion endpoint for the Shopify Web Pixel Extension: receives storefront events (page_view, product_view, cart_view, and similar) and stores each one as a detection so it appears in Analytics. Detection runs server-side from the request's own User-Agent and headers, because Shopify's sandboxed pixel environment cannot run client-side fingerprinting; accuracy is lower than the standard pixel beacon.

Endpoint: POST /api/v1/pixel/track

Auth: Public, authenticated by the projectId in the body (accepts a project UUID or its friendlyId). CORS is open (Access-Control-Allow-Origin: *) and OPTIONS preflight is supported.

Request:

{
  "projectId": "acme-corp",
  "sessionId": "session_abc",
  "eventType": "page_view",
  "timestamp": "2026-07-27T12:00:00.000Z",
  "data": {
    "url": "https://store.example.com/products/widget",
    "pathname": "/products/widget",
    "referrer": "https://google.com",
    "title": "Widget",
    "product": { "id": "123", "title": "Widget" },
    "cart": { "totalQuantity": 2 },
    "checkout": { "token": "chk_abc", "order": { "id": "456" } }
  },
  "metadata": { "source": "shopify-web-pixel", "version": "1.0.0" }
}

Required fields: projectId, sessionId, eventType. Optional fields: timestamp (ISO 8601), correlationId (cross-surface correlation id, validated and normalized server-side), data (every field optional, extra keys preserved), and metadata (source, version, extra keys preserved).

Response:

{
  "success": true,
  "eventId": "d4c0f6f2-5678-90ab-cdef-1234567890ab",
  "sessionId": "session_abc",
  "processingTime": 45
}

eventId is a server-generated acknowledgment id, not the stored detection's id. The detection verdict is not returned on this endpoint; results appear in the dashboard.

Rate limits: requests are limited per client IP + project at the Free-plan limits regardless of your plan, because this endpoint is unauthenticated. A 429 carries X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset headers.

Errors: 400 INVALID_JSON for an unparsable body, 400 VALIDATION_ERROR with a per-field details array when the payload fails validation, 404 PROJECT_NOT_FOUND for an unrecognized projectId, 429 RATE_LIMITED, 500 PIXEL_CREATION_FAILED when the project's default pixel record can't be created or found, 500 INTERNAL_ERROR on an unexpected failure.

This endpoint's rate-limit error code is RATE_LIMITED, not the RATE_LIMIT_EXCEEDED used elsewhere in the API.

Log Detection

Persists a detection produced out-of-band (e.g. by the Gateway Worker) to your dashboard. The middleware SDKs call this after an edge detection so it appears in Analytics.

Endpoint: POST /api/v1/log-detection

Auth: API key (X-API-Key) scoped to a project, read permission.

Request: the detection detail plus request context (and optional enforcement / engine / SDK / identity metadata). The full shape (LogDetectionRequestSchema) mirrors the shared detection-result type rather than a page-specific one and is not itemized field-by-field here.

Response: 202 Accepted immediately — the write happens asynchronously (fire-and-forget). A 202 means the request was accepted, not that the write has completed or succeeded; persistence failures are logged server-side and are not surfaced back to the caller.

Errors: 401 AUTH_INVALID_API_KEY when the API key has no associated project, 400 VALIDATION_INVALID_REQUEST for a malformed body.

Project Management

List Projects

Get all projects associated with your API key.

Endpoint: GET /api/v1/projects

Auth: API key (X-API-Key). A project-scoped key returns exactly its project; an unscoped key returns every project across the key owner's organizations. Permission scope (read / write / admin) does not affect list visibility — it gates what you may do, not what you can see here.

Request: query parameters (no body):

ParameterDefaultDescription
page1Page number
limit20Results per page (max 100)
searchSearch term

Response:

List endpoints return a paginated envelope — the array is always under data:

{
  "success": true,
  "data": [
    {
      "id": "acme-corp",
      "name": "My Project",
      "createdAt": "2024-01-01T00:00:00.000Z",
      "status": "active",
      "stats": {
        "pixelCount": 2,
        "detectionCount": 1234,
        "lastDetection": "2024-01-07T09:30:00.000Z"
      }
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 20,
    "total": 50,
    "totalPages": 3,
    "hasMore": true
  },
  "version": "v1",
  "timestamp": "2026-07-27T12:00:00.000Z"
}

status is active when the project has at least one pixel, otherwise inactive.

Errors: 401 AUTH_MISSING_CREDENTIALS / AUTH_INVALID_API_KEY for missing or invalid keys.

Get Project Details

Endpoint: GET /api/v1/projects/{projectId}

Auth: API key (X-API-Key) with access to this specific project.

Request: no body or query parameters. {projectId} accepts the project UUID or its friendlyId.

Response:

{
  "success": true,
  "data": {
    "id": "acme-corp",
    "name": "My Project",
    "createdAt": "2024-01-01T00:00:00.000Z",
    "status": "active",
    "stats": {
      "pixelCount": 2,
      "detectionCount": 1234,
      "lastDetection": "2024-01-07T09:30:00.000Z"
    }
  },
  "version": "v1",
  "timestamp": "2026-07-27T12:00:00.000Z"
}

Errors: 403 PERMISSION_FORBIDDEN when your API key isn't scoped to this project, 404 PROJECT_NOT_FOUND when the project doesn't exist.

Get Project Detections

Endpoint: GET /api/v1/projects/{projectId}/detections

Auth: API key (X-API-Key) with access to this specific project, read permission.

Request: query parameters (no body):

ParameterDescription
pagePage number (default 1)
limitResults per page (default 50, max 100)
startDateFilter start (ISO 8601)
endDateFilter end (ISO 8601)
botTypeFilter by detected agent type

Response:

{
  "success": true,
  "data": [
    {
      "id": "det_123",
      "timestamp": "2024-01-01T12:00:00.000Z",
      "isBot": true,
      "confidence": 0.92,
      "botType": "ChatGPT",
      "userAgent": "Mozilla/5.0...",
      "url": "https://example.com"
    }
  ],
  "pagination": {
    "page": 1,
    "limit": 50,
    "total": 1000,
    "totalPages": 20,
    "hasMore": true
  },
  "version": "v1",
  "timestamp": "2026-07-27T12:00:00.000Z"
}

On this endpoint confidence is normalized to 0–1 (a 92%-confidence detection is 0.92). The /detect endpoint returns confidence on the 0–100 scale.

Errors: 403 PERMISSION_FORBIDDEN when your API key isn't scoped to this project, 400 VALIDATION_INVALID_START_DATE / VALIDATION_INVALID_END_DATE / VALIDATION_INVALID_DATE_RANGE for malformed or inverted date filters.

Get Project Analytics

Endpoint: GET /api/v1/projects/{projectId}/analytics

Auth: API key (X-API-Key) with access to this specific project.

Request: query parameters (no body):

ParameterDescription
periodTime period (hour, day, week, month; default day)
startDateStart date (ISO 8601)
endDateEnd date (ISO 8601)

Response:

{
  "success": true,
  "data": {
    "period": "day",
    "startDate": "2024-01-01",
    "endDate": "2024-01-07",
    "summary": {
      "totalDetections": 10000,
      "uniqueBots": 300,
      "averageConfidence": 78.5
    },
    "timeSeries": [
      {
        "date": "2024-01-01",
        "detections": 1500,
        "bots": 75,
        "humans": 1425
      }
    ],
    "botTypes": {
      "chatgpt": 45,
      "claude": 20,
      "googlebot": 10
    }
  },
  "version": "v1",
  "timestamp": "2026-07-27T12:00:00.000Z"
}

Errors: 400 INVALID_PERIOD for a period outside hour/day/week/month, 400 INVALID_START_DATE / INVALID_END_DATE / INVALID_DATE_RANGE for malformed or inverted date filters, 500 ANALYTICS_FETCH_ERROR on a downstream aggregation failure.

Managed Deploy

Endpoint: POST /api/v1/deploy/managed

Auth: API key (X-API-Key) with admin permission.

Deploys a KYA-OS agent to Checkpoint's managed hosting on behalf of an existing user, so the deployment appears in that user's dashboard. Built for cross-app integrations — this is not part of the normal dashboard deploy flow.

Request: the body takes projectName (required), template (default "blank"), enableReputation (default true), and at least one user identifier: userEmail or userGithubId. The user must already have a Checkpoint account — the endpoint never creates accounts.

Response: 201 with { projectId, friendlyId, workerUrl, agentDid, ktaClaimUrl, dashboardUrl }. See Deploying a KYA-OS Server for the deployment models.

Errors: 400 VALIDATION_ERROR for a malformed body, 400 MISSING_USER_IDENTIFIER when neither userEmail nor userGithubId is set, 404 USER_NOT_FOUND when no existing account matches the identifier (this endpoint never creates one), 400 NO_ORGANIZATION when the matched user has no organization to deploy into, 400 DEPLOY_FAILED (or a more specific code from the deploy service) on a failed deployment.

Shopify Endpoints

Public, API-key-authenticated endpoints for the Shopify integration to read and update its own project. See that page for setup; this section documents the wire contract.

Get Shopify Project

Endpoint: GET /api/v1/shopify/projects/{projectId}

Auth: API key (X-API-Key) with access to this project, read permission.

Request: no body or query parameters. {projectId} accepts the project UUID or its friendlyId.

Response: project id, friendlyId, name, domain, settings, stats (pixelCount/detectionCount/lastDetection), createdAt, updatedAt — a simplified, public-safe projection of the project record (no sensitive fields).

Errors: 404 PROJECT_NOT_FOUND when the project doesn't exist or was deleted, 403 PERMISSION_FORBIDDEN when your API key isn't scoped to this project.

Update Shopify Project

Endpoint: PATCH /api/v1/shopify/projects/{projectId}

Auth: API key (X-API-Key) with access to this project, write permission.

Request: { settings?: { blockOnHighConfidence?, confidenceThreshold?, enableSessionTracking?, enableWasm? } } — settings are merged with the project's existing settings, not replaced.

Response: the updated project's id, friendlyId, name, domain, settings, updatedAt.

Errors: 400 INVALID_REQUEST for unparsable JSON, 400 VALIDATION_ERROR for a body that fails ShopifyProjectSettingsUpdateSchema, 404 PROJECT_NOT_FOUND, 403 PERMISSION_FORBIDDEN.

Get Shopify Project Stats

Endpoint: GET /api/v1/shopify/projects/{projectId}/stats

Auth: API key (X-API-Key) with access to this project, read permission.

Request: query parameters (no body):

ParameterDefaultDescription
startDate24 hours agoISO date string
endDatenowISO date string
page1Page number
limit100 (max 100)Results per page

Response: { totalRequests, blockedBots, allowedRequests, threatLevel, recentThreats, pagination }. threatLevel is low / medium / high, derived from the percentage of sessions classified as AI agents. recentThreats is non-human sessions grouped by consolidated_session_id, each with a pagesVisited count.

Errors: 404 PROJECT_NOT_FOUND, 403 PERMISSION_FORBIDDEN, 400 INVALID_DATE / INVALID_DATE_RANGE for malformed or inverted date filters, 500 INTERNAL_DATABASE_ERROR / STATS_FETCH_ERROR on a downstream failure.

Enforce API

Evaluate Request

Submit a request for enforcement evaluation against your project's policies. This is the endpoint the middleware SDKs call — the project is resolved from your API key, not the body.

Endpoint: POST /api/v1/enforce

Auth: API key (X-API-Key), read permission.

Request:

All fields are optional — send what you have:

{
  "userAgent": "Mozilla/5.0...",
  "ipAddress": "203.0.113.7",
  "headers": {},
  "path": "/api/data",
  "method": "GET",
  "options": {
    "includeDetectionResult": true
  }
}

Other accepted fields: url, requestId, action ("get_policy" for a policy-only fetch, omit for detection + enforcement), source, and options.skipDetection / options.cacheTTL.

Response:

{
  "success": true,
  "data": {
    "decision": {
      "action": "block",
      "reason": "above_threshold",
      "isAgent": true,
      "confidence": 95,
      "agentName": "ChatGPT-User",
      "agentType": "chatgpt"
    },
    "processingTimeMs": 42,
    "requestId": "req_abc123",
    "detection": {
      "isAgent": true,
      "confidence": 95,
      "detectionClass": "ai_agent",
      "agentName": "ChatGPT-User",
      "agentType": "chatgpt",
      "verificationMethod": "pattern",
      "reasons": ["User agent matched known AI agent pattern"]
    }
  }
}

decision.action is one of allow, block, redirect, challenge, or logrewrite is not part of this set. This endpoint layers two decision sources, and both coerce instruct down to block before it can reach decision.action:

  • The bespoke policy path (deny/allow lists, confidence threshold, default_action) narrows the stored 6-action default_action via narrowToEnforcementAction() (lib/policy/policy-coerce.ts), which maps instruct → block. A default_action of challenge survives that narrowing but has no execution path yet in resolveAboveThresholdAction() (lib/services/detection-enforcement.service.ts) and also falls back to block — so the bespoke path alone never returns challenge either.
  • The composed-Cedar layer that can override the bespoke decision (applyComposedPolicy, lib/services/composed-policy-enforce.service.ts) is the one path that can set action: "challenge" directly, when the project has Cedar policy authoring enabled and the engine's decision is a Challenge. Its own Instruct decision is likewise mapped to block (with the machine-readable guidance folded into decision.message) because the 5-action enforce wire contract has no instruct value.

decision.redirectUrl and decision.message appear when relevant. The detection object is included only when options.includeDetectionResult is set, and there it carries the wire-form detectionClass (human | ai_agent | bot | incomplete_data).

The decision also rides on response headers, so middleware can act without parsing the body:

  • KYA-Enforce-Action: the decision action
  • KYA-Processing-Ms: server processing time
  • KYA-Detection-Method: which detection engine produced the verdict

Errors: 401 AUTH_INVALID_API_KEY when the API key has no associated project, 404 PROJECT_NOT_FOUND / 500 POLICY_FETCH_FAILED on the action: "get_policy" path if the project can't be resolved, 400 VALIDATION_INVALID_REQUEST for a malformed body.

Policy Endpoints

Get Customer Policy

Returns the detection policy for the authenticated project. Consumed by the checkpoint-wasm-runtime to enforce customer-defined policies at the edge.

Endpoint: GET /api/v1/policy (also HEAD /api/v1/policy for cache validation, headers only)

Auth: API key: X-API-Key header or Authorization: Bearer <api-key> (an api_key/apiKey query parameter is also accepted but not recommended). The key must be associated with a project.

Request: No parameters.

Response:

{
  "projectId": "proj_abc123",
  "denyList": ["did:web:agent.example.com", "BadBot"],
  "allowList": ["GoodBot"],
  "blockThreshold": 80,
  "pathRules": [{ "pattern": "/health", "action": "allow" }],
  "version": "m1x2y3z",
  "updatedAt": "2026-07-27T12:00:00.000Z"
}

Unlike most endpoints on this page, the policy object is the response body itself, not wrapped in a success/data envelope.

  • denyList merges the project's Bouncer denied agents (client DID, agent DID, or client name), the pixel configuration's blockedAgents, and the active checkpoint's deny list.
  • allowList merges the pixel configuration's allowedAgents with the active checkpoint's allow list, and is omitted entirely when empty.
  • blockThreshold is on the 0 to 100 scale (a stored 0 to 1 fraction is converted; the default is 80).
  • pathRules entries are { pattern, action, agents? } with action declared as allow, block, or challenge; in the current implementation only allow rules are ever generated (from the pixel configuration's skipPaths), and the field is omitted when there are none.
  • version is a base36 timestamp used for cache invalidation and echoed as the ETag.

Responses carry Cache-Control: private, max-age=300 and an ETag; HEAD returns the same caching headers with no body.

Errors: 401 AUTH_MISSING_CREDENTIALS when no key is provided, 401 AUTH_INVALID_API_KEY for an invalid key or a key with no associated project, 401 AUTH_EXPIRED_API_KEY for an expired key, 404 PERMISSION_RESOURCE_NOT_FOUND when the project no longer exists.

Get Middleware Policy

Dedicated endpoint for middleware clients (checkpoint-dotnet, checkpoint-express) to fetch their project's enforcement policy as a flat DTO. This is the single source of truth for middleware policy responses.

Endpoint: GET /api/v1/middleware/policy (plus OPTIONS for CORS preflight)

Auth: API key (X-API-Key or Authorization: Bearer), read permission, project-scoped.

Request: No parameters; the project is resolved from the API key.

Response:

{
  "success": true,
  "data": {
    "agent_did": null,
    "denied_agents": ["did:web:agent.example.com"],
    "allowed_agents": [],
    "reputation_threshold": null,
    "confidence_threshold": 80,
    "default_action": "allow",
    "redirect_url": null
  }
}

agent_did and reputation_threshold are always null in the current implementation. Structured (v3.2) and legacy stored policies are both coerced to this flat wire shape. When default_action is redirect or instruct and the project has no explicit redirect URL, redirect_url is synthesized as https://kya.vouched.id/connect/{friendlyId}.

Responses are marked Cache-Control: private, no-store with Vary: Authorization, X-API-Key, X-Project-Id and an X-Request-ID header.

Errors: 401 AUTH_MISSING_CREDENTIALS / AUTH_INVALID_API_KEY / AUTH_EXPIRED_API_KEY for missing, invalid (including no associated project), or expired keys, 403 PERMISSION_INSUFFICIENT when the key lacks read permission, 404 PROJECT_NOT_FOUND when the key's project no longer exists.

Webhooks

Manage outbound webhook subscriptions for a project. Events are delivered as CloudEvents 1.0 envelopes signed with HMAC-SHA256.

Auth (all webhook endpoints): API key via X-API-Key header (Authorization: Bearer <key> also accepted). The key must be project-scoped: an unscoped key gets 400 VALIDATION_BAD_REQUEST ("API key must be scoped to a project to manage webhooks"). Requests are rate limited per key owner; responses carry X-Request-ID and X-API-Version: v1.

Envelope: success responses are { success: true, data, metadata: { requestId, timestamp, version: "v1" } }; errors are { success: false, error: { code, message, details? }, metadata }.

Shared errors (all webhook endpoints): 401 AUTH_MISSING_CREDENTIALS / AUTH_INVALID_API_KEY / AUTH_EXPIRED_API_KEY, 403 PERMISSION_INSUFFICIENT when the key lacks the required permission, 429 RATE_LIMIT_EXCEEDED from the per-key rate limiter, 400 VALIDATION_BAD_REQUEST for a non-project-scoped key, 404 PERMISSION_RESOURCE_NOT_FOUND when the subscription id does not exist or belongs to another project.

Verified limitation: on the create and update endpoints, a body that fails Zod validation (for example a non-HTTPS URL or an unrecognized event type) currently surfaces as 500 INTERNAL_SERVER_ERROR, not 400. The route parses with schema.parse() and the v1 error formatter has no ZodError branch, so the raised ZodError falls through to the generic 500 path (message hidden in production). Derived from lib/api/v1-middleware.ts (v1ErrorResponse); no test pins a different status.

Subscription object

All endpoints that return a subscription use this serialization (the signing secret is never included after creation; reads show the fixed placeholder whsec_***):

{
  "id": "0b7f6c9e-...",
  "project_id": "b1c2d3e4-...",
  "url": "https://example.com/hooks/checkpoint",
  "event_types": ["detection.created"],
  "secret": "whsec_***",
  "description": "Prod notifications",
  "status": "active",
  "payload_mode": "full",
  "is_internal": false,
  "filters": { "minConfidence": 0.8 },
  "failure_count": 0,
  "verified_at": "2026-08-19T12:00:00.000Z",
  "last_delivery_at": "2026-08-19T12:05:00.000Z",
  "created_at": "2026-08-19T11:59:00.000Z",
  "updated_at": "2026-08-19T12:05:00.000Z"
}

status is one of pending_verification, active, paused, disabled. Subscribable event types: detection.created, detection.session_ended, enforcement.decided, delegation.created, delegation.revoked, delegation.expired. Maximum 25 subscriptions per project.

Delivery format and signature verification

Deliveries are CloudEvents 1.0, structured JSON mode:

{
  "specversion": "1.0",
  "id": "evt_9f2c...",
  "type": "detection.created",
  "source": "https://kya.vouched.id/projects/{projectId}",
  "subject": "det_123",
  "time": "2026-08-19T12:00:00.000Z",
  "datacontenttype": "application/json",
  "dataschema": "https://kya.vouched.id/schemas/v1/detection.created",
  "data": { "...": "event payload" }
}

payload_mode: "compact" trims data to essential fields (id, projectId, classification/confidence for detection events, status/action for enforcement and delegation events, timestamps) and appends .compact to the dataschema URI.

Every delivery carries three headers:

HeaderValue
X-Webhook-IdCloudEvent id (idempotency key)
X-Webhook-TimestampUnix seconds at signing time
X-Webhook-Signaturesha256=<hex HMAC-SHA256 of "<timestamp>.<raw request body>">

Verify by computing HMAC-SHA256(secret, timestamp + "." + rawBody) with your subscription secret, comparing against the header value (strip the sha256= prefix) using a timing-safe comparison, and rejecting timestamps older than your tolerance (the code comments recommend 5 minutes). Delivery requests use User-Agent: AgentShield-Webhooks/1.0.

Endpoint verification challenge: at creation, on URL change, and via the verify endpoint, Checkpoint POSTs { "type": "webhook.verification", "challenge": "whk_verify_..." } to your URL with header X-Webhook-Verification: true. Your endpoint must respond 2xx with a JSON body echoing the same challenge value within 10 seconds. The challenge request itself is not HMAC-signed: at creation time you have not yet received the secret.

SSRF policy: webhook URLs must be HTTPS and must not resolve to private, loopback, link-local, or otherwise internal addresses. DNS is re-resolved before every outbound call and redirects are refused. A blocked URL returns 400 VALIDATION_BAD_REQUEST with a "Webhook URL is blocked" message.

List Webhook Subscriptions

Endpoint: GET /api/v1/webhooks

Auth: API key (X-API-Key), project-scoped, read permission.

Request: no parameters.

Response: 200; data is an array of subscription objects for the key's project. Not paginated.

Errors: shared errors only.

Create Webhook Subscription

Endpoint: POST /api/v1/webhooks

Auth: API key (X-API-Key), project-scoped, write permission.

Request:

{
  "url": "https://example.com/hooks/checkpoint",
  "event_types": ["detection.created", "enforcement.decided"],
  "description": "Prod notifications",
  "payload_mode": "full",
  "filters": {
    "minConfidence": 0.8,
    "agentTypes": ["ai_agent"],
    "excludeAgentTypes": ["human"]
  }
}

url (required): HTTPS, SSRF-validated. event_types (required): at least one subscribable type. description: optional, max 500 chars. payload_mode: full (default) or compact. filters (optional): minConfidence is 0 to 1 and applies to detection.* events only; agentTypes / excludeAgentTypes are classification include/exclude lists.

The verification challenge is sent synchronously before the row is created: if your endpoint echoes the challenge, the subscription is created with status: "active"; otherwise status: "pending_verification".

Response: 201 with the subscription object plus secret, the raw whsec_... signing secret. This is the only time the secret is returned; store it now.

Errors: 429 RATE_LIMIT_QUOTA_EXCEEDED at the 25-per-project cap (a permanent cap, distinct from the transient RATE_LIMIT_EXCEEDED; free room by deleting a subscription), 400 VALIDATION_BAD_REQUEST for an SSRF-blocked URL, 500 INTERNAL_SERVER_ERROR for a body failing schema validation (see limitation note above), plus shared errors.

Get Webhook Subscription

Endpoint: GET /api/v1/webhooks/{id}

Auth: API key (X-API-Key), project-scoped, read permission.

Response: 200; the subscription object plus recent_events, the last 10 delivery attempts, each:

{
  "id": "e5a1...",
  "event_type": "detection.created",
  "status": "delivered",
  "response_status": 200,
  "response_time_ms": 143,
  "retry_count": 0,
  "last_error": null,
  "created_at": "2026-08-19T12:00:00.000Z",
  "delivered_at": "2026-08-19T12:00:01.000Z"
}

The set of status values for a delivery row is defined by the delivery pipeline, not this route; it is passed through verbatim from storage.

Errors: 404 PERMISSION_RESOURCE_NOT_FOUND for an id that does not exist or is owned by another project, plus shared errors.

Update Webhook Subscription

Endpoint: PATCH /api/v1/webhooks/{id}

Auth: API key (X-API-Key), project-scoped, write permission.

Request: any non-empty subset of url, event_types, description (nullable), payload_mode, filters (nullable; null resets to {}), status. status accepts only active or paused.

Rules enforced by the route:

  • status and url cannot change in the same request: URL changes trigger re-verification, which controls the status (400 VALIDATION_BAD_REQUEST).
  • status can only be changed when the current status is active or paused. A pending_verification subscription must be verified via POST /verify; a disabled one must be re-created (400).
  • The URL of a disabled subscription cannot be changed; delete and re-create (400).
  • A changed URL is re-challenged immediately: success sets status: "active", resets failure_count, and stamps verified_at; failure sets status: "pending_verification".

Response: 200 with the updated subscription object.

Errors: 409 CONFLICT when the row was modified concurrently while this request was in flight (reload and retry), 400 VALIDATION_BAD_REQUEST for the rule violations above or an SSRF-blocked URL, 404 PERMISSION_RESOURCE_NOT_FOUND, 500 INTERNAL_SERVER_ERROR for a body failing schema validation (see limitation note), plus shared errors.

Delete Webhook Subscription

Endpoint: DELETE /api/v1/webhooks/{id}

Auth: API key (X-API-Key), project-scoped, write permission.

Response: 200 with { "deleted": true }. Stored delivery events are removed with the subscription.

Errors: 404 PERMISSION_RESOURCE_NOT_FOUND, plus shared errors.

Rotate Webhook Secret

Endpoint: POST /api/v1/webhooks/{id}/rotate-secret

Auth: API key (X-API-Key), project-scoped, write permission.

Response: 200 with { "secret": "whsec_...", "message": "..." }. The new raw secret is shown only in this response. Rotation is immediate: deliveries signed after this call use the new secret, so update your verifier before rotating if you cannot tolerate a gap.

Errors: 404 PERMISSION_RESOURCE_NOT_FOUND, plus shared errors.

Verify Webhook Subscription

Endpoint: POST /api/v1/webhooks/{id}/verify

Auth: API key (X-API-Key), project-scoped, write permission.

Re-sends the verification challenge for a subscription stuck in pending_verification.

Response: 200 either way:

  • Success: { "verified": true, "status": "active", "message": "Subscription verified and activated" }
  • Failure: { "verified": false, "status": "pending_verification", "message": "Verification failed. Ensure your endpoint responds with the challenge value in the JSON body." }

Errors: 400 VALIDATION_BAD_REQUEST when the subscription is not in pending_verification state or the URL is SSRF-blocked, 409 CONFLICT when the subscription was modified while the challenge was in flight, 404 PERMISSION_RESOURCE_NOT_FOUND, plus shared errors.

Send Test Event

Endpoint: POST /api/v1/webhooks/{id}/test

Auth: API key (X-API-Key), project-scoped, write permission.

Sends a signed webhook.test CloudEvent to the subscription URL (respecting its payload_mode), with the standard delivery headers, a 15 second timeout, and the SSRF checks applied.

Response: 200 with the delivery outcome; a failed delivery is still a 200 from this API:

{
  "success": true,
  "event_id": "evt_9f2c...",
  "response_status": 200,
  "response_time_ms": 143,
  "error": null
}

success mirrors whether your endpoint returned 2xx. On failure, error carries HTTP <status>: <statusText> or the transport error message, and response_status may be null.

Errors: 400 VALIDATION_BAD_REQUEST when the subscription is not active or the URL is SSRF-blocked, 404 PERMISSION_RESOURCE_NOT_FOUND, plus shared errors.

Bouncer (KYA-OS Governance) APIs

The Bouncer API powers Checkpoint's Govern features — delegations, proofs, OAuth, tool configuration, and consent.

Authorization

Endpoint: GET or POST /api/v1/bouncer/authorize

Auth: GET — none; it's the user-facing consent link the user clicks, with parameters in the query string. POST — API key (X-API-Key), read permission; same parameters in the body for server-side callers.

Initiates the delegation authorization flow for an AI agent. Both return an authorization_url (or consent_url for credential and consent-only providers) to send the user to.

Request: GET passes these as query parameters, POST as a JSON body. Required: agent_did and requested_scopes (1 to 20 scopes: comma-separated in the query string, an array in the body). Optional: agent_name, expires_in_days (1 to 365, default 7), session_id, worker_url, metadata, provider (explicit provider id), and tool_name (infer the provider from the tool's authorization requirement). A project is also required: from the API key on POST, or a project_id parameter (UUID or friendlyId) on the public GET.

Response: 200 with data discriminated by provider_type:

{
  "success": true,
  "data": {
    "provider_type": "oauth2",
    "authorization_url": "https://github.com/login/oauth/authorize?...",
    "expires_at": "2026-07-27T12:10:00.000Z",
    "project_id": "a1b2c3d4-...",
    "agent_did": "did:key:z6Mk...",
    "requested_scopes": ["cart:read"]
  }
}

provider_type: "oauth2" carries authorization_url. provider_type: "password" (credential providers) carries consent_url, provider_name, and a credential_config object for the login form. provider_type: "none" (consent-only) carries consent_url. Every variant includes expires_at (the URL expires in 10 minutes), project_id, agent_did, and requested_scopes.

Errors: 400 validation_error for a malformed request (missing agent_did / requested_scopes), 400 no_project when no project can be resolved, 400 not_configured when Bouncer is not configured for the project, 400 invalid_provider_config / invalid_credential_config for an incomplete provider setup (all lowercase Bouncer codes), 429 on the public GET, which is rate-limited before parsing.

Delegations

EndpointMethodAuth
/api/v1/bouncer/delegationsPOSTAPI key, write
/api/v1/bouncer/delegations/{delegationId}DELETEAPI key, write
/api/v1/bouncer/delegations/{delegationId}/tokensGETAuthorization: Bearer <delegation_token> (not an API key)
/api/v1/bouncer/delegations/verifyPOSTAPI key, read
/api/v1/bouncer/delegations/notifyPOSTAPI key, write
/api/v1/bouncer/delegations/status/{requestId}GETNone — public

There is no list endpoint here — browse delegations in the dashboard's Delegations page. Revocation is the DELETE route; notify only records delegations created out-of-band. See Managing Delegations for usage examples. Every endpoint except status polling is documented in full below.

Status polling returns { status: "pending" | "completed" | "expired" | "error" } with authUrl / delegationJwt / errorMessage per status; the current implementation reports pending for well-formed request ids while full status lookup is pending. A non-UUID request id returns 400 INVALID_REQUEST_ID.

Create Delegation

Endpoint: POST /api/v1/bouncer/delegations

Auth: API key (X-API-Key), write permission.

Creates a delegation and mints a delegation_token: a W3C VC-JWT signed with EdDSA (Ed25519) by Checkpoint's server DID, verifiable by KYA-OS servers without a database lookup.

Request:

{
  "agent_did": "did:key:z6Mk...",
  "agent_name": "Shopping Assistant",
  "user_did": "did:key:z6Mk...",
  "user_identifier": "user@example.com",
  "scopes": ["cart:read", "cart:write"],
  "expires_in_days": 7
}

agent_did and at least one scope are required. Optional: user_did (alias issuer_did), user_id, user_identifier, constraints (not_before, not_after, max_calls, allowed_origins, ip_whitelist), expires_in_days (1 to 365), metadata (KYA-OS custom_fields are merged into it), and credential_jwt.

Response: 201 with a Location header pointing at the delegation:

{
  "success": true,
  "data": {
    "delegation_id": "a1b2c3d4-...",
    "agent_did": "did:key:z6Mk...",
    "user_did": "did:key:z6Mk...",
    "scopes": ["cart:read", "cart:write"],
    "status": "active",
    "issued_at": "2026-07-27T12:00:00.000Z",
    "expires_at": "2026-08-03T12:00:00.000Z",
    "created_at": "2026-07-27T12:00:00.000Z",
    "delegation_token": "eyJhbGciOiJFZERTQSJ9...",
    "token_type": "Bearer",
    "token_format": "vc+jwt",
    "expires_in": 604800
  }
}

Additional fields may be present (user_identifier, authorization, provenance).

Errors: 400 validation_error for a body that fails the schema, 400 no_project when the API key has no associated project (lowercase Bouncer codes), 401 for a missing or invalid API key.

Revoke Delegation

Endpoint: DELETE /api/v1/bouncer/delegations/{delegationId}

Auth: API key (X-API-Key), write permission.

Request: optional JSON body { "reason": "..." } (max 500 characters); an empty body is accepted.

Response: 200:

{
  "success": true,
  "data": {
    "delegation_id": "a1b2c3d4-...",
    "status": "revoked",
    "revoked_at": "2026-07-27T12:00:00.000Z",
    "was_already_revoked": false
  }
}

Revocation is idempotent: revoking an already-revoked delegation succeeds with was_already_revoked: true. reason, agent_did, and user_did may also be present.

Errors: 400 validation_error / no_project (lowercase Bouncer codes), 404 for an unidentified delegationId.

Get Delegation Tokens

Endpoint: GET /api/v1/bouncer/delegations/{delegationId}/tokens

Auth: Authorization: Bearer <delegation_token>: the VC-JWT itself, not an API key. The token's sub claim must match {delegationId} and its aud claim names the project.

Request: no body or query parameters.

Response: 200 with the decrypted OAuth tokens stored on the delegation:

{
  "success": true,
  "data": {
    "oauth_access_token": "gho_...",
    "oauth_refresh_token": null,
    "oauth_expires_at": null,
    "oauth_expires_in": null,
    "oauth_token_type": "Bearer",
    "oauth_scope": "repo read:user"
  }
}

Responses are never cached (Cache-Control: no-store); tokens are decrypted on demand.

Errors: 401 invalid_token for a missing, malformed, mismatched, or expired delegation token (its own error code, distinct from the API-key AUTH_* codes used elsewhere on this page), 404 tokens_not_found when the delegation is inactive, revoked, expired, or holds no stored OAuth tokens (including stored tokens that have themselves expired), 500 decryption_error when the stored tokens cannot be decrypted.

Verify Delegation

Endpoint: POST /api/v1/bouncer/delegations/verify

Auth: API key (X-API-Key), read permission.

Request:

{
  "agent_did": "did:key:z6Mk...",
  "delegation_token": "eyJhbGciOiJFZERTQSJ9...",
  "scopes": ["cart:read"]
}

agent_did is required. Optional: user_did, credential_jwt, delegation_token (stateless verification), session_id (anchors grant read-back to a durable session link), scopes, timestamp, and client_info (ip_address and origin are filled in from the request when omitted).

Response: 200 for any well-formed request; the verdict is inside data:

{
  "success": true,
  "data": {
    "valid": true,
    "delegation_id": "a1b2c3d4-...",
    "credential": { "scopes": ["cart:read"] }
  }
}

When valid is false, data.error carries { code, message } instead of credential. credential is the delegation credential detail; additional fields may be present.

Errors: 400 validation_error / no_project (lowercase Bouncer codes), 401 for a missing or invalid API key. A failed verification is not an HTTP error: it comes back 200 with data.valid: false.

Notify Delegation

Endpoint: POST /api/v1/bouncer/delegations/notify

Auth: API key (X-API-Key), write permission.

Audit-trail upsert from KYA-OS after a direct-mode delegation. Idempotent: repeating the same delegation_id updates the existing record.

Request:

{
  "delegation_id": "a1b2c3d4-...",
  "agent_did": "did:key:z6Mk...",
  "user_did": "did:key:z6Mk...",
  "scopes": ["cart:read"],
  "provider": "github",
  "project_id": "mcp-i-ieu0ro",
  "created_at": "2026-07-27T12:00:00.000Z"
}

Required: delegation_id (UUID), agent_did, scopes (1 to 50), provider, project_id (UUID or friendlyId), created_at (ISO 8601). Optional: agent_name, user_did, user_id, user_identifier, expires_at, metadata. Scopes outside the legacy resource:action shape are preserved in metadata rather than rejected.

Response: 201 with { delegation_id, status: "recorded", message } for a new record, 200 with status: "merged" when an existing record was updated.

Errors: 400 validation_error, 404 project_not_found, 403 project_mismatch when the API key doesn't match the delegation's project (lowercase Bouncer codes).

OAuth

EndpointMethodAuthDescription
/api/v1/bouncer/oauth/callbackGETNone — the upstream provider's redirect target, validated by its state parameterUpstream-provider OAuth callback handler
/api/v1/bouncer/oauth/tokenPOSTNone — authenticates with the single-use authorization code it exchangesExchange an authorization code for a delegation token

See OAuth Integration for the full flow.

The callback is not a JSON API: it is the provider's browser redirect target. It validates code and a base64 JSON state parameter, then redirects: to a consent success URL on completion, or to /bouncer/consent/error?error=... on provider errors and downstream failures.

Errors: the callback returns 400 invalid_callback / invalid_state JSON only for a missing or undecodable code / state; every other callback failure redirects to the consent error page rather than returning JSON. The token endpoint's errors are listed on its block below.

OAuth Token Exchange

Endpoint: POST /api/v1/bouncer/oauth/token

Auth: none; authenticates with the single-use authorization code it exchanges. Rate-limited (strict tier) before the body is parsed.

Request: JSON or form-encoded (application/x-www-form-urlencoded); all four fields required:

{
  "grant_type": "authorization_code",
  "code": "auth-code-from-callback",
  "agent_did": "did:key:z6Mk...",
  "project_id": "a1b2c3d4-..."
}

Response: 200. Note: this endpoint returns the token object directly, without the { success, data } envelope used elsewhere on this page:

{
  "delegation_token": "eyJhbGciOiJFZERTQSJ9...",
  "token_type": "Bearer",
  "expires_in": 604800,
  "delegation_id": "a1b2c3d4-...",
  "scopes": ["cart:read"],
  "session_id": "sess_abc",
  "user_did": "did:key:z6Mk..."
}

Errors: 400 invalid_request for a missing field or unsupported content type, 400 unsupported_grant_type for anything but authorization_code, 400 invalid_grant for an invalid or expired code (codes are single-use: a replay fails), 403 invalid_grant when the code doesn't match the agent_did / project_id presented, 404 invalid_delegation when the underlying delegation is gone or inactive, 429 when rate-limited, 500 internal_error on an unexpected failure.

Identity

Endpoint: POST /api/v1/bouncer/identity/resolve

Auth: API key (X-API-Key), write permission.

Resolves an OAuth identity (provider + subject) to a persistent user DID. Called by KYA-OS servers during OAuth flows.

Request:

{
  "project_id": "mcp-i-ieu0ro",
  "oauth_result": {
    "provider": "google",
    "sub": "1234567890",
    "email": "user@example.com",
    "email_verified": true,
    "name": "Ada Lovelace"
  }
}

Exactly one of oauth_result or credential_result must be present (never both). credential_result carries provider, user_id, and optional email / name. project_id accepts a UUID or friendlyId and must match the API key's project.

Response: 201 when a new account was created, 200 for an existing one:

{
  "success": true,
  "data": {
    "user_did": "did:key:z6Mk...",
    "user_account_id": "b1c2d3e4-...",
    "is_new_account": false,
    "auto_linked": false
  }
}

Errors: 400 for a malformed body or an API key with no associated project, 404 project_not_found when project_id doesn't resolve, 403 project_mismatch when the API key isn't scoped to the resolved project. An identity that matches nothing is not an error: a new account (and DID) is created for it.

Proofs

Endpoint: POST /api/v1/bouncer/proofs

Auth: API key (X-API-Key), write permission.

Submit KYA-OS cryptographic proofs for verification — signatures are validated, nonces checked, and accepted proofs stored. See Proof Verification.

Request:

{
  "session_id": "sess_abc",
  "delegation_id": "a1b2c3d4-...",
  "proofs": [
    {
      "jws": "eyJhbGciOiJFZERTQSJ9..signature",
      "meta": {
        "did": "did:key:z6Mk...",
        "kid": "did:key:z6Mk...#key-1",
        "ts": 1690000000,
        "nonce": "unique-session-nonce",
        "audience": "https://kya.vouched.id",
        "sessionId": "sess_abc",
        "requestHash": "sha256:<64 hex>",
        "responseHash": "sha256:<64 hex>"
      }
    }
  ]
}

1 to 100 proofs per batch. Each proof is a detached JWS over the KYA-OS proof meta; meta.scopeId and meta.delegationRef are optional. delegation_id is optional and nullable. Optional top-level fields: correlation_id, and a context object (toolCalls, consentEvents, mcpServerUrl) for dashboard enrichment.

Response: 200 when at least one proof was accepted:

{
  "success": true,
  "data": {
    "accepted": 1,
    "rejected": 0,
    "outcomes": { "success": 1 }
  }
}

outcomes counts proofs per outcome (success, failed, blocked, error); a rejected batch also carries errors[] entries of { proof_index, error: { code, message } }. When every proof is rejected the endpoint returns 400 all_proofs_rejected with the counts in error.details.

Errors: 400 validation_error / no_proofs / batch_too_large / proof_validation_error / all_proofs_rejected for malformed or rejected submissions, 400 no_project when the API key has no associated project, 404 delegation_not_found / session_not_found when the referenced delegation or session can't be found (all lowercase Bouncer codes).

Sessions

Endpoint: GET or POST /api/v1/bouncer/sessions

Auth: API key (X-API-Key) — read permission for GET, write permission for POST.

POST registers a session during the KYA-OS handshake so the dashboard can show which MCP clients (Claude Desktop, Cursor, MCP Inspector) are connecting. GET looks up a session by session_id for diagnostics.

Request (POST):

{
  "session_id": "b64url-anchor-of-22-plus-chars",
  "agent_did": "did:key:z6Mk...",
  "project_id": "mcp-i-ieu0ro",
  "created_at": 1690000000000,
  "client_info": { "name": "Claude Desktop", "version": "1.0.0" },
  "ttl_minutes": 30
}

session_id, agent_did, project_id (UUID or friendlyId), created_at (Unix ms), and client_info.name are required; client_info also accepts version, protocol_version, platform, vendor. Optional: client_identity (did, source, registered), server_did, ttl_minutes (default 30, max 1440), and a challenge object used by enforcement PEPs to anchor a step-up delegation challenge (when challenge is present, session_id must be at least 22 characters).

Response (POST): 201 for a new session, 200 for an idempotent repeat:

{
  "success": true,
  "data": {
    "session_id": "b64url-anchor-of-22-plus-chars",
    "registered": true,
    "created_at": "2026-07-27T12:00:00.000Z"
  }
}

When the request carried challenge, data.challenge returns the server-issued consent_uri, the org-signed challenge_proof, required_scopes, and issued_at.

Request (GET): query parameter session_id. The response is { success, data: { session } } with the stored record (id, projectId, agentDid, clientName, clientVersion, clientDid, registeredAt, createdAt, lastSeenAt). Without session_id the endpoint returns a readiness message only.

Errors: 401 for a missing/invalid API key, 403 when the API key isn't scoped to the session's project, 404 for an unidentified session_id (codes are lowercase: unauthorized, forbidden, project_not_found, not_found), 503 authorization_host_unavailable when the project's configured consent host can't serve the challenge registration.

Project Configuration

EndpointMethodAuthDescription
/api/v1/bouncer/configGETAPI keyDeprecated legacy global config — use the project-scoped endpoint
/api/v1/bouncer/projects/{projectId}/configGET, PUTAPI keyGet or update project tool config
/api/v1/bouncer/projects/{projectId}/config/validatePOSTAPI keyValidate project config
/api/v1/bouncer/projects/{projectId}/consent-configGETAPI keyRead consent screen config (edit in the dashboard)
/api/v1/bouncer/projects/{projectId}/delegationsPOSTNone — browser-based consent flowCreate project-scoped delegations
/api/v1/bouncer/projects/{projectId}/providersGETAPI keyList OAuth and credential provider configuration

See Tool Protection and Consent Flows for usage. In the dashboard, per-tool protection and consent settings are managed at Policy → Auth; tool discovery/removal and per-tool scope display remain on the legacy Control Access surface. Every endpoint except the legacy global config is documented in full below.

The legacy global config route is deprecated and functional: it requires an agent_did query parameter, resolves the project from the API key, and responds with X-Deprecated: true and a Sunset header. Use the project-scoped endpoint instead. Its errors: 400 missing_agent_did / no_project, 404 config_not_found, 403 agent_denied (lowercase Bouncer codes).

Get or Update Project Config

Endpoint: GET or PUT /api/v1/bouncer/projects/{projectId}/config

Auth: API key (X-API-Key, or Authorization: Bearer) with access to this project. {projectId} accepts a UUID or friendlyId.

Request: GET: no body or query parameters. PUT: { "config": { ... } } with a partial KYA-OS server configuration (a bare config object without the config wrapper is also accepted); the service validates and merges it.

Response: GET returns 200 with { success, data: { config } }: the merged configuration with tool protections embedded at config.toolProtection.tools (the single source of truth a KYA-OS MCP server reads; no separate tool-protections call needed). Sensitive fields are masked. Cached for 60 seconds. PUT returns { success, data: { config, changes } } with the masked updated config and the list of changes.

Errors: 404 PROJECT_NOT_FOUND / MCPI_CONFIG_NOT_FOUND, 400 CONFIG_VALIDATION_ERROR (and on PUT: 400 INVALID_REQUEST_BODY / VALIDATION_ERROR), 403 PERMISSION_FORBIDDEN when the key isn't scoped to the project, 500 PROJECT_LOOKUP_FAILED / CONFIG_FETCH_FAILED / CONFIG_UPDATE_FAILED on downstream failures.

Validate Project Config

Endpoint: POST /api/v1/bouncer/projects/{projectId}/config/validate

Auth: API key (X-API-Key) with access to this project.

Request: { "config": { ... } }, the same partial-config shape as the PUT above. Nothing is written: this is a dry-run check.

Response: 200 with { success, data: { valid, errors, warnings } }.

Errors: 404 PROJECT_NOT_FOUND / MCPI_CONFIG_NOT_FOUND, 400 INVALID_REQUEST_BODY / VALIDATION_ERROR, 403 PERMISSION_FORBIDDEN when the key isn't scoped to the project.

Endpoint: GET /api/v1/bouncer/projects/{projectId}/consent-config

Auth: API key (X-API-Key) with access to this project.

Request: no body or query parameters.

Response: 200 with { success, data: { config, metadata } } where config is the consent page configuration (branding, terms, UI settings, custom fields; edit it in the dashboard) and metadata carries projectId, friendlyId, version, updatedAt, cacheVersion. The X-Consent-Version header repeats the cache version; cached for 60 seconds.

Errors: 404 PROJECT_NOT_FOUND / CONSENT_CONFIG_NOT_FOUND, 403 when the key isn't scoped to the project.

Endpoint: POST /api/v1/bouncer/projects/{projectId}/delegations

Auth: none; part of the browser-based consent flow, where no API key is available. The project must exist.

Same request and response contract as Create Delegation, with three differences: the project comes from the path ({projectId}, UUID or friendlyId), no API key is required, and the response data also carries id (an alias of delegation_id).

Errors: 404 project_not_found when {projectId} doesn't resolve, 400 validation_error for a body that fails the schema (lowercase Bouncer codes).

List Providers

Endpoint: GET /api/v1/bouncer/projects/{projectId}/providers

Auth: API key (X-API-Key) with access to this project.

Request: no body or query parameters. Supports conditional requests: send If-None-Match with the previous ETag to get 304 Not Modified when the configuration is unchanged.

Response: 200 with a signed provider-configuration document:

{
  "success": true,
  "data": {
    "version": "1.0",
    "source": {
      "projectId": "a1b2c3d4-...",
      "projectFriendlyId": "mcp-i-ieu0ro",
      "generatedAt": "2026-07-27T12:00:00.000Z",
      "agentShieldVersion": "x.y.z"
    },
    "providers": { "github": { "id": "github" } },
    "signature": "hmac-hex",
    "signedFields": ["version", "providers"],
    "configuredProvider": "github"
  }
}

providers maps provider id to its public configuration (built-in OAuth providers plus any configured custom and credential providers); per-provider fields vary and additional fields may be present. The HMAC signature covers signedFields for tamper detection. configuredProvider is the project's default provider, or the first configured one. Cached for 60 seconds with stale-while-revalidate.

Errors: 404 PROJECT_NOT_FOUND, 403 when the key isn't scoped to the project.

KYA-OS Audit Events

Ingest Audit Events

Batch ingest for the KYA-OS Audit Events format (SPEC-EVENTS v0.1). Customers running the kya-os protocol point their emitter at this one endpoint (KYA_OS_EVENTS_URL + KYA_OS_EVENTS_TOKEN) and Checkpoint fans the events out server-side into its existing surfaces, replacing hand-integration of the four legacy bouncer ingest endpoints (delegations/notify, sessions, proofs, log-detection), which remain for mcp-i compatibility.

Endpoint: POST /api/v1/kya-os/events (plus OPTIONS for CORS preflight)

Auth: API key (X-API-Key), write permission, project-scoped. Events land only in the API key's own project; the body cannot target another project.

Request:

{
  "events": [
    {
      "type": "tool.invoked",
      "tool": { "name": "search_products" },
      "verdict": "pass"
    }
  ]
}

events must contain 1 to 100 elements. Each event is one of the seven SPEC-EVENTS v0.1 types, discriminated on type: delegation.observed, delegation.minted, delegation.revoked, session.started, agent.verified, tool.invoked, proof.verified. Field names are camelCase on the wire (matching @kya-os/mcp protocol types, not the snake_case of the legacy bouncer endpoints). Validation is deliberately lenient per event: a malformed or unrecognized-type event is skipped and counted, never failing the batch. proof.verified JWSes are re-verified server-side as defense in depth.

Response: 202 Accepted:

{
  "accepted": 42,
  "skipped": 1,
  "skipped_details": [{ "index": 7, "type": "tool.invoked", "reason": "..." }]
}

accepted/skipped sit at the top level (no success envelope); emitters across repos assert on this contract shape. skipped_details entries carry the batch index, the event type when one was readable off the raw payload, and a reason; the list is capped for response size. Events are processed before the response is sent, so the counts are real, and the response carries an X-Request-ID header.

Errors: 400 invalid_json for a non-JSON body, 400 validation_error when the envelope is not { events: [...] } with 1 to 100 elements (these two use lowercase codes, unlike the API-key AUTH_* codes), 401 AUTH_MISSING_CREDENTIALS / AUTH_INVALID_API_KEY / AUTH_EXPIRED_API_KEY, 403 PERMISSION_INSUFFICIENT when the key lacks write permission. Per-event problems never produce a 4xx; they surface in skipped_details.

Submit Audit Ledger Entry

Authoritative KYA-OS Auditability Protocol recorder ingest. Intentionally separate from /api/v1/kya-os/events (which is a compatibility fan-out into operational tables): a successful response here contains the exact signed ledger entry and recorder receipt committed by the @kya-os/mcp v1.11.0 recorder contract.

Endpoint: POST /api/v1/audit/entries (plus OPTIONS for CORS preflight)

Auth: Source-bound audit ingest credential, presented as an x-kya-audit-key header or Authorization: Bearer <credential>. This is the recorder credential provisioned for a ledger source, not a Checkpoint API key; the credential's project must match both the submission's source and ledger.

Request: an audit recorder submission envelope:

{
  "ledgerId": "...",
  "expectedLedgerEpochId": "...",
  "producerEvent": { "...": "signed producer event" },
  "encryptedEvidence": [{ "ref": { "...": "evidence reference" }, "ciphertextBase64url": "..." }]
}

expectedLedgerEpochId is optional. Evidence ciphertext must be canonical unpadded base64url, and the total request is capped at 6 MiB (enforced even for chunked bodies without a Content-Length). The full envelope and producer-event schemas live in the @kya-os/mcp audit contract and are not itemized field-by-field here.

Response: 200:

{
  "success": true,
  "data": {
    "schema": "https://schema.kya-os.org/v1/protocol/audit/ingest-response/v1.0.0",
    "entry": { "...": "committed signed ledger entry" },
    "receipt": { "...": "the entry's recorder receipt" },
    "verification": { "...": "verification facts for the committed entry" }
  },
  "requestId": "req_abc123"
}

receipt duplicates entry.recorderReceipt for convenience. Responses are Cache-Control: private, no-store with an X-Request-ID header.

Errors: the body is { success: false, error: { code, message, details? }, requestId }.

StatusCodeWhen
401AUDIT_CREDENTIAL_REQUIRED / AUDIT_INVALID_CREDENTIALMissing or invalid ingest credential
413AUDIT_REQUEST_TOO_LARGERequest exceeds 6 MiB
400AUDIT_INVALID_JSON / AUDIT_INVALID_SUBMISSIONNon-JSON body, or a submission failing envelope validation
429AUDIT_RATE_LIMITEDPer-credential rate limit (carries Retry-After and X-RateLimit-* headers)
403UNAUTHORIZED_SUBMISSIONProtocol-level authorization failure
409LEDGER_MISMATCH / EPOCH_MISMATCH / EVENT_ID_CONFLICTLedger-chain conflicts
422INVALID_EVENT / EVIDENCE_FAILURE / EVIDENCE_INTEGRITYEvent or evidence rejected by the recorder
503INVALID_CONFIGURATION / JOURNAL_FAILURE / APPEND_CONFLICT_EXHAUSTEDRecorder temporarily unavailable (message is genericized)
500AUDIT_INTERNAL_ERRORAny other failure

Gateway APIs

The Gateway API family backs Checkpoint's MCP gateway: the OAuth 2.1 authorization server that MCP clients (Cursor, Claude Desktop, VS Code, ChatGPT) authenticate against, plus the machine endpoints the gateway itself calls. None of these endpoints take an X-API-Key: each one authenticates by its flow, the same pattern as the /bouncer/oauth/* exceptions in Authentication.

EndpointMethodAuth
/api/v1/gateway/oauth/registerPOSTNone, public (rate-limited): RFC 7591 dynamic client registration
/api/v1/gateway/oauth/authorizeGETBrowser flow: requires a signed-in dashboard session to proceed
/api/v1/gateway/oauth/authorize/{orgSlug}GETBrowser flow: dashboard session, or merchant credential mode per org config
/api/v1/gateway/oauth/tokenPOSTNone: authenticates with the code + PKCE verifier, or the refresh token
/api/v1/gateway/oauth/needs-authorizationGETNone, public discovery
/api/v1/gateway/oauth/status-listGETNone, public revocation list
/api/v1/gateway/connections/{provider}/callbackGETNone: the upstream provider's redirect target, validated by its state token
/api/v1/gateway/vault/{projectId}/resolvePOSTEd25519 gateway assertion JWT in the body (no API key)

The OAuth surface is rate-limited per client IP. A rate-limited request returns 429 with { "error": "rate_limit_exceeded", "error_description": "Too many requests. Please try again later." } and X-RateLimit-Limit / X-RateLimit-Remaining (plus X-RateLimit-Reset when available). The connections callback is the one exception: it responds to a rate-limited browser with its redirect contract (connection_error=rate_limited) instead of a raw JSON 429.

Register OAuth Client

Dynamic client registration per RFC 7591. MCP clients that cannot be pre-registered POST their own metadata and receive a generated client_id for the subsequent authorize + token flow. Registrations are effectively ephemeral: the token endpoint authenticates by PKCE and round-trip checks, not a client registry lookup, so the client_id is a correlation identifier. The registration row is persisted best-effort for attribution only.

Endpoint: POST /api/v1/gateway/oauth/register

Auth: None, public. Rate-limited per client IP.

Request:

JSON body, validated in the handler (no shared Zod schema):

FieldRequiredConstraints
redirect_urisYesNon-empty array; each entry must be HTTPS, loopback HTTP, or a native-app private-use URI scheme (RFC 8252)
token_endpoint_auth_methodNoMust be "none" when present (public clients only); defaults to "none"
grant_typesNoArray; allowed values authorization_code, refresh_token; defaults to both
response_typesNoArray; only code is allowed; defaults to ["code"]
client_nameNoString, at most 200 characters
scopeNoString; defaults to "mcp"
software_idNoString, at most 200 characters; silently dropped otherwise
software_versionNoString, at most 100 characters; silently dropped otherwise

Response: 201 with Cache-Control: no-store:

{
  "client_id": "d3b1a6a0-1234-4c1e-9f6a-abcdef012345",
  "client_id_issued_at": 1753617600,
  "client_name": "Cursor",
  "redirect_uris": ["cursor://anysphere.cursor-mcp/oauth/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "response_types": ["code"],
  "token_endpoint_auth_method": "none",
  "scope": "mcp"
}

software_id and software_version are echoed back when they were accepted.

Errors: 400 invalid_redirect_uri for a missing, empty, or disallowed redirect_uris entry; 400 invalid_client_metadata for a non-JSON body, a token_endpoint_auth_method other than none, an unsupported grant or response type, or an over-long client_name; 429 rate_limit_exceeded; 500 server_error if the generated client_id collides with the CIMD client-id shape (a defensive guard, not expected in practice). Errors use the RFC 6749 shape { error, error_description }.

Authorize (Global)

OAuth 2.1 authorization endpoint. Validates the params, gates behind a signed-in dashboard session, and redirects the browser to the consent screen at /oauth/gateway/consent, which mints the authorization code. The target org is derived from the signed-in user's current organization; multi-org users should use the per-org variant instead.

Endpoint: GET /api/v1/gateway/oauth/authorize

Auth: None to reach; an unauthenticated browser is redirected to /signin with a callbackUrl back to this endpoint. Rate-limited per client IP.

Request:

Query parameters:

ParameterRequiredDescription
response_typeYesMust be code
client_idYesFrom dynamic client registration, or a URL-shaped CIMD client id
redirect_uriYesHTTPS, loopback HTTP, or native-app private-use URI scheme (RFC 8252)
stateYesCSRF protection; clients that omit it get an error redirect
code_challengeYesPKCE is mandatory
code_challenge_methodYesMust be S256; plain PKCE is rejected
scopeNoSpace-delimited list of scope identifiers, at most 16; defaults to mcp

Response: a redirect. Signed-in and valid: redirect to the consent screen. Not signed in: redirect to /signin. Per RFC 6749 §4.1.2.1, most validation failures redirect to redirect_uri with error, error_description, and state query parameters.

Errors: direct 400 JSON ({ error, error_description }) when there is no safe redirect target: missing or disallowed redirect_uri, missing response_type, or a URL-shaped client_id whose CIMD metadata document fails to resolve or does not match redirect_uri. Error redirects carry invalid_request (missing client_id / state / code_challenge, or a code_challenge_method other than S256), unsupported_response_type, or invalid_scope. 429 rate_limit_exceeded as JSON.

Authorize (Per-Org)

Per-org variant of the authorization endpoint: the target org rides on the URL path instead of the user's session, so an MCP client always gets a token for the org it asked for. This is the preferred authorize endpoint.

Endpoint: GET /api/v1/gateway/oauth/authorize/{orgSlug}

Auth: Depends on the org's gateway consent config. Default mode requires a signed-in dashboard session plus membership in {orgSlug}. Orgs configured for merchant credential mode skip both gates: the consent page handles merchant sign-in via the org's configured credential provider. Rate-limited per client IP.

Request: same query parameters as the global authorize endpoint.

Response: a redirect to the consent screen with the same parameters plus org={orgSlug}, and identity_mode=merchant_credential on the merchant path. Unauthenticated browsers on the session path are redirected to /signin.

Errors: the same validation errors as the global endpoint, plus an access_denied error redirect when the org slug does not resolve (or the org is deleted), and when the signed-in user is not a member of the org. 429 rate_limit_exceeded as JSON.

Token

OAuth 2.1 token endpoint. Exchanges an authorization code for a KYA-OS Delegation VC-JWT access token plus a refresh token, or rotates a refresh token. Access tokens are signed with the issuing org's own key and are only minted while the underlying delegation is active.

Endpoint: POST /api/v1/gateway/oauth/token

Auth: None (public clients). The request authenticates with the single-use authorization code plus the PKCE code_verifier, or with the refresh token. Rate-limited per client IP.

Request:

Body must be application/x-www-form-urlencoded.

grant_type=authorization_code:

FieldRequiredConstraints
codeYesSingle-use; claimed atomically
redirect_uriYesMust match the original authorization request
client_idYesMust match the original authorization request
code_verifierYes43 to 128 characters (RFC 7636); S256 only

grant_type=refresh_token:

FieldRequiredConstraints
refresh_tokenYesRotated on every use; a replay revokes the chain
client_idYesMust match the token's bound client

Response: 200 with Cache-Control: no-store:

{
  "access_token": "eyJhbGciOiJFZERTQSIs...",
  "token_type": "Bearer",
  "expires_in": 900,
  "refresh_token": "N2Y4...base64url...",
  "scope": "mcp"
}

access_token is a W3C Verifiable Credential in JWT form and expires after 15 minutes. Every grant returns a fresh refresh_token (30-day expiry); the previous refresh token is invalidated, and replaying an already-used refresh token revokes every outstanding token on its delegation.

Errors: 400 invalid_request for a non-form body, a missing required parameter, or a code_verifier outside 43 to 128 characters; 400 unsupported_grant_type for any grant_type other than the two above; 400 invalid_grant for a code that is invalid, expired, or already used, a client_id or redirect_uri mismatch, a failed PKCE check, a refresh token that is invalid, expired, or revoked, or a delegation that is no longer active; 429 rate_limit_exceeded; 500 server_error when the delegation row is missing, the issuing org is missing, deleted, or has an invalid slug, or minting fails (the one-time code or refresh-token claim is rolled back so the client can retry). Errors use the RFC 6749 shape { error, error_description }.

Needs-Authorization Challenge

Public discovery endpoint serving the org-signed needs_authorization consent challenge that the RFC 9728 resource metadata's kyaos_needs_authorization extension points at. A KYA-OS-aware client fetches the challenge here and verifies the proof against the org's did:web.

Endpoint: GET /api/v1/gateway/oauth/needs-authorization

Auth: None, public. The accountable operator is resolved server-side from the project owner and is deliberately not accepted from the request.

Request:

Query parameters:

ParameterRequiredDescription
project_idYesProject UUID
scopeYesSpace- or comma-separated scope list; at least one scope after de-duping
agent_didNoMust be a did:web identifier when present; becomes the credential subject
agent_nameNoDisplay name threaded into the challenge

Response: 200 with Cache-Control: no-store and a body of { challenge, content, proof }, where content is exactly the material the challenge's response hash binds. The inner shapes come from the producer service and are not itemized here.

Errors: 400 invalid_request for a missing project_id, an empty scope list, or a malformed agent_did; 404 unknown_resource when no org resolves for the project; 500 server_error on a producer failure. Errors use the RFC 6749 shape { error, error_description }.

Revocation Status List

Serves a signed W3C StatusList2021 Verifiable Credential: a gzip-compressed 131,072-bit bitstring where each bit is one delegation's revocation status (0 = valid, 1 = revoked). The gateway's delegation verifier fetches this list and reads the bit position named by each VC-JWT's credentialStatus.statusListIndex.

Endpoint: GET /api/v1/gateway/oauth/status-list

Auth: None, public.

Response: 200 with Cache-Control: public, max-age=30:

{
  "@context": ["https://www.w3.org/2018/credentials/v1"],
  "type": ["VerifiableCredential", "StatusList2021Credential"],
  "credentialSubject": {
    "type": "StatusList2021",
    "statusPurpose": "revocation",
    "encodedList": "H4sIAAAA..."
  },
  "proof": {
    "type": "JwtProof2020",
    "jwt": "eyJhbGciOiJFZERTQSIs..."
  }
}

This route's envelope carries no top-level issuer field; its consumers pin that shape. The signed VC-JWT inside proof.jwt carries the full credential including the issuer.

Errors: 500 signing_key_unavailable when the server's signing key is not configured (the document is never served unsigned).

Connection Callback

The upstream OAuth provider's redirect target for the dashboard's "Connect" flow (Checkpoint acting as an OAuth client to providers such as GitHub). Not an endpoint integrations call directly: the provider redirects the user's browser here after the consent step. Validates the single-use CSRF state token, exchanges the code server-to-server, rejects scope sprawl, stores the encrypted tokens, and sends the browser back where it started.

Endpoint: GET /api/v1/gateway/connections/{provider}/callback

Auth: None: validated by the single-use state token minted at authorize time. Rate-limited per client IP.

Request: the provider supplies code and state query parameters on success, or error (with state) when the user denies consent.

Response: always a 307 redirect. On success the original page is reopened with ?connected={provider}. On failure the redirect carries ?connection_error={reason}&connection_provider={provider} and, when the provider returned its own error string, connection_provider_error. When the state cannot be resolved at all, the fallback target is /dashboard with connection_error only. The stable reason codes:

ReasonMeaning
provider_deniedUser cancelled on the provider's consent screen
state_invalidMissing, expired, replayed, or mismatched state token
unknown_provider{provider} path segment is not a registered provider
token_exchange_failedProvider returned non-2xx on the token exchange
token_parse_failedProvider's token response did not match the expected shape
scope_sprawlProvider granted more scopes than Checkpoint requested
rate_limitedToo many callback requests from this IP
internal_errorUnexpected failure

Errors: none as HTTP error statuses; every failure is reported through the redirect contract above.

Vault Resolve (Gateway)

Decrypts a user's per-upstream secrets for a gateway tool call. Called by Checkpoint's own gateway, not by customer code: the trust anchor is the project's stored gateway public key and trusted gateway DID, set during upstream provisioning. Secrets resolve from the user's OAuth connections first (refreshing near-expiry tokens inline), then fall back to pasted secrets. Distinct from /api/v1/vault/{projectId}/resolve (Resolve Secrets), which is a different route with API-key auth.

Endpoint: POST /api/v1/gateway/vault/{projectId}/resolve

Auth: Ed25519 assertion JWT in the body (assertion field), not an API key. The JWT must be EdDSA-signed by the project's registered gateway key, its iss must equal the project's trusted gateway DID, its projectId claim must match the route, and its userDid, toolName, and sessionId claims must match the body copies.

Request:

All fields required (Zod-validated):

{
  "assertion": "eyJhbGciOiJFZERTQSIs...",
  "envVarNames": ["GITHUB_TOKEN"],
  "userDid": "did:web:example.com:users:abc",
  "toolName": "create_issue",
  "sessionId": "sess_123"
}

envVarNames must contain at least one name.

Response: 200 with Cache-Control: no-store:

{
  "success": true,
  "data": {
    "secrets": {
      "GITHUB_TOKEN": "gho_..."
    },
    "_meta": {
      "orgSlug": "acme",
      "upstreamSlug": "github"
    }
  }
}

A user with no matching secrets gets 200 with an empty secrets map, not a 404. Names that fail to resolve are omitted from secrets. The response is validated against the shared vaultResolveSuccessSchema wire contract.

Errors: every error path uses the single envelope { success: false, error: { code, message, data? } }:

StatusCodeWhen
400MALFORMED_REQUEST_BODYNon-JSON body or failed schema validation
400INVALID_ASSERTIONMalformed JWT, wrong alg/typ, or missing claims
401INVALID_SIGNATURESignature verification failed
403ASSERTION_EXPIREDexp/iat outside the allowed windows
403PROJECT_MISMATCH / ISSUER_MISMATCH / USER_DID_MISMATCH / TOOL_NAME_MISMATCH / SESSION_ID_MISMATCHA claim does not match the route or body
403NOT_AUTHORIZEDResolved user is not a member of the project's org
403OAUTH_EXPIREDA connection's token expired and cannot refresh; user must reconnect
404NOT_FOUNDProject missing or deleted
404CREDENTIALS_NOT_CONFIGUREDUser has no credentials; error.data.actionUrl deep-links to the dashboard connect page
500GATEWAY_NOT_CONFIGUREDProject has no gateway public key or trusted DID

Genuinely unexpected failures surface as 500 INTERNAL_SERVER_ERROR via the shared error handler.

Vault

Runtime secret resolution for KYA-OS workers. Per-user secrets are stored encrypted and resolved at call time; this endpoint is called by the deployed worker, not by hand-written customer integrations.

Resolve Secrets

Resolves per-user encrypted secrets (environment variable values) for a runtime tool call. Auth is dual-factor: the project API key plus a signed assertion that binds the request to a user, tool, and session. Distinct from the gateway-side Vault Resolve (Gateway), which authenticates with a gateway assertion only.

Endpoint: POST /api/v1/vault/{projectId}/resolve

Auth: API key (X-API-Key) with access to this project, plus an Ed25519 assertion JWT in the body. The assertion is a compact EdDSA JWT signed by the project's agent deployment key; it must carry projectId, userDid, toolName, sessionId, exp, and nonce claims, be unexpired, and its projectId claim must match the route.

Request:

{
  "assertion": "eyJhbGciOiJFZERTQSIsInR5cCI6IkpXVCJ9.eyJwcm9qZWN0SWQiOiIuLi4ifQ.c2ln",
  "envVarNames": ["MY_API_KEY", "OTHER_SECRET"]
}

envVarNames must contain at least one name.

Response:

{
  "success": true,
  "secrets": {
    "MY_API_KEY": "decrypted-plaintext-value"
  }
}

Served with Cache-Control: no-store. A 200 with an empty secrets object means nothing matched: requested names with no stored secret are omitted rather than erroring, as are secrets that fail to decrypt. Every resolve is audit-logged.

Current scoping behavior (this matches the code and is flagged there as an in-progress area): secrets are looked up for the API key owner's user account. The assertion's userDid is required and audit-logged but is not yet mapped to a user for the lookup, and envVarNames is not yet filtered against a per-tool allowlist.

Errors: 401 AUTH_MISSING_CREDENTIALS / AUTH_INVALID_API_KEY / AUTH_EXPIRED_API_KEY for API-key failures, 404 NOT_FOUND when the project doesn't exist, 403 FORBIDDEN when the API key isn't scoped to this project, 400 INVALID_JSON for an unparsable body, 400 VALIDATION_INVALID_REQUEST when the body fails schema validation, 404 NO_AGENT_KEY when the project has no agent deployment public key to verify against, 400 INVALID_ASSERTION for a structurally invalid assertion (wrong algorithm, missing claims, missing nonce), 403 INVALID_SIGNATURE on a failed signature check, 403 ASSERTION_EXPIRED for an expired assertion, 403 PROJECT_MISMATCH when the assertion's projectId doesn't match the route.

Molti (Managed Agent Runtime) APIs

These endpoints are the wire contract between Checkpoint and a deployed agent's @kya-os/compute process. They are machine-to-machine: the caller is the agent container, not a customer integration.

Two credentials appear here:

  • Heartbeat token (X-Heartbeat-Token header): a per-deployment secret issued at deploy time. Used by managed deployments.
  • API key (X-API-Key header, or Authorization: Bearer): the standard project API key. Used by BYOK deployments on the config-bundle endpoint.

Error responses use the base envelope ({ success: false, error: { code, message } } with a metadata block that has no version field).

Agent Heartbeat

Receives a liveness heartbeat from the compute process, updates the agent's status, and returns any pending commands (stop, config refresh).

Endpoint: POST /api/v1/molti/agents/{agentDid}/heartbeat

Auth: X-Heartbeat-Token header: the per-deployment heartbeat token (not an API key). Compared timing-safe against the stored token. A missing token, an invalid token, and an unrecognized agentDid all return the same 401 so deployment existence cannot be probed. Rate limiting is applied after authentication, keyed by agent DID.

Request:

The agentDid path segment is the URL-encoded agent DID. Body (HeartbeatRequestSchema):

{
  "agentDid": "did:key:z6Mk...",
  "status": "running",
  "computeVersion": "1.7.7",
  "openclawVersion": "2026.8.1",
  "machineId": "d891234f5e6789",
  "uptimeSeconds": 3600,
  "memoryMb": 256
}

Required fields: agentDid (must match the URL parameter, otherwise 400), status (starting | running | error | stopping), computeVersion. Optional: openclawVersion, machineId, uptimeSeconds, memoryMb.

Response:

200 with the raw heartbeat object (this endpoint does not use the { success, data } envelope):

{
  "ack": true,
  "commands": [{ "type": "update_config" }],
  "configUpdatedAt": "2026-08-19T12:00:00.000Z"
}

commands this handler actually emits: { "type": "stop" } when the deployment has been killed, and { "type": "update_config" } when the deployment's config changed since the container last fetched its bundle. The shared ComputeCommandSchema also declares restart and update_version command types, but this handler never emits either: treat them as reserved.

Errors: 401 AUTH_UNAUTHORIZED for a missing/invalid heartbeat token or unrecognized agentDid, 429 RATE_LIMIT_EXCEEDED, 400 VALIDATION_BAD_REQUEST for invalid JSON, a body that fails schema validation, or a body agentDid that does not match the URL parameter, 503 SERVICE_UNAVAILABLE_TRANSIENT (with Retry-After: 2) when the database is cold-starting: back off and retry.

Get Config Bundle

Returns the agent's full runtime configuration. Fetched by the compute process on boot and again whenever a heartbeat returns an update_config command. The response contains live secrets (channel tokens, gateway token, AI provider key) and is served with Cache-Control: no-store.

Endpoint: GET /api/v1/molti/deployments/{agentDid}/config-bundle

Auth: Dual, by hosting mode. Managed deployments: X-Heartbeat-Token header (per-deployment token, timing-safe compare). BYOK deployments: API key (X-API-Key or Authorization: Bearer) whose project matches the deployment's project. An unrecognized agentDid returns 401, not 404, to prevent DID enumeration.

Request: no body. The agentDid path segment is the URL-encoded agent DID.

Response:

200 with { success: true, data: bundle }. Managed deployments get a version 2 bundle:

{
  "success": true,
  "data": {
    "version": 2,
    "killed": false,
    "openclawVersion": "2026.8.1",
    "soulFile": "...",
    "gatewayToken": "...",
    "channels": {
      "telegram": { "enabled": true, "token": "..." },
      "discord": { "enabled": false },
      "slack": { "enabled": true, "token": "...", "appToken": "..." },
      "whatsapp": { "enabled": true }
    },
    "aiProvider": { "type": "anthropic", "apiKey": "..." },
    "updatedAt": "2026-08-19T12:00:00.000Z",
    "meta": {
      "lastTouchedVersion": "2026.8.1",
      "lastTouchedAt": "2026-08-19T12:00:00.000Z"
    },
    "configHash": "sha256-hex..."
  }
}

Notes on the managed bundle:

  • aiProvider.type is anthropic or openai. For managed-LLM deployments, aiProvider also carries baseUrl (the Checkpoint LLM proxy) and its apiKey is the gateway token.
  • heartbeatEvery and modelOverride (tier-driven overrides) are included only when the deployment's OpenClaw version supports them; older runtimes get the bundle without those fields.
  • channels entries are omitted (undefined) for channels that were never configured; whatsapp carries only enabled.
  • Fetching the bundle records config acknowledgment (lastConfigAckAt), which is what stops the heartbeat from re-sending update_config.

BYOK deployments get a version 1 bundle: { version: 1, killed, soulFile, channels, updatedAt } only.

The agent's private key is never included in either bundle; it is delivered as an environment variable at machine creation.

Errors: 401 AUTH_MISSING_CREDENTIALS when no credential is presented at all (or a BYOK deployment presents no API key), 401 AUTH_UNAUTHORIZED for an unrecognized agentDid, an invalid heartbeat token, or an API key that does not belong to the deployment's project, 500 INTERNAL_SERVER_ERROR when no OpenClaw version is configured for a managed deployment, 500 DECRYPTION_FAILED when the stored gateway token cannot be decrypted.

OpenClaw Integration APIs

Endpoints that let an OpenClaw runtime use Checkpoint as its tool-policy provider and approval backend. config and approve are API-key endpoints; the consent endpoint is part of the browser-based consent flow and authenticates by consent token instead, by design.

The config and approve endpoints use the shared v1 error envelope ({ success: false, error: { code, message, details? }, metadata: { requestId, timestamp, version: "v1" } }, plus X-Request-ID and X-API-Version: v1 headers); the consent endpoint uses the base envelope without the version field.

Get OpenClaw Tool Policies

Returns the project's tool protections translated into OpenClaw's native tools.policyProvider format.

Endpoint: GET /api/v1/openclaw/config

Auth: API key (X-API-Key, or Authorization: Bearer) with access to the requested project.

Request:

Query ParameterRequiredDescription
projectIdYesCheckpoint project ID

Response:

200 with { success: true, data } (no top-level metadata on this endpoint; the metadata block lives inside data). Served with Cache-Control: public, max-age=60.

{
  "success": true,
  "data": {
    "tools": {
      "policies": {
        "gmail_send_email": {
          "approval": "always",
          "riskLevel": "high",
          "authorization": {
            "type": "oauth2",
            "provider": "google",
            "requiredScopes": ["email:send"]
          }
        },
        "read_file": {
          "approval": "off",
          "riskLevel": "low"
        }
      }
    },
    "metadata": {
      "refreshInterval": 60,
      "source": "agentshield",
      "projectId": "acme-corp",
      "timestamp": "2026-08-19T12:00:00.000Z",
      "requestId": "req_abc123"
    }
  }
}

Per-tool policy fields: approval is always when the tool requires delegation, otherwise off (the type also declares auto, but this handler never emits it). riskLevel (low | medium | high | critical), authorization (type, plus provider and requiredScopes for OAuth types, provider for password type), and requiredScopes appear when configured. A project with no configured tools returns an empty policies object; a failed tool-protection lookup also degrades to empty policies rather than erroring, so OpenClaw keeps functioning.

Errors: 400 MISSING_PROJECT_ID when the query parameter is absent, 404 PROJECT_NOT_FOUND, 500 PROJECT_LOOKUP_FAILED on a failed project lookup, 403 PERMISSION_FORBIDDEN when your API key is not scoped to this project, 401 AUTH_MISSING_CREDENTIALS / AUTH_INVALID_API_KEY for missing or invalid keys, 429 RATE_LIMIT_EXCEEDED.

Approval Webhook

Called by OpenClaw's approval workflow when a protected tool is invoked. Low-risk tools are approved immediately with a signed delegation token (a W3C VC-JWT, also persisted as an active delegation). Tools that need a human are parked as a pending approval, and the response carries a consent URL to put in front of the user.

Endpoint: POST /api/v1/openclaw/approve

Auth: API key (X-API-Key, or Authorization: Bearer). The key must be project-scoped: the project is resolved from the key, not the body.

Request:

{
  "tool": "gmail_send_email",
  "args": { "to": "user@example.com", "subject": "Hello" },
  "agentDid": "did:key:z6Mk...",
  "sessionId": "session-123",
  "userDid": "did:key:z6Mkuser...",
  "requireHumanApproval": false,
  "callbackUrl": "https://agent.example.com/callback",
  "metadata": {}
}

Required fields: tool (1-255 chars), agentDid (1-500 chars). Optional: args (object), sessionId, userDid, requireHumanApproval (boolean), callbackUrl (URL, SSRF-validated), metadata (object).

Human approval is required when any of these holds: the request sets requireHumanApproval, the tool's protection has riskLevel high or critical, or the tool has requiresDelegation: true. Scopes come from the tool protection's requiredScopes, defaulting to ["{tool}:execute"].

Response:

200 with { success: true, data, metadata: { requestId, timestamp } }. data is one of two shapes:

Immediate approval (token valid 1 hour):

{
  "approved": true,
  "delegationToken": "eyJ...",
  "expiresAt": "2026-08-19T13:00:00.000Z",
  "scopes": ["email:send"],
  "requestId": "..."
}

Pending human approval (request valid 15 minutes):

{
  "approved": false,
  "pending": true,
  "consentUrl": "https://kya.vouched.id/consent/{requestId}?token=...",
  "requestId": "...",
  "expiresAt": "2026-08-19T12:15:00.000Z"
}

The route's own doc comment also describes a denied shape (approved: false, denied: true), but this handler has no code path that emits it: every valid request is either approved immediately or routed to human consent. Denials happen on the consent endpoint below.

If callbackUrl was provided, the consent endpoint will POST the final result to it when the human completes the flow.

Errors: 400 INVALID_REQUEST_BODY for unparsable JSON, 400 VALIDATION_ERROR for a body that fails the schema, 400 NO_PROJECT when the API key is not associated with a project, 404 PROJECT_NOT_FOUND, 500 PROJECT_LOOKUP_FAILED, 503 TOOL_PROTECTION_LOOKUP_FAILED on a failed tool-protection read (deliberately an error, never a silent auto-approve: retry), 400 INVALID_CALLBACK_URL for a callback URL that fails SSRF validation, 500 PENDING_APPROVAL_FAILED / TOKEN_GENERATION_FAILED on a failed persist or signing step, plus 401 AUTH_* and 429 RATE_LIMIT_EXCEEDED from the shared v1 middleware.

The consent flow endpoint behind the consentUrl returned by the approval webhook. GET feeds the consent UI; POST completes the approval.

Endpoint: GET or POST /api/v1/openclaw/consent/{requestId}

Auth: GET: none, it backs the user-facing consent page. POST: authenticated by the consent token minted by the approval webhook, passed in the body (token) or as a ?token= query parameter. Neither method takes an API key, by design.

Request:

GET takes no body. POST body:

{
  "action": "approve",
  "reason": "optional, max 500 chars",
  "token": "consent token, if not in the query string"
}

action is required: approve or deny. reason is stored as the denial reason on deny (defaults to "User declined").

Response:

GET returns 200 with { success: true, data }:

{
  "success": true,
  "data": {
    "requestId": "...",
    "tool": "gmail_send_email",
    "agentDid": "did:key:z6Mk...",
    "scopes": ["email:send"],
    "args": { "to": "user@example.com" },
    "expiresAt": "2026-08-19T12:15:00.000Z",
    "status": "pending",
    "projectId": "...",
    "projectName": "My Project"
  }
}

status reads expired when the request is past its expiry but was still pending; otherwise it is the stored status.

POST returns 200 with { success: true, data }, where data is the approved shape (approved: true, delegationToken valid 1 hour, expiresAt, scopes, requestId) or the denied shape (approved: false, denied: true, reason, requestId). Completion is guarded by optimistic locking, so two racing completions cannot both win. If the original approval request carried a callbackUrl, the result is also POSTed there fire-and-forget as { event: "approval_completed", requestId, approved, ... }; callback failures never affect the approval itself.

Errors: 404 REQUEST_NOT_FOUND for an unrecognized requestId (both methods). POST only: 400 INVALID_REQUEST_BODY for unparsable JSON, 400 VALIDATION_ERROR when action is missing or invalid, 401 MISSING_CONSENT_TOKEN / INVALID_CONSENT_TOKEN for an absent or failed token check, 400 REQUEST_ALREADY_COMPLETED when the request was already completed, 400 REQUEST_EXPIRED when it expired before completion, 409 REQUEST_ALREADY_COMPLETED when a concurrent completion won the race.

Fingerprint

Testing and profiling endpoints for the fingerprinting system. These do not use the standard v1 envelope consistently, most take no authentication, and several exist to feed the internal comparison and profiling tools. All of them export permissive-CORS OPTIONS handlers.

The fingerprint endpoints are testing and profiling tools, not part of the supported public contract: most take no authentication, return _debug payloads, and do not use the standard v1 response envelope. Treat them as internal tooling until the contract says otherwise.

Capture Request Fingerprint

Endpoint: GET /api/v1/fingerprint or POST /api/v1/fingerprint

Auth: none. The route runs through the core API middleware with requireAuth: false; a permission: 'read' option is passed but has no effect without auth.

Request: no body required. Optional query parameters test or id tag the capture with a test identifier for the fingerprint log. A POST body of any content type is echoed back in the response (body, bodyType, contentType); JSON is parsed, anything else is read as text.

Response: 200 with { success: true, data, metadata: { requestId, timestamp } }. data is the captured fingerprint (shape derived from the FingerprintResponse type plus route-added debug fields):

  • Echo of the request: timestamp, method, url, path, queryParams, headers (every request header, lowercased), headerCount, ip (first match across nine proxy headers; literal "unknown" when none present), httpVersion
  • fingerprint: rule-engine analysis (hasChromiumHeaders, hasBrowserHeaders, hasSecFetchHeaders, isAutomated, suspiciousPatterns, confidence, detectedAgent, isLikelyBot, isLikelyAI, plus rulesApplied and breakdown debug detail)
  • analysis: named header extracts (userAgent, acceptHeader, acceptLanguage, acceptEncoding, hasReferer, hasCookies, hasAuthorization, and the sec-ch-ua* / sec-fetch-* values), each "Not provided" when absent
  • detection: { isLikelyBot, isLikelyAI, agentType, confidence } where confidence is a percent string such as "92%"
  • _debug: rulesApplied, breakdown, processingTime

Flag: the two confidence usages disagree on scale. detection.isLikelyBot tests fingerprint.confidence > 70 while detection.confidence renders Math.round(fingerprint.confidence * 100), which cannot both be right for one scale. Documenting the fields as returned; do not build on the numeric scale without checking the rule engine.

Errors: none emitted explicitly; an unexpected failure returns 500 INTERNAL_SERVER_ERROR in the standard error envelope via the shared error handler.

Enhanced Fingerprint Capture

Endpoint: GET /api/v1/fingerprint/enhanced or POST /api/v1/fingerprint/enhanced

Auth: none.

Request: no body required. Optional test / id query parameters as above. A JSON POST body is treated as client-side detection data (the route uses body.clientDetection when present, otherwise the whole body) and is folded into the analysis, including a Perplexity-specific detector; body.metadata.testIdentifier overrides the query parameter. Request shape is derived from property reads, not a Zod schema: there is no validation, and any body is accepted.

Response: 200, not enveloped: the fingerprint object is the response body. Everything from the base capture endpoint plus:

  • enhanced: tls (version, cipherSuites, ja3, ja3s, read from x-tls-* / x-ja3* proxy headers when present), network (ttfb, tcp), processingTime, ipInfo (primary, allForwarded), suspiciousHeaders (nonstandard x-* headers), missingHeaders (browser headers absent from the request)
  • metadata: flattened detection summary for storage (isAgent, isAI, confidence as a number, agentType, ipAddress, userAgent, url, method, detectionMethod, factors, testIdentifier)
  • _debug: adds additionalMetadata (informational observations) and perplexityDetection (isPerplexity, confidence, signals, matchedPatterns)
  • POST also echoes body, bodyAnalysis (hasBody, bodyType, bodySize, isJson, jsonKeys), contentType

Responses carry X-Fingerprint-Version: enhanced-v1.

Errors: the handlers have no error wrapper; an unexpected failure surfaces as a framework 500, not the standard error envelope.

Collect Fingerprint Data

Endpoint: POST /api/v1/fingerprint/collect

Auth: none. Rate limited per client IP: 1 request/second and 10/minute (the route also configures a 100/hour cap, but the enforcement path checks only the burst, per-second, and per-minute windows, so the hourly figure is not enforced). Responses carry X-RateLimit-Limit / X-RateLimit-Remaining / X-RateLimit-Reset.

Request: validated by CollectFingerprintRequestSchema (@kya-os/checkpoint-shared):

{
  "testId": "test-abc123",
  "integrationType": "beacon",
  "data": {
    "clientSide": { "canvas": { "hash": "..." }, "webgl": {}, "audio": {} },
    "behavioral": {},
    "metadata": { "collectionDuration": 120, "errors": [], "warnings": [] }
  }
}

integrationType (required): one of pixel, beacon, noscript, middleware, express, edge, wasm. testId optional. data is z.any() in the schema, so its interior is not validated; the keys shown are the ones the handler reads (clientSide is stored only for pixel/beacon, request headers are stored for the server-side types, geo-enhanced server data for middleware/express). Note the geo data attached for middleware/express is a hardcoded mock (San Francisco), not a real lookup.

The stored record is raw collection data with a preliminary, low-confidence classification; real detection runs later via the detect endpoint. When testId starts with test-, the result is also forwarded to the comparison log (fire-and-forget) as the pixel source.

Response: 200 validated by CollectFingerprintResponseSchema:

{
  "success": true,
  "sessionId": "V1StGXR8_Z5jdHi6B-myT",
  "fingerprintId": "0b7f6c9e-...",
  "message": "Fingerprint collected successfully via beacon"
}

fingerprintId is the stored row id (use it with the detect and test endpoints); sessionId is a per-collection identifier generated by this request.

Errors: 400 VALIDATION_BAD_REQUEST for unparsable JSON, 400 VALIDATION_INVALID_REQUEST with field details for a schema failure, 429 RATE_LIMIT_EXCEEDED, 500 INTERNAL_SERVER_ERROR otherwise (all in the standard error envelope).

Run WASM Detection on a Fingerprint

Endpoint: POST /api/v1/fingerprint/detect

Auth: none.

Request: { "fingerprintId": "<uuid>" }. Derived from a TypeScript interface, not a Zod schema; only fingerprintId is checked. The interface also declares runFullDetection?: boolean but the handler never reads it: sending it has no effect.

Runs the WASM detection engine over the stored fingerprint and writes the results back to the row (this endpoint mutates the record).

Response: 200, not enveloped:

{
  "success": true,
  "fingerprintId": "0b7f6c9e-...",
  "detection": {
    "is_agent": true,
    "confidence": 0.92,
    "agent_type": "chatgpt",
    "verification_method": "pattern",
    "risk_level": "high",
    "signature_verified": false,
    "detection_time_ms": 12
  },
  "message": "Detected as chatgpt with 92% confidence"
}

confidence here is on the engine's 0 to 1 scale. signature_verified is true when the engine's verification_method indicates cryptographic verification.

Errors: non-standard shapes ({ success: false, error: "<string>" }): 400 "fingerprintId is required", 404 "Fingerprint not found", 500 "Detection engine error" (with a details string) when the WASM call throws, 500 with the error message otherwise. Error error is a plain string, not the { code, message } object used elsewhere.

Simulate Detection Across Integration Types

Endpoint: POST /api/v1/fingerprint/test

Auth: none.

Request: validated by TestDetectionRequestSchema: { "fingerprintId": "<uuid>", "integrationTypes": ["pixel", "beacon"] }. integrationTypes optional; defaults to all seven. This endpoint runs a simplified heuristic simulation per integration type (signature headers, user-agent patterns, headless WebGL, webdriver, behavioral signals), not the WASM engine, and stores a test-run record. It also overwrites the fingerprint row's behavioral_data with the test result and can update its ai_type/confidence, so it mutates state.

Response: 200, not enveloped:

{
  "fingerprintId": "0b7f6c9e-...",
  "timestamp": "2026-08-19T12:00:00.000Z",
  "results": {
    "pixel": {
      "isAgent": true,
      "confidence": 0.85,
      "agentType": "chatgpt",
      "vendor": "openai",
      "reasons": ["GPT user agent pattern"],
      "processingTime": 3
    }
  },
  "summary": {
    "consensusIsAgent": true,
    "averageConfidence": 0.72,
    "mostLikelyAgent": "chatgpt",
    "integrationAgreement": 71.4
  },
  "recommendations": ["Strong consensus: 5/7 integrations detected an agent"]
}

results has one entry per tested integration type. integrationAgreement is the percentage of integrations voting agent.

Errors: 404 { success: false, error: "Fingerprint not found" }. Every other failure, including a body that fails schema validation, returns 500 with the error message: the Zod parse happens inside the catch-all try block, so malformed requests get a 500, not a 400. Error error is a plain string.

Log a Comparison Result

Endpoint: POST /api/v1/fingerprint/comparison/log

Auth: split by source. middleware and pixel submissions are unauthenticated. gateway submissions must present the internal gateway key (X-Gateway-Key: <key> or Authorization: Bearer <key>, checked against the GATEWAY_INTERNAL_KEY environment secret); a missing or wrong key returns 401 { success: false, error: "Unauthorized - invalid gateway key" }. When the secret is unconfigured on the server, every gateway submission is rejected. The gateway leg is for the Gateway Worker, not customers.

Aggregates detection results from the three methods under one testId for side-by-side comparison. Each source upserts its own column set, so the three can arrive in any order; methodsAgree is recomputed once at least two are present.

Request: validated by an inline Zod schema:

{
  "testId": "test-abc123",
  "source": "middleware",
  "detection": {
    "isAgent": true,
    "confidence": 92,
    "agentName": "ChatGPT-User",
    "agentType": "chatgpt",
    "detectionClass": "ai_agent",
    "verificationMethod": "pattern",
    "reasons": ["..."],
    "signatureVerified": false,
    "metadata": {}
  },
  "metadata": {
    "tlsFingerprint": { "ja3Fingerprint": "..." },
    "processingTimeMs": 42,
    "fingerprintId": "0b7f6c9e-...",
    "userAgent": "Mozilla/5.0...",
    "ipAddress": "203.0.113.7"
  }
}

testId must start with test-. source is gateway | middleware | pixel. detection.confidence is 0 to 100 here (unlike the detect endpoint's 0 to 1). All metadata fields are optional; tlsFingerprint (ciphersSha1, extensionsSha1, httpProtocol, tlsVersion, clientHelloLength, ja3Fingerprint) is meaningful for the gateway source, fingerprintId for pixel.

Response: 200, not enveloped:

{
  "success": true,
  "testId": "test-abc123",
  "source": "middleware",
  "comparisonId": "7c1d...",
  "message": "Detection logged from middleware",
  "methodsLogged": { "gateway": false, "middleware": true, "pixel": true },
  "methodsAgree": true
}

methodsAgree is null until at least two sources have logged.

Errors: 401 invalid gateway key (gateway source only), 400 { success: false, error: "Invalid testId format - must start with \"test-\"" }, 400 { success: false, error: "Validation error", details: [...] } for a schema failure, 500 with the error message otherwise. Error error is a plain string.

Get Comparison Results

Endpoint: GET /api/v1/fingerprint/comparison/{testId}

Auth: none. Anyone holding a testId can read its comparison record, including the logged user agent and IP address.

Response: 200, not enveloped; a fixed shape assembled from the stored row:

{
  "success": true,
  "testId": "test-abc123",
  "comparisonId": "7c1d...",
  "gateway": {
    "detected": true,
    "detectedAt": "2026-08-19T12:00:00.000Z",
    "detection": { "isAgent": true, "confidence": 92 },
    "tlsFingerprint": { "ja3Fingerprint": "..." },
    "processingTimeMs": 8
  },
  "middleware": {
    "detected": true,
    "detectedAt": "2026-08-19T12:00:01.000Z",
    "detection": { "isAgent": true, "confidence": 88 },
    "processingTimeMs": 42
  },
  "pixel": {
    "detected": false,
    "detectedAt": null,
    "detection": null,
    "fingerprintId": null,
    "fingerprintData": null
  },
  "userAgent": "Mozilla/5.0...",
  "ipAddress": "203.0.113.7",
  "summary": {
    "methodsLogged": 2,
    "methodsAgree": true,
    "consensusResult": "AI Agent (ChatGPT-User)",
    "discrepancies": []
  },
  "createdAt": "2026-08-19T12:00:00.000Z",
  "updatedAt": "2026-08-19T12:00:01.000Z"
}

Each source block's detection carries whatever that source logged (the common fields are isAgent and confidence; the rest are optional). pixel.fingerprintData, when a fingerprint is linked, contains clientFingerprints, serverFingerprints, and behavioralData from the stored row. summary.consensusResult is "AI Agent (<name>)", "Human", or "Inconclusive" by majority vote; when the majority says agent but no source named one, the literal string is "AI Agent (Unknown)" (a code literal, quoted as emitted). discrepancies lists human-readable disagreement notes (isAgent splits, confidence spread over 30 points, agent-name mismatches).

Errors: 404 { success: false, error: "Comparison not found", testId }, 400 "testId is required" (unreachable through normal routing since the path segment is always present), 500 with the error message otherwise. Error error is a plain string.

Platform Status

Get Platform Status

Public platform health report. Backs the /status page and any external monitor a customer points at Checkpoint.

Endpoint: GET /api/v1/status

Auth: None, public and unauthenticated by design.

Request: No parameters.

Response:

{
  "overall": "operational",
  "checks": [
    {
      "id": "db",
      "label": "Database",
      "description": "One-line description of what the check proves",
      "group": "platform",
      "status": "operational",
      "latencyMs": 12,
      "detail": null
    }
  ],
  "generatedAt": "2026-07-27T12:00:00.000Z",
  "version": "1.2.3"
}

overall is operational, degraded, or major_outage. Each check's status is operational, degraded, down, or not_configured (not_configured checks are excluded from the overall rollup). group is platform, engines, or edge. latencyMs is null for checks that never ran; version is null when the runtime does not expose the web app version. detail carries only a fixed public vocabulary (versions, mapped failure codes), never raw error messages.

Results are memoized server-side and returned with Cache-Control: public, s-maxage=<ttl>, stale-while-revalidate=<4x ttl> (the route comments state a 30 second window), so polling cannot stampede the underlying probes.

Errors: None structured. Failures inside individual probes are folded into per-check status values rather than error responses; an unexpected handler failure would surface as a framework 500.

Search Documentation

Full-text search over the documentation pages. This backs the docs site's search box; it searches documentation content only, not customer data.

Endpoint: GET /api/v1/search

Auth: None, public.

Request:

ParameterDescription
qSearch query (query is accepted as alias)

An empty or missing query returns an empty result list.

Response:

{
  "results": [
    {
      "title": "Public API Reference",
      "url": "/docs/api-reference",
      "description": "Complete reference for the Checkpoint REST API"
    }
  ]
}

Results are sorted by relevance (title matches rank above description matches, which rank above body matches). description may be absent.

Errors: 500 with { "results": [], "error": "Search failed" } on an unexpected failure. Note this is not the standard error envelope used by the authenticated endpoints.

Infrastructure and Demo Endpoints

The endpoints below exist under /api/v1 but are not part of the supported customer integration contract. They are documented here for completeness and honesty about the surface.

Managed LLM Proxy (Anthropic Messages)

Platform infrastructure for managed-mode deployments, not a customer-callable API. Forwards Anthropic /v1/messages requests on behalf of managed deployments: the deployment's OpenClaw runtime presents its per-deployment gateway token, and Checkpoint forwards to api.anthropic.com using a platform-owned shared key while metering a per-deployment token budget. BYOK deployments do not use this route; they call Anthropic directly with their own key.

Endpoint: POST /api/v1/llm/anthropic/v1/messages

Auth: Per-deployment gateway token in the x-api-key header (the same header slot Anthropic's own API uses, so the SDK needs no changes). Not a Checkpoint API key. The whole route is behind a kill switch: unless the MANAGED_LLM_ENABLED environment variable is true, it returns a plain 404 Not Found.

Request: the Anthropic Messages request body, forwarded byte-for-byte to the upstream (default https://api.anthropic.com/v1/messages, overridable by environment). The anthropic-version header is forwarded (defaulting to 2023-06-01) and anthropic-beta is forwarded when present. The top-level stream flag in the body selects streaming.

Response: the upstream Anthropic response, status and body forwarded. Streaming responses (text/event-stream) are piped through with usage frames (message_start, message_delta) tapped for budget accounting without buffering; non-streaming and all non-2xx upstream responses are returned as JSON. A non-2xx upstream is always returned as JSON even for a streaming request, matching Anthropic's own behavior for pre-flight errors.

Errors: Anthropic-style bodies, { "type": "error", "error": { "type": "...", "message": "..." } }:

  • 401 authentication_error: missing or invalid x-api-key (no matching live managed deployment)
  • 429 rate_limit_error: the deployment's token budget is exhausted (a missing or zero budget counts as exhausted, fail-closed; carries retry-after: 86400)
  • 502 server_error: upstream unreachable, or an empty upstream stream
  • 503 server_error: platform upstream key not configured
  • Upstream Anthropic errors pass through with their original status and body

Generate Demo Data

Demo-only endpoint that generates synthetic session and detection data for product demos. Unauthenticated, reads and writes no customer data, and every response is stamped demo: true. Do not build integrations against it.

Endpoint: POST /api/v1/demo/generate (a GET variant exists for simple testing)

Auth: None.

Request: POST body: { url?: string (must be a valid URL), count?: number (1 to 20, default 5) }. GET accepts the same as url and count query parameters, with count capped at 10.

Response:

{
  "success": true,
  "sessions": [{ "sessionId": "demo_...", "isAgent": true, "...": "..." }],
  "metadata": {
    "totalSessions": 5,
    "totalDetections": 23,
    "agentSessions": 4,
    "humanSessions": 1,
    "demo": true,
    "generatedAt": "2026-07-27T12:00:00.000Z",
    "requestId": "req_abc123"
  }
}

Each generated session mirrors the shape of real session data (sessionId, agentType, isAgent, confidence, ipAddress, userAgent, paths, detectionReasons, detectionSources, per-event events[], and more), with weighted random agent types (ChatGPT, Claude, Perplexity, Grok, Human). Note one wrinkle: for human sessions agentType carries the literal string Unknown, which is not a real detection class (demo filler only); do not branch on it.

Errors: 400 VALIDATION_BAD_REQUEST for a non-JSON POST body, 400 VALIDATION_INVALID_REQUEST when the body fails schema validation (e.g. count out of range).

WASM Detection (Pixel Support)

A dedicated WASM detection endpoint designed to be called by the main pixel endpoint for separation of concerns. It is unauthenticated and CORS-open, but it is a service-support endpoint, not a supported public contract; integrations should use /api/v1/detect instead.

Endpoint: POST /api/v1/pixel-wasm (plus OPTIONS for CORS preflight)

Auth: None.

Request: JSON body with userAgent, ipAddress, headers, url, method (property reads, no schema validation; all effectively optional with fallbacks).

Response: 200 with { success: true, detection: { isAgent, confidence, detectionClass, signals, agent, agentType, detectedAgent, verificationMethod, riskLevel, timestamp } }. confidence is on the 0 to 100 scale. On an internal detection failure the endpoint still returns 200 with a zeroed stub result (isAgent: false, confidence: 0) rather than an error.

Errors: None specific; an unexpected handler failure goes through the shared error handler.

Rate Limits

Default API rate limits by plan:

PlanRequests per SecondRequests per Day*
Free11,000
Pro1050,000
Enterprise100Unlimited

* The per-day figure is your plan's quota, not a runtime-enforced limit — live enforcement checks burst, per-second, and per-minute windows only.

Detection responses (/detect) include rate limit headers:

  • X-RateLimit-Limit: Maximum requests allowed in the per-minute window (this header always reports the minute limit, not the per-second or per-day figures above)
  • X-RateLimit-Remaining: Requests remaining
  • X-RateLimit-Reset: Time when limit resets (Unix timestamp)

Enforcement responses (/enforce) carry KYA-* headers instead — see Evaluate Request.

The plan limits above apply to the API-key endpoints. The unauthenticated ingestion and OAuth endpoints are rate-limited per client IP instead; their per-endpoint notes call out any deviations (for example /pixel/track's RATE_LIMITED code, or the gateway OAuth surface's RFC 6749-shaped rate_limit_exceeded).

Error Handling

Checkpoint uses standard HTTP status codes and returns errors in a consistent format:

{
  "success": false,
  "error": {
    "code": "RATE_LIMIT_EXCEEDED",
    "message": "Rate limit exceeded",
    "details": {
      "limit": 100,
      "reset": 1234567890
    }
  },
  "metadata": {
    "requestId": "req_abc123",
    "timestamp": "2026-07-27T12:00:00.000Z"
  }
}

Common Error Codes

StatusCodeDescription
400VALIDATION_INVALID_REQUESTRequest body or parameters failed validation
401AUTH_MISSING_CREDENTIALSNo API key provided
401AUTH_INVALID_API_KEYAPI key not found or revoked
401AUTH_EXPIRED_API_KEYAPI key has expired
401AUTH_UNAUTHORIZEDAuthentication required or failed
403PERMISSION_INSUFFICIENTAPI key lacks the required permission scope
404PERMISSION_RESOURCE_NOT_FOUNDResource not found
404PIXEL_NOT_FOUNDPixel ID not recognized
429RATE_LIMIT_EXCEEDEDToo many requests
500INTERNAL_SERVER_ERRORServer error

Individual routes also emit codes outside this shared enum: the per-endpoint Errors lines on this page name them (for example PROJECT_NOT_FOUND, PERMISSION_FORBIDDEN, the RFC 6749 { error, error_description } shape on the gateway OAuth surface, and the lowercase Bouncer codes such as validation_error and invalid_token).

SDKs & Packages

Official packages for integration:

PackageInstall
Next.js Middlewarenpm install @kya-os/checkpoint-nextjs
Express Middlewarenpm install @kya-os/checkpoint-express
.NET Middlewaredotnet add package KyaOs.Checkpoint
JavaScript Beaconnpm install @kya-os/checkpoint-beacon
Govern Middlewarenpm install @kya-os/bouncer-middleware

See Choose Your Integration for detailed comparison.

Best Practices

  1. Cache API responses when possible to reduce API calls
  2. Use batch endpoints for bulk event tracking
  3. Implement exponential backoff for rate limit errors
  4. Store API keys securely and rotate them regularly
  5. Use middleware packages instead of raw API calls for detection

Support

On this page

OverviewBase URLAuthenticationWhich endpoints need a key?Core Detection APIsDetect AI AgentEvent TrackingSend EventBatch EventsPixel TrackingPixel EndpointShopify Pixel TrackLog DetectionProject ManagementList ProjectsGet Project DetailsGet Project DetectionsGet Project AnalyticsManaged DeployShopify EndpointsGet Shopify ProjectUpdate Shopify ProjectGet Shopify Project StatsEnforce APIEvaluate RequestPolicy EndpointsGet Customer PolicyGet Middleware PolicyWebhooksSubscription objectDelivery format and signature verificationList Webhook SubscriptionsCreate Webhook SubscriptionGet Webhook SubscriptionUpdate Webhook SubscriptionDelete Webhook SubscriptionRotate Webhook SecretVerify Webhook SubscriptionSend Test EventBouncer (KYA-OS Governance) APIsAuthorizationDelegationsCreate DelegationRevoke DelegationGet Delegation TokensVerify DelegationNotify DelegationOAuthOAuth Token ExchangeIdentityProofsSessionsProject ConfigurationGet or Update Project ConfigValidate Project ConfigGet Consent ConfigCreate Delegation (Consent Flow)List ProvidersKYA-OS Audit EventsIngest Audit EventsSubmit Audit Ledger EntryGateway APIsRegister OAuth ClientAuthorize (Global)Authorize (Per-Org)TokenNeeds-Authorization ChallengeRevocation Status ListConnection CallbackVault Resolve (Gateway)VaultResolve SecretsMolti (Managed Agent Runtime) APIsAgent HeartbeatGet Config BundleOpenClaw Integration APIsGet OpenClaw Tool PoliciesApproval WebhookConsent CompletionFingerprintCapture Request FingerprintEnhanced Fingerprint CaptureCollect Fingerprint DataRun WASM Detection on a FingerprintSimulate Detection Across Integration TypesLog a Comparison ResultGet Comparison ResultsPlatform StatusGet Platform StatusDocumentation SearchSearch DocumentationInfrastructure and Demo EndpointsManaged LLM Proxy (Anthropic Messages)Generate Demo DataWASM Detection (Pixel Support)Rate LimitsError HandlingCommon Error CodesSDKs & PackagesBest PracticesSupport