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 summary
SchemaRequired fieldsDescription
ProblemRFC 9457 problem details (application/problem+json).
AllocationSlicevariant, weight
ExperimentA persisted experiment — its config plus server-set timestamps.
ExperimentConfigname, salt, bucket_count, allocation, metricsThe versioned experiment config SDKs poll. Embeds the frozen bucketing inputs.
MetricDefinitionname, kind, category, method_default, metric_version, shared_org_wide
StatTest
Eventevent_id, type, experiment_id, user_id, timestampSee schemas/event.schema.json (authoritative). v1 stores raw prompt/response.
MetricResulttest, 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, preliminarySee schemas/metric-result.schema.json (authoritative).
AssignRequestexperiment_id, user_id
AssignResponsevariant, config_version
IngestAckaccepted, config_version
ApiKeyscope, key, created_at
KeySummaryscope, created_at, revealable
BillingSummaryplan, has_billing_account, billing_configured
HostedUrlurl
PlanLimitplan, retention_days, max_permutations, max_sample_per_arm, monthly_event_allowanceSee schemas/plan-limits.schema.json (authoritative).
ResultsOne experiment's computed results, keyed `<metric>::<treatment>`.
LiveStatsUncorrected counts straight off the event store — never a basis for a decision.
TrafficSeries
AuditEntry
UsageMeter
UsageSummary
Feedback
AdminOverview
AdminOrgSummary
Distributionbin_edges, countsA 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.
ImportResultevents, errorsThe 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.