An API that checks whether an AI answer is supported by the sources you gave it.
Send the question, your model’s answer, and the passages your retriever returned. Verify splits the answer into atomic claims, checks each one against the passages, and returns a verdict per claim with the exact quoted span, a release recommendation for the whole answer, and an evidence-ledger record.
We don’t vote. We verify spans. A model may propose that a passage supports a claim; code decides. A claim is supported only when code finds the quoted text in your source and every material qualifier the source attaches (conditions, eligibility, dates, limits, scope) survives in the claim. Model agreement is a signal, never a verdict.
Verdicts describe whether the supplied passages support each claim. They do not establish whether any claim is true in the world. “Supported” means supported by the supplied evidence, not true in the world.
Quickstart (under five minutes)
- Create a key at /account/api-keys. It is shown once. Free: 1,000 checks a month, no card.
- Send one request:
curl https://xyntherium.com/v1/verify \
-H "Authorization: Bearer $XYN_API_KEY" \
-H "Content-Type: application/json" \
-H "Idempotency-Key: 6f1c2a" \
-d '{
"question": "Can I get a refund on my plan?",
"answer": "Yes. You can get a full refund within 30 days of purchase.",
"sources": [{
"id": "kb-refunds",
"title": "Refund policy",
"text": "Annual plans can be refunded in full within 30 days of purchase. Monthly plans are not refundable."
}]
}'- Read
release_recommendation:allow,cite_required,reviseorhuman_review. Ship only onallow. Every claim carries the evidence behind its verdict.
Try it without a key in the free demo.
TypeScript and Python
Gate an answer before it is sent. The SDK (@xyntherium/verify-sdk) is a thin client over this endpoint; so are the MCP server and the GitHub Action.
import { Xyntherium } from '@xyntherium/verify-sdk';
const xyn = new Xyntherium({ apiKey: process.env.XYN_API_KEY! });
const result = await xyn.verify({
question,
answer: draft, // what your model generated
sources: retrieved.map((d) => ({ id: d.id, title: d.title, text: d.text })),
});
if (result.release_recommendation === 'allow') {
send(draft);
} else {
// result.claims[i].verdict, .failure_reason, .provenance.spans (exact offsets)
escalate(result);
}import os, requests
r = requests.post(
"https://xyntherium.com/v1/verify",
headers={"Authorization": f"Bearer {os.environ['XYN_API_KEY']}"},
json={"question": question, "answer": draft, "sources": [
{"id": d["id"], "text": d["text"]} for d in retrieved
]},
timeout=120,
)
r.raise_for_status()
result = r.json()
if result["release_recommendation"] == "allow":
send(draft)
else:
for c in result["claims"]:
if c["verdict"] != "supported":
print(c["verdict"], c["failure_reason"], c["text"])Verdict contract — schema 2026-10-15
- supported
- A code-verified quoted span supports the claim AND every material qualifier in the source context is preserved in the claim.
- contradicted
- A code-verified span conflicts with the claim: a different number, date, entity, negation or condition.
- unsupported
- No adequate support in the supplied passages — including support that exists only once a material qualifier is dropped (
failure_reason: "qualifier_loss"). - unverifiable
- The passages cannot resolve the claim under these rules: comparisons with things they do not describe, predictions and judgements, sources that conflict with nothing to say which governs, or a check that could not run (with the reason).
Spans are located in code: exact match first, then a normalisation layer for whitespace, quote style and punctuation only, with a hard edit-distance cap. Offsets always index your original source text. A quote a model proposes that is not in the source is discarded and counted in discarded_quotes. Numbers, dates, unit conversions and arithmetic are recomputed in code. Deterministic checks run first, so many claims are resolved by code alone (resolved_by: "code").
| failure_reason | meaning |
|---|---|
| qualifier_loss | Drops a condition or limit that the source attaches |
| no_supporting_span | No passage says this |
| unsupported_detail | States a detail the cited passage does not |
| unverified_contradiction | A conflict was suggested, but no passage could be quoted for it |
| number_mismatch | Gives a different number from the source |
| date_mismatch | Gives a different date from the source |
| entity_mismatch | Attributes this to a different name, plan or group |
| negation | The source says the opposite |
| condition_mismatch | Conflicts with a condition in the source |
| arithmetic_error | The arithmetic does not add up |
| superseded | Follows an older passage that a newer one replaces |
| comparative | Compares against things your sources do not describe |
| out_of_scope | A prediction or judgement your sources cannot settle |
| conflicting_sources | Your sources disagree on this |
| model_abstained | Could not be resolved from these passages |
| cost_ceiling | Not checked: this request reached its cost ceiling |
| deadline | Not checked: this request reached its time limit |
| provider_error | Not checked: a model provider was unavailable |
| deliberation_disagreement | Reviewers disagreed, so it is left for a person |
| not_adjudicated | Needs a model to check; none was used |
Release recommendation rules
Deterministic, in this order of precedence:
revise— any material claim contradicted, any qualifier loss, or more unsupported material claims thanoptions.thresholds.max_unsupported(default 0).human_review— results are partial, or more unverifiable material claims thanoptions.thresholds.max_unverifiable(default 0).cite_required— everything is supported, but you setoptions.require_citationsand a supported claim lacks a citation marker ([1],[kb-12]) pointing at a passage that supports it.allow— every material claim is supported (and cited, when required).
A known problem outranks an unknown one: a partial result containing a contradiction is revise, with the partial reason listed.
Response
{
"schema_version": "2026-10-15",
"request_id": "0b6e…",
"evidence_ledger_id": "5d1f…",
"status": "completed",
"overall_status": "issues_found",
"release_recommendation": "revise",
"release_reasons": ["1 claim(s) drop a material qualifier stated in the passages"],
"counts": { "supported": 0, "contradicted": 0, "unsupported": 1, "unverifiable": 0, "total": 1, "resolved_by_code": 0 },
"claims": [{
"index": 0,
"text": "You can get a full refund within 30 days of purchase.",
"verdict": "unsupported",
"failure_reason": "qualifier_loss",
"resolved_by": "model",
"calibrated_confidence": null,
"answer_start": 5, "answer_end": 57,
"provenance": {
"spans": [{ "source_id": "kb-refunds", "start": 0, "end": 63,
"quote": "Annual plans can be refunded in full within 30 days of purchase",
"match_type": "exact", "role": "qualifies", "source_hash": "9a3c…" }],
"qualifiers": [{ "type": "scope", "claim_text": null, "source_text": "Annual plans",
"status": "dropped", "material": true }],
"checks": [{ "kind": "qualifiers", "result": "fail", "detail": "dropped: Annual plans" }],
"rule_id": "R-MODEL-QUALIFIER-LOSS",
"model_used": true, "models": ["claude-haiku-5-5"], "prompt_versions": ["verify-adjudicate-2026-10-15.1"]
}
}],
"material_unsupported_claims": [0],
"qualifier_loss_detected": true,
"billable_units": 1,
"provider_cost_breakdown": [{ "provider": "anthropic", "model": "claude-haiku-5-5", "calls": 2, "cost_cents": 0.07 }],
"disclaimer": "Verdicts describe whether the supplied passages support each claim. They do not establish whether any claim is true in the world."
}overall_status is all_supported, issues_found, insufficient_sources or partial. calibrated_confidence stays null until the benchmark calibration is validated on its held-out split; once set, it is labelled “calibrated on benchmark vX” and is a measured hit rate on labelled items, never a probability of truth. GET /v1/verify/{id} returns a stored result.
Idempotency
Send Idempotency-Key (1–255 chars). It is enforced in storage: a repeat with the same key and body returns the stored result with Idempotent-Replayed: true — no re-run, no second charge. The same key with a different body is a 422. A request still running is a 409 with Retry-After.
Evidence ledger
Every result appends an entry to an append-only, hash-chained ledger for your account. The entry commits to hashes of the question, answer and each source, every claim’s verdict, rule and span offsets — never the text. GET /v1/ledger/{evidence_ledger_id} returns the entry with its checks recomputed.
payload_hash = sha256(canonical_json(payload))
entry_hash = sha256((prev_hash or "GENESIS") + "|" + payload_hash + "|" + seq + "|" + request_id)Limits and errors
- Text only. Sources are passages;
urlis metadata and is never fetched. PDF input returns 415. - Question ≤ 2,000 chars · answer ≤ 20,000 · up to 25 sources · each ≤ 100,000 chars · all sources ≤ 400,000 · body ≤ 1 MB.
- One check covers an answer of up to 25 claims; longer answers count as more checks. Partial and failed requests are never billed.
- Free: 1,000 checks a month, 30 requests a minute per account, content kept 7 days, ledger 30 days.
- Builder ($39/month, about 8,000 checks) and Team ($199/month, about 40,000 checks) continue past the allowance at $5 per 1,000 checks. API, MCP and hosted Gate checks share one allowance;
GET /v1/usageshows the split. Pricing - Question, answer and source text are stored encrypted for the retention window only, and never written to logs.
| 400 | bad_request | Malformed JSON, missing question/answer/sources, duplicate source ids, or a bad Idempotency-Key. |
| 401 | unauthorized / invalid_key | No key, or a key we do not recognise. |
| 403 | key_revoked / key_expired | The key was revoked or has passed its expiry date. |
| 409 | idempotency_in_progress | A request with this Idempotency-Key is still running. Retry in a few seconds. |
| 413 | payload_too_large | Over a size limit (below). |
| 415 | unsupported_media_type | PDF or multipart input. v1 is text only; PDF input is on the roadmap. |
| 422 | unprocessable / idempotency_conflict | A source has a url but no text (we never fetch URLs), or an Idempotency-Key reused with a different body. |
| 429 | rate_limited / quota_exceeded | Per-key or per-account rate limit, or the monthly allowance is used up. Retry-After is set. |
| 500 | server_error | Verification failed. Not billed. |
| 503 | service_unavailable | We could not record the request or its result. Not billed; retry with a new Idempotency-Key. |
Schema versioning
Every response carries schema_version and the X-Xyntherium-Schema-Version header (currently 2026-10-15). Adding a field or a new failure_reason value is not breaking. Changing what a verdict or a release recommendation means is breaking and requires a new major schema version; the previous version keeps being served alongside it.
The deterministic core is open source (@xyntherium/verify-core, MIT). See the benchmark for measured error rates, including where Verify loses, and the rules changelog for every change to how verdicts are decided (versions.rules in each response names the version).
