Developer docs
Check any company from your own code.
One GET returns the witnessed record for any company on the Network, no key, no account. Every example below runs against the live API at api.trooth.co.
In one lineOne witnessed record, three ways to read it: a single API call, the CLI, and the embeddable badge.
# No key, no account. The public profile is open.
curl -s https://api.trooth.co/public/trust/your-co
{ "profile": { "displayName": "Your Co", "domain": "your-co.com" } }
# Or read the same record from a terminal.
npx trooth check your-co.com
Reference
Everything on this page
| Resource | Endpoint | Access |
|---|---|---|
| Agents & MCP | POST /public/mcp | Public |
| Fill your profile with your own AI | WebMCP · 4 tools | Signed in |
| Verifiable evidence | tlt2 · VC crosswalk | Public |
| Agentic commerce | position | Public |
| Public Trust Profile API | GET /public/trust/:slug | Public |
| Trust Badge | trooth.co/badge.js | Public embed |
| CLI | npx trooth check <domain> | Public, no key |
| Webhooks | POST · outbound | Signed |
Start here
Quickstart
The public Trust Profile API needs no key or account. It returns what Trooth observed about a company, with the source and the date on each line: the signed, witnessed record, fetched by slug. Authenticated endpoints (your dashboard, webhooks) use a Bearer token you mint in your dashboard. Trooth automates the reading and signs the event; Trooth never signs on a company's behalf.
curl https://api.trooth.co/public/trust/your-coReady for authenticated calls? Grab a key and see rate limits on the API reference.
How it fits
Architecture
Every value the API returns came through this path. Select a node to see what it receives, what it emits, and what it is unable to do.
Topology
↺ The whole path repeats on a schedule, which is why a profile does not go stale.
Your stack
Cloud accounts, identity providers, repositories and endpoints, connected with read-only grants you approve one at a time.
Cannot
Cannot be written to. No write scope is requested, so Trooth is unable to change the systems it reports on.
Payload shape
{ "connector": "aws", "grant": "read-only", "scopes": ["describe*", "list*", "get*"], "write_scopes": []}Shapes are real; the values are an illustrative sample, not anyone's account.
Public API
Public Trust Profile API
/public/trust/:slugPublicGET /public/trust/:slug returns a company's public, signed Trust Profile as JSON: what Trooth observed, the pillar summary, the chain evidence and the last witnessed time. A company Trooth has not witnessed returns a typed absence rather than a guess, with the reason and a claim link. Public and unauthenticated.
curl -s https://api.trooth.co/public/trust/your-co \
-H "Accept: application/json"Press Run. Nothing is shown here until a real response comes back, so this pane never displays a payload the API did not send.
Calls api.trooth.co directly from your browser. Public, unauthenticated, and rate limited for each IP address.
Client embed
Trust Badge
Drop a live, self-updating Trust Badge anywhere. Paste the embed where you want it to appear. It reads your public profile and re-renders automatically; no redeploy needed.
<div id="trooth-trust-badge" data-slug="your-co"></div>
<script src="https://trooth.co/badge.js"></script>This loads the real badge.js against Trooth's own slug, so what you see is a live witnessed record. Swap in your own slug and the same script renders your record.
The badge on the right is rendered by the real badge.js against Trooth's own slug, so it shows a real witnessed record. Your embed uses your slug.
Full install steps and placement tips are on the badge install page.
Command line
CLI
/directory/api/vendorsPublic, no keyRead any company's public record from your terminal. No key, no account, and nothing about you is sent: it is a read of a record that is already published. A company with no record exits 1; a Trooth that could not be reached exits 3, so a pipeline never mistakes an outage for a company with no record.
# Published to npm as `trooth`. Zero dependencies, Node 18+.
npm install -g trooth
trooth --version
0.4.0
# No install, no key, no account. Reads a published public record.
npx trooth check stripe.comtrooth lint is the local half: it reads what the infrastructure in a directory declares and prints those declarations as facts, with a canonical digest over them. It issues no verdict and checks nothing against any named standard, and nothing leaves the machine. Exit codes, flags and the full reference are on the CLI page.
Events
Webhooks
Subscribe to state changes and Trooth will POST a JSON event to your endpoint. Manage endpoints from your dashboard.
Trooth sends webhooks on two channels, and they share one header name while signing different bytes. Endpoints you register at /dashboard/webhooks are the channel the events, envelope and first recipe below belong to. Alert destinations, which a workspace configures under Alerts, are delivered by Trooth itself and sign a timestamp as well as the body; their recipe is the second one, at the end of this section. Tell a delivery apart by its headers: an x-trooth-timestamp header, or an x-trooth-signature that begins sha256=, is the alert channel. A recipe written for one channel returns false on every genuine delivery from the other.
| Event | Fires when |
|---|---|
| witness.changed | What Trooth has witnessed changed: the counts in the published coverage object, with the previous pair. trust.score.changed is accepted as a legacy name at registration and delivered as witness.changed. |
| control.witnessed | A control was witnessed again. control_id and framework carry whatever names your own control set uses. |
| profile.viewed | A buyer opened your public Trust Profile. |
| profile.requested | A buyer asked you to publish a profile. |
| monitoring.drift | Monitoring recorded a regression or a lost connection on a connected source. One event per workspace per re-witness run; data carries count, changes[] and a url. |
Each event shares the outer envelope of id, type, created and data. The shape inside data varies by type, and so does its nesting: read the sample for the event you are handling rather than assuming a common wrapper.
{
"id": "evt_a1b2c3d4e5f6",
"type": "witness.changed",
"created": 1785340800000,
"data": {
"coverage": { "passed": 41, "run": 44 },
"previous": { "passed": 40, "run": 44 },
"change": { "passed": 1, "run": 0 },
"source": "capability",
"changed_at": 1785340800000
}
}The legacy name control.verified is accepted as an alias when registering webhooks.
Verifying a delivery to an endpoint you registered
This recipe is for the channel above: the endpoints you register at /dashboard/webhooks, delivered by the Trooth worker at api.trooth.co. Every delivery on it carries two headers. x-trooth-signature is the HMAC-SHA256 of the raw request body, keyed with your endpoint's signing secret, as 64 lowercase hex characters with no prefix. x-trooth-event repeats the event type. This channel sends no timestamp header, and the signed bytes are the body and nothing else.
Compute the same HMAC over the exact bytes you received, before any JSON parsing, and compare in constant time. Key order, whitespace and Unicode are all part of the signed bytes: a body your framework parsed and re-serialised will not verify. Deduplicate on the event id; with no timestamp signed on this channel, that is what makes a replayed delivery harmless.
import crypto from "node:crypto";
// rawBody: the exact bytes of the request as a Buffer, read BEFORE any JSON
// parsing. A body a framework parsed and re-serialised is not what was signed.
// headers: the request headers with lower-case names.
// secret: the endpoint signing secret shown once at registration.
export function verifyTrooth(rawBody, headers, secret) {
const signature = String(headers["x-trooth-signature"] || "");
const expected = crypto.createHmac("sha256", secret).update(rawBody).digest("hex");
const a = Buffer.from(expected, "utf8");
const b = Buffer.from(signature, "utf8");
// Compare the length first: timingSafeEqual throws on unequal lengths, and
// a missing or malformed header must be a clean false, not a crash.
if (a.length !== b.length) return false;
return crypto.timingSafeEqual(a, b);
}Verifying a delivery to an alert destination
A workspace can also send Trooth's signals to a destination it configures under Alerts: a chat app, or a generic HTTPS endpoint with a SIEM, a ticket queue or an automation behind it. Those deliveries never touch the worker. Trooth sends them itself, and signs them the other way. x-trooth-timestamp carries the second the delivery was signed at, as a Unix time. x-trooth-signature is the string sha256= followed by the HMAC-SHA256, in 64 lowercase hex characters, of the timestamp, a literal full stop, and the exact bytes of the body. The full stop is part of the signed material and the prefix is part of the header value, so the recipe above returns false on every one of these deliveries.
Check the timestamp before the HMAC, and refuse a delivery whose timestamp is more than 300 seconds from your own clock in either direction. A delivery that reaches you honestly is seconds old: it is attempted at most three times inside a 15 second budget, and every attempt carries the timestamp that was signed before the first one. The remainder of the window is room for clock skew. Narrow it if your clock is disciplined; widening it is the one change that costs you something, because the width of this window is how long a captured delivery stays usable against you.
Each destination has its own key, and it is not the per-endpoint secret shown once at /dashboard/webhooks. It is derived from Trooth's master key and that destination's own identity: an HMAC-SHA256 over the label trooth-notify-route-v1, the workspace, the destination id and the key generation, each on its own line, given to you as 64 lowercase hex characters. It verifies deliveries to that destination and to no other, and it does not yield the master key it was derived from.
A destination created before 19 September 2026 is at key generation 0 and is still signed with the shared key it has been signed with since it was created; nothing about its deliveries changes until its owner rotates it. A destination created since then has a key of its own from its first delivery. Rotating one destination changes that destination's key and no other, so no other workspace's receiver is affected by it.
Read x-trooth-signature as a list. It carries one value normally, and two comma-separated values, newest first, for the seven days after a rotation, so a receiver that has not swapped its key yet keeps working. Split the header on commas, trim each value, and accept the delivery if any value matches the signature you compute. A receiver that compares the whole header string to one expected value works until its destination's key is rotated and then rejects every genuine delivery for that week.
The second value is the key the rotation replaced, and for a destination rotated off the shared key that is the shared key itself: the one its receiver is already verifying with. So a receiver that has not been given the new key keeps working for seven days, and a destination that has never been rotated sees a list for the first time on the day it is rotated. Those seven days are the time to swap the key, not a week in which nothing has to be done. When the window closes, only the new key is sent.
import crypto from "node:crypto";
// rawBody: the exact bytes of the request as a Buffer, read BEFORE any JSON
// parsing. What was signed is what came off the wire, not a re-serialised object.
// headers: the request headers with lower-case names.
// secret: the signing key Trooth issued for this destination. It is NOT the
// per-endpoint secret from /dashboard/webhooks; that one signs the other channel.
export function verifyTroothAlert(rawBody, headers, secret, toleranceSeconds = 300) {
const timestamp = String(headers["x-trooth-timestamp"] || "");
const header = String(headers["x-trooth-signature"] || "");
// A missing or non-numeric timestamp is a delivery that cannot be placed in
// time, which is the thing this check exists to refuse.
if (!/^[0-9]+$/.test(timestamp)) return false;
const age = Math.abs(Math.floor(Date.now() / 1000) - Number(timestamp));
// The window is checked first so a replay cannot be rescued by the fact that
// its signature is genuine. It is genuine; that is what a replay is.
if (age > toleranceSeconds) return false;
const signed = Buffer.concat([Buffer.from(`${timestamp}.`, "utf8"), rawBody]);
const expected = "sha256=" + crypto.createHmac("sha256", secret).update(signed).digest("hex");
const a = Buffer.from(expected, "utf8");
// The header carries one value normally, and two comma-separated values,
// newest first, for seven days after this destination's key is rotated.
// Split the header on commas, trim each value, and accept the delivery if any
// value matches the signature you compute.
let matched = false;
for (const value of header.split(",")) {
const b = Buffer.from(value.trim(), "utf8");
// Compare the length first: timingSafeEqual throws on unequal lengths, and a
// missing or malformed value must be a clean false, not a crash.
if (a.length !== b.length) continue;
// Every value is compared rather than returning on the first match, so how
// long the check takes says nothing about which value matched.
if (crypto.timingSafeEqual(a, b)) matched = true;
}
return matched;
}Use of the Trooth API is subject to the Terms of Service.