All documentation
API
Push evidence in from your own CI, logs, or scripts with a per-system key: what it can and cannot do to your score, idempotency, and copy-paste examples.
Create a key on your system's API keys page — it is shown exactly once — and send this.
Response — 201 because something was appended:
More detail
What ingested evidence can and cannot do
Ingested evidence can satisfy the checks that are decided by evidence. It can never satisfy the ones that are decided by a person. That is not a caveat — it is the design, stated as a commitment rather than an apology. No other product in this space draws the line at all.
Concretely: this API can write five kinds of evidence — trace_batch, usage_stat, eval_result, commit_ref, config_snapshot. Of those, exactly four requirements in the shipped pack are decided automatically from evidence you ingest, and this is the complete list:
disclosure_in_tracesArt. 50(1). Fed bytrace_batch— 50 sampled conversation traces whose first message discloses that the user is talking to an AI system.percall_log_continuityAnnex IV / Art. 12 logging. Fed bytrace_batch— continuous per-call logs covering at least 99% of a rolling period.model_inventoryAnnex IV(2). Fed byconfig_snapshot— the model provider is recorded against the current system version.dev_process_refsAnnex IV(2). Fed bycommit_ref— commits linked to the current system version.
usage_stat and eval_result are real evidence too, and appear in your record and your documents, but neither currently moves a requirement by itself — the same honest gap the OpenAI and Anthropic connectors already document for usage totals.
An attestation can never be written through this API, by design and permanently. An attestation is a person making a statement about their system; a script making one would be the product manufacturing evidence about itself. There is no plan, scope, or configuration that unlocks this — if you need programmatic attestation, the honest answer is that a person has to make the statement. doc_upload is excluded too, for an unrelated and much less interesting reason: it needs a presign-upload-confirm round trip this single-`POST` API does not do. See Evidence for what a human attestation actually records.
Idempotency: a retry is a no-op, not a duplicate
Every event you send carries an eventId you choose — required, not optional. Send the same (key, eventId) pair twice and the second call appends nothing: you get back the status of the original event, including its original sequence number, at 200 instead of 201.
This is the opposite of the bulk-import behaviour on the Connectors page, where re-importing the same file appends every row again — deliberately, and for a reason that does not apply here. A human choosing to re-upload a file has made a choice; duplicating on that choice is the safe direction to be wrong in. A webhook retry is not a choice — it is a timeout, and the sender usually cannot tell whether the first attempt landed. Making a retry safe is what makes this API usable by something that never chooses to send an event twice.
One key per system, shown once, revoke instead of rotate
A key is bound to exactly one AI system at the moment you create it, and that binding cannot change. The honest trade this creates: if you run five AI systems and want one pipeline pushing evidence into all five, you need five keys and five secrets in your CI. That is deliberate — a leaked key only ever costs you the one system it was bound to.
The full key is shown exactly once, on your system's API keys page, at the moment you create it. It is never shown again by any surface, including this one — Clause50 stores only a one-way hash of it, so there is nothing to show even if we wanted to. If a key is compromised or simply no longer needed, revoke it; there is no rotation flow, because create-a-new-one-then-revoke-the-old-one already is rotation. A revoked key stays visible in your key list, greyed out with the date it was revoked — it is never deleted, because its id still appears in the provenance of every evidence item it ever wrote, and that record can only be added to.
Four copy-paste examples
The curl example above is the first of these four. Each is under 15 lines, reads the key from an environment variable, and never prints it.
GitHub Actions — reporting a deploy from a workflow step, using a repository secret:
Node — a log shipper reporting a batch of sampled traces:
Python — the same batch, from a Python-based shipper:
Limits, rate limits, and status codes
Per request, without drama:
- Up to 100 events per requestEach event's
payloadup to 64 KiB, and the whole request body up to 1 MiB.eventIdup to 200 characters. - 120 requests/minute, 50,000 events/day, per keyOver either limit, every request gets
429with aRetry-After: 60header and writes nothing.
What each status code means:
201At least one event in the batch was newly appended.200Every event in the batch was already known — the whole call was a retry.401Missing, malformed, unknown, or revoked key — deliberately one identical message for all four, so a leaked key teaches whoever holds it nothing.422The batch failed validation — a badkind, a missingeventId, a cap exceeded, or an unknown field. Nothing in the batch is appended, even if only one event was the problem.429Rate or daily budget exceeded. See above.
recordedAt is our clock; occurredAt is yours. The row's recordedAt is always the moment Clause50 received the event — you cannot set it, and a request that tries to is a 422. If you know when the thing actually happened, send it as occurredAt; it is kept as metadata alongside your evidence, but it can never move you into a satisfied window retroactively. This is deliberate: a caller-settable clock would let a client backdate itself into compliance.
Not sure a key is wired up correctly? GET /api/ingest/v1/whoami with the same bearer token returns exactly what it is bound to — the system, the allowed kinds, and these same limits — and nothing about your evidence, coverage, or score.
No code access to your AI system? Tap the traffic instead
Everything above assumes you can add a line of code somewhere in your own pipeline. If you can't — the system sits behind an API gateway or edge proxy you don't own the application code for — you can still get evidence into Clause50 by shaping traffic you already control into this same endpoint. All three patterns below end in the identical POST /api/ingest/v1/evidence call above; Clause50 never runs, hosts, or holds credentials for any of the infrastructure described here.
Reverse-proxy / edge-worker: a copy-paste starting point
A worker that sits in front of your own LLM-provider endpoint or backend, passes the request/response through completely unchanged, and — in the background, never on the request path — extracts a trace_batch or config_snapshot and reports it. Zero added latency on the path your users actually feel.
docs/examples/reverse-proxy-worker.js — a full Cloudflare Worker, committed to this repo as a reference file. Clause50 never deploys or runs it; copy it into your own Cloudflare account (or the equivalent on any edge platform) and point it at your ingest key.
Can't run Cloudflare Workers? The same shape works from an nginx/OpenResty location block — this is a sketch to adapt, not a runnable file:
This worker (and any adaptation of it) must only ever emit the five machine-safe kinds — trace_batch, usage_stat, eval_result, commit_ref, config_snapshot. Never forward a field that could read as a human attestation (a "reviewed by" or "approved" flag) — that is a hard rule, not a suggestion, for the same reason this API can never write an attestation directly.
API Gateway tap
If your traffic already passes through an API gateway (Kong, Envoy, Apigee, AWS API Gateway, …), its own async-logging or webhook configuration can forward matched requests to the same endpoint — no code, no reference file to maintain, because every gateway's configuration language is different and yours already knows how to adapt a documented target shape.
Kong — an http-log plugin (or a small transform plugin ahead of it, since Kong's stock log format is not the ingest envelope) pointed at the endpoint:
Envoy — an access-log service or tap filter forwarding matched requests through a small translator that reshapes Envoy's log format into the ingest envelope:
The same hard rule applies here as above: a gateway tap may only ever produce one of the five machine-safe kinds, never an attestation.
CMS publish hook (WordPress)
Publishing AI-generated content through a CMS? Report the publish event after it happens, rather than trying to gate it live — there is no "is this covered yet?" check to call before you publish, and out-of-band reporting is the same pattern as everything else on this page.
docs/examples/wordpress-publish-hook.php — a WordPress transition_post_status hook, committed to this repo as a reference file. Clause50 never installs or runs it; copy it into your own theme or a small custom plugin and point it at your ingest key.
Metadata only, never post content: content type, the AI system the post is bound to, whether a disclosure was shown, and the publish time. The hook never sends a title, body, or excerpt — the same machine-safe-kinds rule as the reverse-proxy worker above, applied here because post content is very often end-user-facing data that has no reason to leave your own site.
OpenTelemetry: planned, not yet supported
OTel support: planned, tracking upstream semantic-convention stability. The GenAI semantic conventions (gen_ai.* span/event attributes) are still stabilizing. Building a translator against them now risks a rewrite the first time an attribute name changes, so this stays a placeholder rather than a mapping likely to break silently. If you already run an OTel Collector, the reverse-proxy pattern above is the nearest fit today.