API reference
Generated from the frozen OpenAPI contract (v1.0.0). All routes live under /v1; dashboard routes authenticate with a Clerk session JWT, SDK ingest routes with a per-org API key (X-API-Key).
Endpoints
GET/v1/experiments
List experiments for the caller's org
Returns every experiment the caller's org can see (latest configuration version each). Use it to populate the dashboard list, then drill into an id for its computed results.
auth: clerkJwt · responses: 200, 401
POST/v1/experiments
Create an experiment (a new version is minted)
Creates an experiment from a config (variants, allocation weights, metrics), minted at version 1. The `salt` and `bucket_count` you set here freeze how users are bucketed, so keep them stable for the life of the experiment. Returns the stored experiment with its id.
auth: clerkJwt · responses: 201, 400, 401
GET/v1/experiments/{id}
Get an experiment
Fetches one experiment (its latest version) by id, scoped to the caller's org.
auth: clerkJwt · responses: 200, 404
PUT/v1/experiments/{id}
Update an experiment (mints a new version; sticky assignments preserved)
Applies a config change by minting a new version. Because the salt and bucketing are preserved, users already assigned keep their variant — in-flight traffic is never reshuffled.
auth: clerkJwt · responses: 200, 400
POST/v1/assign
Resolve a variant for a user (SDK ingest plane)
Server-side variant resolution for callers that can't run the SDK's local bucketing (e.g. a non-supported language). Deterministic and sticky. Prefer the SDK's in-process `assign` on hot paths — it needs no network round-trip.
auth: apiKeyPublishable | apiKeySecret · responses: 200, 401, 429
POST/v1/events
Ingest a batch of events (SDK ingest plane, async write path)
Asynchronous write path for `exposure`, `model_call`, and `business_event` records. Returns immediately with an ack and the current `config_version` (a staleness hint the SDK uses to refresh). The SDK batches, retries, and spills offline for you — call `track`, not this.
auth: apiKeyPublishable | apiKeySecret · responses: 202, 401, 429
POST/v1/events/import
Bulk import historical events (server-side only)
Backfill historical events from a CSV or JSON payload using a secret key (server-side only, never a browser). Use it to seed an experiment from data you already collected; see the "Bring your own data" guide for the field mapping. Rows are independent: a malformed row is reported and skipped, never aborting the import. The response's `errors` name rows by their 1-based line in the payload **as submitted** — a CSV row spanning several physical lines (a quoted field containing newlines, which prompt and response text routinely do) reports the line it started on. Rows past the org's remaining monthly event allowance are reported the same way rather than silently dropped, so a half-landed backfill can be retried against exactly the rows that did not take. CSV is parsed per RFC 4180: fields may be quoted, `""` is an escaped quote, and a newline inside a quoted field is part of the value.
auth: apiKeySecret · responses: 200, 400, 429
GET/v1/config
Fetch the current experiment config (versioned, poll-friendly)
SDKs and the assignment service poll this on an interval using `If-None-Match`. An unchanged config returns `304`. The org is derived from the API key, never a query param.
auth: apiKeyPublishable | apiKeySecret · responses: 200, 304
GET/v1/experiments/{id}/results
Get computed results for an experiment
Returns the latest per-metric results: effect + 95% CI, raw and FDR-corrected p-values, the test used, the per-arm value distribution, and the SRM gate status. Accepts an optional `day` query parameter to read one run — an ISO date (nightly experiment-day) or a stored manual run's `manual-<ISO instant>` label; the response's `available_days` lists every day and permanently stored manual run with computed rows and `computed_for` names the served run. `GET /v1/experiments/{id}/traffic` (additive route) serves the daily exposure counts per variant for the traffic timeline. Responds `404` until the daily pipeline has produced results (the dashboard shows a "collecting" state meanwhile).
auth: clerkJwt · responses: 200, 404
GET/v1/keys
List the caller org's active keys (metadata only)
Returns each active key's scope, issue time, and whether it can be revealed — never the raw key itself; use the reveal route for that.
auth: clerkJwt · responses: 200
POST/v1/keys
Issue or rotate an org API key
Issues or rotates a `publishable` (safe client-side, write-only) or `secret` (server-side) org key. Rotating invalidates the previous key of that scope. Admin-only. The raw key is returned here and can be revealed again later via the reveal route.
auth: clerkJwt · responses: 201
GET/v1/keys/{scope}/reveal
Reveal the active key of a scope
Returns the raw active key of the scope — the dashboard's masked-field eye toggle. Admin-only and audit-logged. 404 when no key is active or the key predates raw-key storage (rotate to mint a revealable replacement).
auth: clerkJwt · responses: 200, 404
POST/v1/contact
Submit a marketing contact or design-partner message
The marketing site's form backend — the only unauthenticated `/v1` route. Abuse controls instead of auth: a honeypot field (`website` must stay empty; a filled one is acknowledged and silently dropped) and a per-IP hourly cap. Messages surface in the owner inbox.
auth: clerkJwt · responses: 202, 400, 429
GET/v1/admin/inbox
The platform-owner inbox (contact messages + all-org feedback)
Every marketing contact/design-partner submission plus every org's in-app feedback, newest first. Platform-owner only (`llmjury.platform.owner-emails`); everyone else gets 403 — which is also how the dashboard decides whether to show the Inbox nav item.
auth: clerkJwt · responses: 200, 403
POST/v1/business-events
Receive a business event via HMAC-signed webhook
Ingests a downstream business outcome (conversion, revenue, retention) via an HMAC-signed webhook, so post-experience metrics can be attributed back to the experiment and variant. Events count against the org's monthly event allowance exactly as `/v1/events` does.
auth: hmacSignature · responses: 202, 401, 429
POST/v1/webhooks/clerk
Clerk identity-sync webhook (Svix-signed)
Upserts user/organization/membership changes into LLMJury's Postgres (internal UUIDs with `clerk_*` external-reference columns; CONVENTIONS §11). The Svix signature is verified.
auth: svixSignature · responses: 204, 401
GET/v1/billing/summary
The caller org's plan and subscription state
Returns the org's current plan (`free`/`pro`/`business`/`enterprise`), its Stripe subscription status, and whether billing is available on this environment. An org that has never checked out is the free tier.
auth: clerkJwt · responses: 200
POST/v1/billing/checkout-session
Start a Stripe Checkout for a paid plan (admin-only)
Creates a Stripe-hosted Checkout Session for the `pro` or `business` subscription and returns its URL; the browser is redirected there, so no card data ever transits LLMJury. The completed purchase arrives back via the Stripe webhook.
auth: clerkJwt · responses: 200, 409
POST/v1/billing/portal-session
Open the Stripe Billing Portal (admin-only)
Creates a Stripe Billing Portal session (manage payment method, cancel, invoices) for an org that has completed a checkout, and returns its URL. 409 when no billing account exists.
auth: clerkJwt · responses: 200, 409
POST/v1/webhooks/stripe
Stripe billing webhook (signature-verified)
Applies subscription lifecycle events (`checkout.session.completed`, `customer.subscription.created/updated/deleted`) to the org's plan. The `Stripe-Signature` header is verified against the endpoint secret; processing is an idempotent upsert, so Stripe retries are safe. Other event types are acknowledged and ignored.
auth: stripeSignature · responses: 204, 401
GET/v1/experiments/{id}/versions
Every retained version of one experiment
Newest first. An experiment is versioned wholesale: any config change mints a new version, and this is the edit history the dashboard renders. Assignments stay sticky across versions (contracts/bucketing.md §4), so an older version's rows remain readable.
auth: clerkJwt · responses: 200, 404
POST/v1/experiments/{id}/analyze
Run a preliminary analysis now
Runs the same stages as the nightly pipeline (SRM gate → significance → FDR) over a window ending at the current instant. The result is stamped `preliminary` and stored permanently under a `manual-<ISO instant>` label, so it stays addressable in the day picker without ever suppressing the nightly run for that day. Repeated looks at partial data inflate the false positive rate; the dashboard says so wherever it shows one.
auth: clerkJwt · responses: 200, 404
POST/v1/experiments/{id}/test-event
Send one marked test event
Emits a single `exposure` for this experiment through the real ingest queue — the same path `POST /v1/events` publishes to — so a reader with no data can prove the pipeline works without writing code. The event is NOT excluded from analysis: it carries the reserved `llmjury-test-` user-id prefix and `metadata.llmjury_test` so it can always be identified, but nothing filters on those. What makes it safe is its shape — an exposure carries no metric values, so it moves no metric result; it does add one to the control arm's exposure count. Member-open, audit-logged, and limited to 5 per experiment per hour.
auth: clerkJwt · responses: 202, 404, 429
GET/v1/experiments/{id}/live
Live exposure counts and in-flight metric distributions
Uncorrected, unanalysed counts straight off the event store — what has arrived so far, not a result. Powers the live ticker and the in-flight histograms. Never use it to decide an experiment: it carries no p-values, no FDR correction, and no SRM gate.
auth: clerkJwt · responses: 200, 404
GET/v1/experiments/{id}/traffic
Daily exposure counts per variant
Per-day, per-variant exposures for the traffic timeline, oldest first.
auth: clerkJwt · responses: 200, 404
GET/v1/results/summary
Latest results for every experiment in the org
One entry per experiment that has computed rows — the same shape as `GET /v1/experiments/{id}/results`. Lets the experiment list render each row's verdict without a request per experiment.
auth: clerkJwt · responses: 200
GET/v1/audit
The org's audit trail
Every config mutation, key rotation, method override and plan change, newest first, with the acting user resolved to a name and email. Optionally filtered to one target id.
auth: clerkJwt · responses: 200
GET/v1/usage
This month's metered usage against the plan's allowances
Events and judge evaluations used so far this UTC calendar month, each against its included allowance, plus whether the plan bills overage and when the counters reset. `enforced` reports whether plan limits are being applied in this environment.
auth: clerkJwt · responses: 200
GET/v1/feedback
List feedback the caller org has submitted
auth: clerkJwt · responses: 200
POST/v1/feedback
Submit in-product feedback
auth: clerkJwt · responses: 201, 400
GET/v1/webhook-secrets
Whether a business-event webhook secret is configured
Status only — the secret itself is returned exactly once, when it is rotated.
auth: clerkJwt · responses: 200
POST/v1/webhook-secrets
Rotate the business-event webhook secret
Mints a new secret and returns it **once** — it is not retrievable afterwards. Rotating immediately invalidates the previous secret, so every sender must be updated. Requires an org admin or owner.
auth: clerkJwt · responses: 201, 403
GET/v1/whoami
The identity the caller's credential resolves to
The internal org and user ids the gateway resolved the credential to, plus the caller's role in that org. Useful for confirming an integration is authenticating as who it thinks it is.
auth: clerkJwt · responses: 200, 401
GET/v1/admin/overview
Platform-wide totals and traffic (platform owner only)
Platform-owner route, not part of the customer API. Gated on the caller's sign-in email matching `llmjury.platform.owner-emails` — a different axis from the OWNER/ADMIN roles *within* a customer org. Documented so the surface is known, not because customers call it.
auth: clerkJwt · responses: 200, 403
GET/v1/admin/orgs
Every org on the platform, with plan and usage (platform owner only)
Platform-owner route, not part of the customer API. Gated on the caller's sign-in email matching `llmjury.platform.owner-emails` — a different axis from the OWNER/ADMIN roles *within* a customer org. Documented so the surface is known, not because customers call it.
auth: clerkJwt · responses: 200, 403
GET/v1/admin/orgs/{id}
One org's plan, members and usage (platform owner only)
Platform-owner route, not part of the customer API. Gated on the caller's sign-in email matching `llmjury.platform.owner-emails` — a different axis from the OWNER/ADMIN roles *within* a customer org. Documented so the surface is known, not because customers call it.
auth: clerkJwt · responses: 200, 403, 404
PUT/v1/admin/orgs/{id}/plan-override
Pin an org's plan, overriding Stripe (platform owner only)
Platform-owner route, not part of the customer API. A sticky override: it wins over whatever the Stripe subscription says, so a later webhook cannot revoke it. Send a null plan to clear it and fall back to Stripe.
auth: clerkJwt · responses: 200, 403
POST/v1/admin/seats/resync
Push current per-plan seat caps to Clerk (platform owner only)
Platform-owner route, not part of the customer API. Seat caps reach Clerk on a plan change; changing the numbers themselves therefore needs this one-off, idempotent push to apply them to orgs already settled on their tier (CONVENTIONS §10).
auth: clerkJwt · responses: 200, 403
GET/v1/admin/health
Dependency and pipeline health (platform owner only)
Platform-owner route, not part of the customer API. Gated on the caller's sign-in email matching `llmjury.platform.owner-emails` — a different axis from the OWNER/ADMIN roles *within* a customer org. Documented so the surface is known, not because customers call it.
auth: clerkJwt · responses: 200, 403
Schemas
| Schema | Required fields | Description |
|---|---|---|
| Problem | — | RFC 9457 problem details (application/problem+json). |
| AllocationSlice | variant, weight | — |
| Experiment | — | A persisted experiment — its config plus server-set timestamps. |
| ExperimentConfig | name, salt, bucket_count, allocation, metrics | The versioned experiment config SDKs poll. Embeds the frozen bucketing inputs. |
| MetricDefinition | name, kind, category, method_default, metric_version, shared_org_wide | — |
| StatTest | — | — |
| Event | event_id, type, experiment_id, user_id, timestamp | See schemas/event.schema.json (authoritative). v1 stores raw prompt/response. |
| MetricResult | test, statistic, p_value, p_value_fdr, effect, ci_low, ci_high, n_per_arm, srm_p, permutations_used, sample_cap_used, window_days, retention_truncated, preliminary | See schemas/metric-result.schema.json (authoritative). |
| AssignRequest | experiment_id, user_id | — |
| AssignResponse | variant, config_version | — |
| IngestAck | accepted, config_version | — |
| ApiKey | scope, key, created_at | — |
| KeySummary | scope, created_at, revealable | — |
| BillingSummary | plan, has_billing_account, billing_configured | — |
| HostedUrl | url | — |
| PlanLimit | plan, retention_days, max_permutations, max_sample_per_arm, monthly_event_allowance | See schemas/plan-limits.schema.json (authoritative). |
| Results | — | One experiment's computed results, keyed `<metric>::<treatment>`. |
| LiveStats | — | Uncorrected counts straight off the event store — never a basis for a decision. |
| TrafficSeries | — | — |
| AuditEntry | — | — |
| UsageMeter | — | — |
| UsageSummary | — | — |
| Feedback | — | — |
| AdminOverview | — | — |
| AdminOrgSummary | — | — |
| Distribution | bin_edges, counts | A per-arm value histogram over shared bin edges, for visualizing how a metric is spread within each arm. Served both on a computed MetricResult and on the live, uncorrected `/v1/experiments/{id}/live` view. |
| ImportResult | events, errors | The outcome of a bulk import. Row-level, not all-or-nothing: `events` are the rows accepted and enqueued, and `errors` explains every row that was not, addressed by its line in the submitted payload. |
The full contract lives at contracts/openapi.yaml in the repository; this page is regenerated from it on every build.