API reference
KYE Protocol™ Site API
23 operations · site.yaml
Public-site Pages Functions surface for kyeprotocol.com — the lead-capture forms (Audit Pilot™, PoC, GovernedUI access, contact), DSAR intake, expert-review wall, the Consultant Marketplace™ public listing, the public stats ticker, the quiz evidence sink, the §38 Comms Engine™ unsubscribe surface, the Clerk webhook receiver and the public self-audit trust surface.
Operations are unauthenticated public form endpoints unless noted. Form endpoints are rate-limited per salted IP hash (the raw IP is never stored) and carry a hidden website honeypot field — a non-empty honeypot is silently discarded with a 200 so bots learn nothing. Outbound email always routes through the KYE™ Comms Engine™ (constitution §38); privileged actions emit the §0.3 evidence-event family.
Internal
GET/api/_internal/healthcheck-workersFan-out healthcheck of every deployed KYE™ Worker (operator-only)
Probes each deployed KYE™ Worker's /healthz via the Service bindings configured on the kye-protocol Pages project (the only reachable path because every Worker runs with workers_dev=false and no public route). Returns one JSON payload with a per-worker status, latency and version, plus a summary block; the healthcheck-workers.yml workflow archives it to _diagnostics/healthcheck/<ts>.json. Gated by the HEALTHCHECK_INTERNAL_TOKEN shared secret when set.
Auth: HealthcheckBearer
Responses
200Every probed Worker reported healthy207Multi-status — at least one Worker reported unhealthy (same body shape as the 200).401Bearer token missing or mismatched
LeadCapture
GET/api/audit-pilotEndpoint self-description for the Audit Pilot™ application form
Returns a small JSON descriptor (expected method + the pilot-apply page URL) so a GET probe of the form endpoint is self-documenting rather than a 405.
Responses
200Endpoint descriptor
POST/api/audit-pilotSubmit an Audit Pilot™ application (public)
Receives the pilot-apply form (JSON or form-data), validates the enum fields (role, company size, industry, urgency, SKU), blocks free-email-provider addresses, requires all four consent clauses (tos, privacy, aup, authority), emits a signed kye.consent.acceptance.v1 record, persists application + consent to D1, then in the background dispatches the §38 admin alert + applicant confirmation (suppressed for detected test traffic) and enqueues the §22 onboarding and §27 commercial-lifecycle workflows. Rate limit: 3 submissions per 24 h per IP hash. Honeypot field website.
Request body (required)
| field | type | description |
|---|---|---|
full_name required | string | |
email required | string | Work email — free providers are rejected |
role required | string | One of CISO, CDO, Head of AI, Head of Compliance, Procurement Lead, Engineering Lead, Other |
company required | string | |
company_size required | string | One of <100, 100-1k, 1k-10k, 10k-50k, 50k+ |
industry required | string | One of Financial Services - Banking, Financial Services - Insurance, Financial Services - Payments/PSP, Healthcare, Pharma, Public Sector, Other Regulated, Other |
regulatory_regime | object | One or more applicable regulators (unknown values are dropped) |
ai_workflow required | string | |
urgency required | string | One of This quarter, Next quarter, This half, Within 12 months, Exploring |
sku_id required | string | One of KYE-DISCOVERY-001, KYE-AUDIT-PILOT-001, KYE-REG-PILOT-001, KYE-GOVOPS-PILOT-001, KYE-EDGE-PILOT-001, KYE-SANDBOX-PILOT-001, KYE-CKAN-AUDIT-PILOT-001, KYE-CKAN-GOVOPS-PILOT-001, UNDECIDED |
heard_from | string | |
consent_tos | object | Must be accepted |
consent_privacy | object | Must be accepted |
consent_aup | object | Must be accepted |
consent_authority | object | Must be accepted |
consent_dpa | object | Optional DPA clause |
website | string | Honeypot — leave empty |
Responses
200Application accepted (or honeypot silently discarded). The signed consent record is echoed back so the client can store the receipt.400Validation failure. Error codes include `invalid_body`, `missing_field:<name>`, `invalid_sku_id`, `invalid_email_shape`, `free_email_provider_blocked`, `invalid_role`, `invalid_company_size`, `invalid_industry`, `invalid_urgency`, `ai_workflow_too_short`, `co…429Per-IP-hash rate limit exceeded. The `retry-after` header carries the wait in seconds.
GET/api/contactEndpoint self-description for the contact form
Responses
200Hint that the endpoint accepts POSTed contact forms
POST/api/contactSubmit the contact-modal form (public)
Receives the contact-modal submission from any KYE Protocol™ page (JSON or form-urlencoded), validates + spam-filters it, dispatches the §38 contact.inbound.v1 admin notification (with one-click Approve/Reject buttons for partner/trainer/auditor topics, §27 §4 dual-channel admin) and a contact.applicant-ack.v1 receipt to the submitter, then persists an audit row to D1. Rate limit: 5 submissions per IP per 5 minutes. Honeypot field website. When the mail binding is unattached the endpoint answers 503 with fallback: "mailto" so the client degrades to a mailto: link — a submission is never silently dropped.
Request body (required)
| field | type | description |
|---|---|---|
name required | string | |
email required | string | |
organisation required | string | |
phone required | string | |
position required | string | |
topic | string | Routing topic (default `general`; partner/trainer/auditor get admin action buttons) |
message required | string | |
accept required | object | Terms + privacy acceptance — required truthy |
website | string | Honeypot — leave empty |
Responses
200Mail dispatched (or honeypot silently discarded)400Validation failure — `invalid_body`, `missing_field:<name>`, `invalid_email`, `terms_not_accepted`, `message_too_long`.429Per-IP-hash rate limit exceeded. The `retry-after` header carries the wait in seconds.502The mail-sender dispatch failed; client should fall back to mailto503Mail binding unattached on this deployment; client should fall back to mailto
POST/api/engage-accessRequest access to a GovernedUI™ SKU tier (public)
Single canonical entrypoint for every GovernedUI™ access request from the public site (constitution §27 §4 dual-channel admin). Maps the tier field to one of the 5 canonical GovernedUI SKUs (pilot → KYE-GUI-PILOT-001, department → KYE-GUI-DEPT-001, enterprise → KYE-GUI-ENT-001, regulated → KYE-GUI-REG-001, national → KYE-GUI-NAT-001), persists the request to D1, emits the engagement-access evidence event to the audit chain, then dispatches the §38 admin alert with Approve / Request-changes buttons plus an applicant confirmation. The Stripe payment link is returned after admin approval, never inline. Rate limit: 3 per 24 h per IP hash. Honeypot field website. Work email required.
Request body (required)
| field | type | description |
|---|---|---|
tier required | string | One of pilot, department, enterprise, regulated, national |
name required | string | |
email required | string | Work email — free providers are rejected |
organisation required | string | |
phone | string | |
role required | string | |
agent_classes | string | Scope — agent classes to govern |
protected_systems | string | Scope — systems the agents touch |
business_units | string | |
regulatory_exposure | string | |
deployment_topology | string | |
message | string | |
consent_accepted required | boolean | Must be truthy |
website | string | Honeypot — leave empty |
Responses
200Request recorded (or honeypot discarded with `request_id: "discarded"`)400Validation failure — `json_parse_failed`, `unknown_tier` (body lists the valid tiers), `missing_required_field`, `invalid_email`, `work_email_required`, `consent_required`.429Per-IP-hash rate limit exceeded. The `retry-after` header carries the wait in seconds.500Unhandled handler exception (message echoed)
/api/consultant-leadCORS preflight for the consultant-lead capture form
Answers 204 allowing GET/POST/OPTIONS with the content-type header from the https://kyeprotocol.com origin, cacheable for 24 h. The GET/POST operations themselves are declared in the consultant-programme OpenAPI document (§0 — one declaration per operation).
Responses
204Preflight accepted (empty body)
POST/api/v1/poc/applySubmit a Proof-of-Concept application (public)
Backs /poc.html. Maps the tier field to the canonical PoC SKU (discovery/undecided → KYE-POC-DISCOVERY-001, audit → KYE-POC-AUDIT-001, reg → KYE-POC-REG-001, govops → KYE-POC-GOVOPS-001, edge → KYE-POC-EDGE-001, sandbox → KYE-POC-SANDBOX-001), persists the kye.poc.application.v1 shape to D1, then dispatches the §38 admin alert with Approve / Request-changes buttons and the applicant confirmation (§27 §4 dual-channel admin). Rate limit: 3 per 24 h per IP hash. Honeypot field website. Work email required.
Request body (required)
| field | type | description |
|---|---|---|
tier required | string | One of discovery, audit, reg, govops, edge, sandbox, undecided |
name required | string | |
email required | string | Work email — free providers are rejected |
company required | string | |
role | string | |
workflow | string | The AI workflow to govern in the PoC |
frameworks | string | Regulatory frameworks in scope |
website | string | Honeypot — leave empty |
Responses
200Application recorded (or honeypot discarded with `application_id: "discarded"`)400Validation failure — `json_parse_failed`, `unknown_tier` (body lists the valid tiers), `missing_required_field`, `invalid_email`, `work_email_required`.429Per-IP-hash rate limit exceeded. The `retry-after` header carries the wait in seconds.500Unhandled handler exception (message echoed)
Dsar
GET/api/dsarEndpoint self-description for the DSAR intake
Responses
200Hint describing the expected POST body
POST/api/dsarFile a Data Subject Access Request (public)
Self-service DSAR intake under GDPR Art. 15–21 and the equivalent rights in UK GDPR, CCPA/CPRA, PIPEDA, LGPD, PIPL and POPIA (backs /dsar.html). Mints a stable kye:dsar-request:<date>.<hex> reference, persists a WORM row to D1 (status transitions are append-only via supersession), emits a §0.3 kye.evidence.decision_map.v1 event (dsar_request.filed), then sends the §38 admin alert with one-click Acknowledge/Reject buttons and a subject acknowledgement carrying the statutory deadline. Rate limit: 5 requests per 24 h per IP hash.
Request body (required)
| field | type | description |
|---|---|---|
controller required | string | The data controller the request targets |
right required | string | One of access, rectification, erasure, restriction, portability, objection, withdraw_consent, ccpa_know, ccpa_delete, ccpa_opt_out |
regime | string | One of gdpr, uk_gdpr, ccpa, pipeda, lgpd, pipl, popia, other |
email required | string | |
name | string | |
detail | string | Specific items / time window |
consent required | boolean | Must be `true` |
Responses
201Request filed; statutory window computed per regime400Validation failure — body carries `reason` ∈ {invalid_json, controller_required, invalid_right, invalid_regime, invalid_email, consent_required}.429More than 5 requests in 24 h from the same IP hash
/api/dsarCORS preflight for the DSAR intake
Answers 204 with the allowed methods (POST, GET, OPTIONS) and allowed request header (content-type). No allow-origin header is set — the form is same-origin on kyeprotocol.com.
Responses
204Preflight accepted (empty body)
ExpertReviews
GET/api/expert-reviewList published expert reviews (public)
Lists reviews for the expert wall. Only status=published is publicly readable; requesting pending or rejected returns 401 (moderation listing is owners-only via the Admin Console). Without the D1 binding the endpoint degrades to an honest empty list (not_provisioned: true) so the page still renders.
Parameters
| name | in | type | description |
|---|---|---|---|
status | query | string | Only `published` is allowed unauthenticated |
limit | query | integer |
Responses
200Published reviews, newest first401Non-published status requested without owner auth
POST/api/expert-reviewSubmit an expert review for moderation (public)
Accepts a review of a named KYE™ artefact (JSON or form-data), validates the verdict enum and the optional LinkedIn profile URL (https linkedin.com /in or /pub paths only), emits a kye.evidence.audit_event.v1 to the AI Call Ledger queue, inserts the row as status=pending, then notifies the moderation inbox via the §38 expert-review.brief.v1 template with one-click Approve-&-publish / Reject buttons and sends the submitter the expert-review.applicant-ack.v1 receipt (both fail-soft — the persisted row is canonical). Rate limit: 3 submissions per 24 h per day-salted IP hash. Honeypot field website.
Request body (required)
| field | type | description |
|---|---|---|
name required | string | |
role required | string | |
affiliation required | string | |
email required | string | Used for the receipt + embed code; never published |
artefact required | string | The KYE™ artefact reviewed |
review_text required | string | |
verdict required | string | One of approved, approved_with_suggestions, changes_requested, under_discussion |
linkedin | string | Optional public LinkedIn profile URL (https only) |
website | string | Honeypot — leave empty |
Responses
200Review queued for moderation (or honeypot discarded)400Validation failure — `invalid_body`, `missing_field:<name>`, `invalid_email`, `invalid_verdict`, `review_too_long`, `invalid_linkedin` (each with a human `hint`).429Per-IP-hash rate limit exceeded. The `retry-after` header carries the wait in seconds.500D1 insert failed (truncated cause in `hint`, review id echoed)503D1 binding not yet attached on this deployment (`not_provisioned`)
/api/expert-reviewCORS preflight for the expert-review endpoint
Answers 204 allowing GET/POST/OPTIONS with the content-type header from the https://kyeprotocol.com origin, cacheable for 24 h.
Responses
204Preflight accepted (empty body)
Marketplace
GET/api/marketplace/consultantsList visible consultants for the public marketplace
Returns every consultant whose status is onboarding or certified (master → professional → associate, then newest first) for the Consultant Marketplace™ page, with facet counts for the filter UI. Email addresses are never returned and applicant lead rows are never exposed. Edge-cached for 5 minutes. Without the D1 binding (or before the table exists) the endpoint degrades to an honest empty list (not_provisioned: true).
Parameters
| name | in | type | description |
|---|---|---|---|
sector | query | string | Keep only consultants whose sectors include this tag |
jurisdiction | query | string | ISO-3166 alpha-2 country filter |
level | query | string | Certification-level filter |
q | query | string | Case-insensitive name / bio match |
limit | query | integer |
Responses
200Visible consultants + facet counts500Query failed (truncated message echoed)
/api/marketplace/consultantsCORS preflight for the marketplace listing
Answers 204 allowing GET/OPTIONS with the content-type header from the https://kyeprotocol.com origin, cacheable for 24 h.
Responses
204Preflight accepted (empty body)
PublicData
GET/api/v1/public/statsPublic counters for the hero live ticker
Returns the small public counter set consumed by assets/live-ticker.js (evidence packs verified, decisions governed, frameworks mapped). Tier 1 reads bounded COUNT queries from D1 when bound; otherwise it serves the repo-derived snapshot (source pins which tier answered). Edge-cached 60 s to match the client poll. Emits a fire-and-forget kye.compliance.attestation.v1 per response (§0.3).
Responses
200Current public counters
POST/api/v1/quiz/respondRecord a completed quiz response (public, best-effort)
Evidence sink for the public quiz widget (/quiz.html → assets/quiz.js): the widget POSTs a kye.quiz.response.v1 envelope on completion so even the lead-capture funnel lands in the WORM audit chain (§0.3). The client treats this as fire-and-forget, so the handler is resilient — it persists when D1 is bound, emits the audit event when the audit-chain base URL is set, and always answers 200 for a well-formed body so a transient backend wobble never breaks the share/score UX.
Request body (required)
| field | type | description |
|---|---|---|
quiz_id required | string | |
score required | number | |
band required | string | Scoring band the result fell into |
answers | array | Raw answer list (persisted as JSON, capped at 8000 chars) |
submitted_at | string |
Responses
200Response accepted; persistence + evidence flags reported honestly400`invalid_body` (unparseable JSON) or `missing_required_field` (quiz_id, score, band)500Unhandled handler exception (truncated message echoed)
Comms
GET/comms/unsubscribeHuman one-click unsubscribe (renders a confirmation page)
Destination of the canonical link.unsubscribe footer link in every §38 / §62 outbound email (PECR reg. 22, CAN-SPAM §5, GDPR Art. 21 — one click, no login, honoured immediately). Verifies the signed token in ?u=, records the suppression through the ONE canonical comms-engine suppress path (the recipient travels as a one-way hash, never the raw email), then renders an HTML confirmation page. A backend hiccup still confirms intent with a 202 — the suppress call is idempotent on the next click.
Parameters
| name | in | type | description |
|---|---|---|---|
u required | query | string | Signed unsubscribe token minted into the email footer |
Responses
200Suppression recorded; confirmation page rendered202Token verified but the suppress backend errored — intent acknowledged, will be actioned400Token missing, altered or expired (error page rendered)
POST/comms/unsubscribeRFC 8058 one-click unsubscribe (mail-client POST)
Mail clients POST List-Unsubscribe=One-Click here; the signed token rides in ?u= and/or the form/JSON body (u or token). No HTML is returned — a 2xx text/plain is the RFC 8058 contract. Suppression goes through the same canonical comms-engine path as the GET flow.
Request body
| field | type | description |
|---|---|---|
u | string | |
token | string |
Responses
200Suppression recorded ("unsubscribed")202Token verified, suppress backend errored — accepted for action400Missing or invalid token
Trust
GET/trust/self-audit/live/{[path}]Fetch a production-signed live self-audit bundle (public)
Public read side of §0.3 self-governance-on-production: serves the Ed25519-signed live self-audit bundles the kye-self-audit-daemon Worker publishes to the shared R2 bucket. The catch-all segment addresses one partition — latest, latest/bundle.json, <YYYY-MM-DD>/ or <YYYY-MM-DD>/bundle.json; an empty segment resolves to latest. Any other shape is rejected (no arbitrary R2 traversal). Anyone can verify the signature against /trust/self-audit-jwks.json with node scripts/verify-self-audit.mjs. Edge-cached 5 minutes.
Parameters
| name | in | type | description |
|---|---|---|---|
[path required | path | string | Catch-all partition selector — `latest` or a `YYYY-MM-DD` date, optionally followed by `/bundle.json`. |
Responses
200The signed bundle bytes, verbatim404Non-addressable path shape (`not_found`) or no bundle published for the partition yet (`no_live_bundle`)503R2 binding not attached on this deployment (`r2_binding_unavailable`) — honest machine-readable fail, never a synthesised bundle
Webhooks
GET/webhooks/clerkWebhook receiver self-description
Returns the receiver hint plus the list of the 18 routed Clerk event types, so a GET probe of the configured webhook URL is self-documenting.
Responses
200Receiver hint + routed event list
POST/webhooks/clerkReceive a Clerk webhook delivery (Svix-signed)
Verifies the Svix v1 signature (svix-id / svix-timestamp / svix-signature headers) against the configured signing secret, then projects each of the 18 routed Clerk event types (user.*, session.*, organization.*, organizationMembership.*, organizationInvitation.*, invitation.*) to a kye.evidence.<bucket>.<verb>.v1 envelope persisted to R2 and fanned out on the webhook queue when those bindings are wired. Recognised events are always ACKed 200 even if a downstream projection fails (the audit chain is canonical; the D1 projection is rebuildable) — Clerk's retry budget is small and a half-applied projection is worse than a missed one. With the secret unset the endpoint answers 503 so Clerk retries with backoff.
Parameters
| name | in | type | description |
|---|---|---|---|
svix-id required | header | string | |
svix-timestamp required | header | string | |
svix-signature required | header | string | Space-separated `v1,<base64>` signature tokens |
Request body (required)
| field | type | description |
|---|---|---|
type | string | |
data | object |
Responses
200Delivery verified and acknowledged400`missing_svix_headers` or `invalid_json`401`invalid_signature` or `signature_verification_failed`503`webhook_secret_unconfigured` — signing secret unset; Clerk will retry