Developers
Use TruthLock from its own console, or build it in: a REST API, Python and TypeScript SDKs, a command line tool, and an MCP server for agents. Every example on this page is one real run. A draft answer says a workplace fatality must be reported to OSHA within 24 hours. The regulation says eight. Here is what each way in gives you back.
REST API · Reliance · Webhooks · SDKs · CLI · MCP · Errors and limits
REST API
Verification is asynchronous because it takes seconds to a minute or two; this run took twelve seconds. Submit, receive an id, then poll the status URL or let a webhook tell you. Authenticate with Authorization: Bearer tl_… or X-API-Key. Keys are made under Advanced → API keys; one limited to the verify role can do everything on this page.
queued, running, done or failed, then every claim, its grade, and the evidence.Grades, worst first: CONTRADICTED (a ranked source says otherwise), REFUSED (it could not be checked, and says why), UNCONFIRMED (no adequate source), SUPPORTED, VERIFIED. The overall grade is the worst among the claims. Configuration is CRUD under /v1 for organizations, hierarchies, connectors, claim types, presets, agents, reliance policies and API keys. Every stack serves its own reference at /docs and /openapi.json.
Submit
curl https://app.truthlock.io/v1/organizations/field-safety/verify \
-H "Authorization: Bearer $TRUTHLOCK_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"question": "How quickly must an employer report a workplace fatality to OSHA?",
"response": "Employers must report a work-related fatality to OSHA within 24 hours. An inpatient hospitalization must be reported within 24 hours as well.",
"external_id": "ticket-48213"
}'
202 Accepted
{
"id": "2dd37a1f-12b0-4174-b1a7-38844eba3b14",
"status": "queued",
"status_url": "https://app.truthlock.io/v1/organizations/field-safety/verifications/2dd37a1f-12b0-4174-b1a7-38844eba3b14"
}
The record, abridged
GET /v1/organizations/field-safety/verifications/2dd37a1f-12b0-4174-b1a7-38844eba3b14
{
"id": "2dd37a1f-12b0-4174-b1a7-38844eba3b14",
"status": "done",
"external_id": "ticket-48213",
"overall": { "grade": "CONTRADICTED", "scored": 2, "verified": 1,
"counts": { "VERIFIED": 1, "CONTRADICTED": 1 } },
"claims": [
{ "claim_id": 1,
"claim": "Employers must report a work-related fatality to OSHA within 24 hours.",
"grade": "CONTRADICTED", "reason_code": "quantitative",
"explanation": "29 CFR 1904.39(a) states a work-related fatality must be reported to OSHA within eight (8) hours, not 24 hours; the 24-hour window applies to in-patient hospitalizations, amputations, and loss of an eye.",
"evidence_indexes": [3, 1, 7, 8, 9, 10] },
{ "claim_id": 2,
"claim": "An inpatient hospitalization must be reported within 24 hours.",
"grade": "VERIFIED", "reason_code": null,
"evidence_indexes": [1, 2, 3, 4, 5, 6] }
],
"evidence": [
{ "index": 1, "tier_rank": 1, "source_slug": "us-federal-agencies",
"title": "1904.39 - Reporting fatalities, hospitalizations, amputations…",
"locator": "http://www.osha.gov/laws-regs/regulations/standardnumber/1904/1904.39" },
…
],
"versions": { … }, "timings_ms": { "extract": 1683, "gather": 5007, "grade": 8076 },
"cost_usd": 0.204404
}
One claim wrong, one right, each tied to the tier-one source it was checked against. The second claim is the same regulation read correctly, which is why a single "this answer looks fine" score would have missed the first.
Ask before you act
Your application does not interpret grades. It names the operation it is about to perform and receives a decision. The operations and what each grade means for them are your organization's policy, set in the console; the defaults are display, quote and act. allow is the only yes. review means a person looks first, and their adjudication is recorded and counted the next time you ask.
Pass "apply": "mark" or "redact" and the response comes back with the failing claims marked or removed, so there is something you can show.
POST /v1/organizations/field-safety/reliance/check
{ "verification_id": "2dd37a1f-12b0-4174-b1a7-38844eba3b14", "operation": "display" }
200 OK
{
"operation": "display",
"decision": "deny",
"counts": { "allow": 1, "review": 0, "deny": 1 },
"claims": [
{ "claim_id": 1, "grade": "CONTRADICTED", "decision": "deny",
"reason": "CONTRADICTED → deny under 'display'" },
{ "claim_id": 2, "grade": "VERIFIED", "decision": "allow",
"reason": "VERIFIED → allow under 'display'" }
],
"policy_version": 1
}
Signed webhooks
Give an API key a webhook under Advanced → API keys → Webhook…, or with one call. You are shown the signing secret once. Every verification that key submits then announces itself when it finishes, verification.completed or verification.failed. A single run can name its own destination with "webhook_url" in the submit body.
PUT /v1/api-keys/{key_id}/webhook
{ "url": "https://example.com/hooks/truthlock" }
200 OK
{ "webhook_url": "https://example.com/hooks/truthlock",
"has_webhook_secret": true,
"secret": "whsec_…" } ← shown once
The signature is an HMAC-SHA256 of <t>.<raw body> under that key's secret. The timestamp is inside what is signed, so a captured event cannot be replayed with a fresh one. Verify against the raw bytes, before parsing. The SDKs do it in one call, and refuse an altered body, another key's secret, and anything older than five minutes.
Answer with any 2xx, quickly. Anything else is retried eight times over about a day, and every attempt, with what your server answered, is readable at GET …/verifications/{id}/webhook-deliveries. A delivery can arrive twice; its id is the same each time. Webhook addresses must be public HTTPS, and redirects are not followed.
What arrives
POST https://example.com/hooks/truthlock
TruthLock-Signature: t=1789940304,v1=83a06f4950d8096dd7b926a0150f30009f2f1e9c…
TruthLock-Event: verification.completed
TruthLock-Delivery: 5c0bf873-9ed1-418f-870e-b9211ea4c8cc
{
"id": "5c0bf873-9ed1-418f-870e-b9211ea4c8cc",
"type": "verification.completed",
"created": 1789940304,
"data": {
"id": "2dd37a1f-12b0-4174-b1a7-38844eba3b14",
"organization": "field-safety",
"status": "done",
"external_id": "ticket-48213",
"overall": { "grade": "CONTRADICTED", "scored": 2, "verified": 1 },
"reliance": { "display": "deny", "quote": "deny", "act": "deny" },
"error": null,
"url": "https://app.truthlock.io/v1/organizations/field-safety/verifications/2dd37a1f-12b0-4174-b1a7-38844eba3b14"
}
}
Receiving it
from truthlock_io import parse_webhook, WebhookSignatureError
@app.post("/hooks/truthlock")
async def hook(request):
try:
event = parse_webhook(await request.body(),
request.headers.get("TruthLock-Signature"),
WEBHOOK_SECRET)
except WebhookSignatureError:
return Response(status_code=400) # altered, wrong secret, or a replay
if event.completed and event.grade == "CONTRADICTED":
hold(event.data["external_id"])
return Response(status_code=204)
SDKs
Python
# pip install truthlock-io
from truthlock_io import TruthLock
tl = TruthLock(api_key="tl_…", organization="field-safety")
record = tl.verify_and_wait(
question="How quickly must an employer report a workplace fatality to OSHA?",
response=answer_text,
)
if tl.may(record, "display"):
show(answer_text)
else:
print(record.grade) # CONTRADICTED
print(record.caveats())
hold(record.review_ids()) # [1]
Prints
CONTRADICTED
['Employers must report a work-related fatality to OSHA within 24 hours.
[CONTRADICTED: 29 CFR 1904.39(a) states a work-related fatality must be
reported to OSHA within eight (8) hours, not 24 hours; …]']
TypeScript
// npm install @truthlock-io/sdk
import { TruthLock, parseWebhook } from "@truthlock-io/sdk";
const tl = new TruthLock({ organization: "field-safety" }); // key from TRUTHLOCK_API_KEY
const record = await tl.verifyAndWait({ question, response: answerText });
if (await tl.may(record, "display")) show(answerText);
else hold(record.reviewIds(), record.caveats());
// in your webhook route, against the raw body:
const event = await parseWebhook(await request.text(),
request.headers.get("TruthLock-Signature"), process.env.TRUTHLOCK_WEBHOOK_SECRET);
Both wrap submit, wait with backoff, reliance checks, the responder, attestations, the export bundle, and webhook verification. TypeScript has no dependencies and runs on Node 18+, Deno, Bun and the edge runtimes. Errors are typed, so you can tell a spent allowance (PaymentRequired) from a burst (RateLimited, with retry_after) from a bad key.
The packages are truthlock-io on PyPI and @truthlock-io/sdk on npm. Similarly named packages without the -io belong to a different company and do not talk to this service. Both are published: truthlock-io on PyPI and @truthlock-io/sdk on npm, under the Apache 2.0 license.
Command line
The truthlock command comes with the Python package. Every command takes --json. Exit codes are the interface: 0 for done or allowed, 1 for a finding that says no, 2 for an error. So truthlock may … && publish publishes only what your policy allows, in a CI job or a cron script, with no code.
--file, --text, or - for standard input. Waits by default; --no-wait prints the id.--export the bundle.--grounded.$ export TRUTHLOCK_API_KEY=tl_… TRUTHLOCK_ORGANIZATION=field-safety
$ truthlock verify --file answer.txt --question "How quickly must an employer report…"
2dd37a1f-12b0-4174-b1a7-38844eba3b14 CONTRADICTED (2 claims)
- Employers must report a work-related fatality to OSHA within 24 hours.
[CONTRADICTED: 29 CFR 1904.39(a) states a work-related fatality must be reported
to OSHA within eight (8) hours, not 24 hours; …]
$ truthlock verify --file answer.txt --json | jq -r .overall.grade
CONTRADICTED
$ truthlock may 2dd37a1f… display && publish.sh
deny claims 1 # exit 1, so publish.sh never runs
$ truthlock attest 2dd37a1f… --export bundle.zip
bundle.zip
MCP server
Every stack serves a remote Model Context Protocol server at /mcp, over streamable HTTP. An agent in Claude, ChatGPT, Claude Code, Cursor, or your own harness gets five tools: verify a draft against your hierarchy, fast_check it in about ten seconds, get_verification, reliance_check, and ask your organization's responder. It can check its own answer, see which claim failed and against what, and correct it before anyone reads it.
# Claude or ChatGPT: Settings → Connectors → add this address, then sign in when asked
https://app.truthlock.io/mcp
# Claude Code, with sign-in through the browser
$ claude mcp add --transport http truthlock https://app.truthlock.io/mcp
# or any client, with a key
$ claude mcp add --transport http truthlock https://app.truthlock.io/mcp \
--header "Authorization: Bearer tl_…"
The server is an OAuth 2.1 authorization server as well: a client that can open a browser sends the person to TruthLock to sign in and choose organizations and a role, and what it receives is an API key owned by that person, listed under Advanced → API keys and revocable there. The agent is governed by exactly what governs the API, because the tools call it with that key: its organization scope, your plan's allowance, the rate limit, your reliance policy. A program that cannot open a browser uses an ordinary API key instead: make one under Advanced → API keys → New, give it the verify role, scope it to the one organization the agent should work in, and pass it as a header. What comes back is sized for a context window: each claim, its grade and reason, and the sources it rests on, without the evidence text.
What the agent sees
verify(response="Employers must report a work-related fatality to OSHA within 24 hours. …")
{
"verification_id": "2dd37a1f-12b0-4174-b1a7-38844eba3b14",
"organization": "field-safety",
"status": "done",
"overall_grade": "CONTRADICTED",
"do_not_present_as_fact": [
"Employers must report a work-related fatality to OSHA within 24 hours."
],
"claims": [
{ "claim_id": 1, "grade": "CONTRADICTED", "reason": "quantitative",
"claim": "Employers must report a work-related fatality to OSHA within 24 hours.",
"explanation": "29 CFR 1904.39(a) states … within eight (8) hours, not 24 hours; …",
"sources": [ { "title": "Report a Fatality or Severe Injury",
"locator": "http://www.osha.gov/report",
"source": "us-federal-agencies" }, … ] },
{ "claim_id": 2, "grade": "VERIFIED", … }
],
"reliance": { "display": "deny", "quote": "deny", "act": "deny" }
}
Errors and limits
No key, a wrong one, a paused or revoked one, or a key whose role or organization scope does not cover the call. {"detail": "sign in or send an API key"}
The plan's monthly allowance is spent, or an invoice is past its grace period. The message says which, and when the allowance resets. Reads keep working.
The thing is not in a state for that: a reliance check on a record that is not done, a webhook URL on a key with no secret yet. The message names the call that fixes it.
The body is wrong: an operation your policy does not define (the message lists the ones it does), a webhook address that is not public HTTPS.
One company may start 120 verifications or answers a minute. Retry-After says how many seconds to wait. The SDKs surface it as retry_after.
If a provider is down or a stage times out, the verification ends failed with the reason, is not charged, and does not count against an allowance. It never returns a guess.
Guarantees
Never a single score for the answer. Each claim carries its own grade, reason code, explanation, and the evidence it was checked against, ranked by your hierarchy.
Each API key has its own signing secret. Events carry an HMAC-SHA256 over the timestamp and the raw body, so tampered or replayed payloads are rejected. A receiver that is down is retried for about a day, and every attempt is on the record.
A timeout, a provider error, or an open circuit breaker returns REFUSED with the reason. It never returns a guess.
Hierarchy, policy, prompts, presets, and model identifiers are pinned in every record. Change any of them and old records still say what they said.
Every source consulted is snapshotted and hashed at the time. The record shows what the page said then, not what it says today.
Finished records are attested: a Merkle tree over the claims, signed by a key in AWS KMS. The export bundle verifies without calling us.