Developers

Three calls: verify, check reliance, fetch the proof.

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

Submit a response. Get a record of what it rests on.

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.

POST /v1/organizations/{org}/verify
Submit a response, with the question and any sources it cited. 202 with the id and a status URL.
GET …/verifications/{id}
The record: queued, running, done or failed, then every claim, its grade, and the evidence.
GET …/verifications/{id}/evidence/{index}
The full text of one evidence snapshot, as it was when it was read.
POST …/reliance/check
May this record be used for an operation: allow, review or deny, per claim and overall.
POST …/respond
Ask the organization's own responder agent; the answer comes back ready to verify.
GET …/verifications/{id}/attestations
The signed attestation chain for the record.
GET …/verifications/{id}/export
One zip: record, evidence, Merkle proof, signature, and how to check them offline.
GET /v1/attest/{code}
Look up an attestation by its code. Public, no key.

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

The reliance check is the enforcement point.

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

Be told when it is done, and be able to prove who told you.

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

Two lines in Python or TypeScript.

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 same thing from a shell, built for pipes and gates.

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.

verify
--file, --text, or - for standard input. Waits by default; --no-wait prints the id.
get · wait
Fetch a record, or wait for one.
may
The reliance decision for an operation. Exit 0 only for allow.
attest
The attestation chain, or --export the bundle.
respond
Ask the organization's responder, optionally --grounded.
deliveries · webhook-verify
What was sent for a record, and a check of a captured delivery against a secret.
$ 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

Let your agent ask where a claim stands.

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

Every refusal says why.

401 · 403

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"}

402 Payment required

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.

409 Conflict

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.

422 Unprocessable

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.

429 Too many requests

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.

A failed run is a record

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

What the API promises.

One finding per claim

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.

Signed webhooks

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.

Refusal is a state

A timeout, a provider error, or an open circuit breaker returns REFUSED with the reason. It never returns a guess.

Versions in the record

Hierarchy, policy, prompts, presets, and model identifiers are pinned in every record. Change any of them and old records still say what they said.

Evidence as it was read

Every source consulted is snapshotted and hashed at the time. The record shows what the page said then, not what it says today.

Proof you can check offline

Finished records are attested: a Merkle tree over the claims, signed by a key in AWS KMS. The export bundle verifies without calling us.

Get a key and a stack.