Developer API
Run the Trust Score from your own tools
One REST endpoint, one uniform JSON response, five scoring pipelines underneath. Mint a bearer token inside your dashboard, send a request, get a verdict with an explanation, a confidence rating, and three recommended next steps.
/api/v1/trust-score — POST any of the five kind payloads.Authorization: Bearer <key>. Keys are mint-then-revoke, with the plaintext shown once at mint.Authentication
Pass your plaintext key in the Authorization header with a Bearer prefix. The plaintext is shown to you exactly once, when you mint a key — store it then. A missing or empty header returns 401 { "error": "Unauthorized" }; an unknown or revoked plaintext returns 401 { "error": "invalid api key" }.
curl -X POST /api/v1/trust-score \
-H "Authorization: Bearer <your-plaintext-key>" \
-H "Content-Type: application/json" \
-d '{"kind":"url","url":"https://example.com/login"}'Scopes
Today the Developer API exposes a single scope: read:trust. Every key minted through the dashboard is granted it. A key without read:trust is rejected by the gateway with 403 { "error": "Forbidden — api key missing read:trust scope" }, regardless of whether its plaintext authenticated. New scopes will be listed here as they ship.
Request — pick a kind
The request is a discriminated union keyed on kind. Each variant carries only the scalar payload its pipeline needs.
| Field | Type | Required |
|---|---|---|
| kind | "url" | "email" | "sms" | "qr" | "screenshot" | yes |
| <variant field> | url · content · text · dataUrl · extracted | yes |
/api/v1/trust-score with the body below.{
"kind": "url",
"url": "https://example.com/login"
}/api/v1/trust-score with the body below.{
"kind": "email",
"content": "From: noreply@example.com\nSubject: Verify your account\n\nReply with the 6-digit code we sent."
}/api/v1/trust-score with the body below.{
"kind": "sms",
"text": "USPS: Your package has unpaid toll. Reply with your code within 24 hours."
}/api/v1/trust-score with the body below.{
"kind": "qr",
"dataUrl": "data:image/png;base64,<…paste your base64 PNG here…>"
}/api/v1/trust-score with the body below.{
"kind": "screenshot",
"extracted": {
"text": "Apple ID locked. Verify now: https://bit.ly/abc",
"urls": [
"https://bit.ly/abc"
],
"textLength": 48,
"urlCount": 1,
"preview": "Apple ID locked. Verify now: …"
}
}Response — symmetric across all kind values
Every kind returns the same shape — score, verdict, riskLevel, aiExplanation, confidence, recommendedActions, meta, limitReached, upgradeHref. The envelope is deterministic: the explanation names the worst-fired signal, the confidence rating is calibrated against the fired-signal count, and the recommended actions list always has exactly three entries. The two limitReached / upgradeHref keys are optional and only stamped when an anonymous bearer crosses the Free-tier monthly cap.
| Field | Description |
|---|---|
| score | Integer 0–100. 75+ reads as low risk; 40–74 caution; below 40 high risk. |
| verdict | Short human-readable verdict — "no major warning signs detected" or "potential warning signs detected". |
| riskLevel | Band label used by the result-page chip ("Low Risk", "Medium Risk", "High Risk"). |
| aiExplanation | One-paragraph narrative naming the worst-fired signal so you can quote or surface the reason. |
| confidence | Integer 0–100 — distance from 100 plus a penalty per warn/bad signal that fired. |
| recommendedActions | Three short next-step strings keyed off the band. Always three entries. |
| meta | Block with `kind`, `target` (the human-readable identity), `signals`, and `redFlags`. |
| limitReached | Optional — present (true) only when an anonymous bearer crossed the shared Free-tier monthly cap. Signed-in Premium / Family keys skip the meter and never see this field. |
| upgradeHref | Optional — companion to `limitReached`; `/pricing` so clients that read it can render an upgrade prompt without hardcoding a URL. |
Confidence climbs as the score nears 100 and falls per warn/bad signal; actions length is 3 across all bands.
{
"score": 56,
"verdict": "potential warning signs detected",
"riskLevel": "Medium Risk",
"aiExplanation": "Potential warning signs detected — the analyzer flagged urgency or threat language alongside one or two neutral signals. Verify before acting.",
"confidence": 38,
"recommendedActions": [
"Verify the sender through a separate channel before clicking any link surfaced by this request.",
"Reply via the original thread or surface only — never use contact info pulled out of the message itself.",
"Forward the original message to the brand through their published support address if you are unsure."
],
"meta": {
"kind": "sms",
"target": "+1 (555) 010-1234",
"signals": [
{ "id": "urgency-language", "label": "Urgency or threat language", "tone": "bad", "weight": 14 }
],
"redFlags": ["urgency-language"]
}
}Errors
401 { "error": "Unauthorized" }— noAuthorizationheader, or the header is not inBearer <...>form.401 { "error": "invalid api key" }— empty bearer value, unknown plaintext, or revoked key.403 { "error": "Forbidden — api key missing read:trust scope" }— plaintext authenticated, but the key does not carryread:trust. Re-mint a key with the right scope or revoke and re-issue.400 { "errors": { "<field>": "<message>" } }— invalid request (missing required field, malformed URL, image that isn't a PNG, etc.).500 { "error": "Internal Server Error" }— unexpected failure on our side. Retry with exponential back-off.
Operational notes
- Plaintext handled once. The plaintext is shown to the user at mint time, never re-emitted on subsequent reads, and never logged by the auth lookup.
- Revoke, don't rotate. A leaked key should be revoked (UI button or
DELETE /api/dev/keys/<id>). Re-mint a new one rather than reuse the id. - Scope-gated last-used stamp. A key's
lastUsedAttimestamp is updated only after a successfulread:trustscope check, so the dashboard reflects real scoring successes rather than auth-grants-then-deny probe traffic. - Screenshot variant requires pre-OCR. The
kind:"screenshot"branch expectsextractedas already-extracted text + a URL list. Run your own OCR upstream and pass the result.