trustscore-api

TrustScore API

One API call, one verdict. Send a phone, email, and/or IP — get back allow, review, or block, plus the signals behind it.

Authentication

Every request to /v1/check needs a Bearer key:

Authorization: Bearer <your-api-key>

A missing or invalid key returns 401. The MVP issues one static key per account — request one here.

Try it live

No key yet? This box hits the public, unauthenticated demo endpoint. Have a key? Paste it in and it calls the real /v1/check endpoint instead.

live tester

POST /v1/check

Validate any combination of a phone number, email address, and IP address in one call.

Request body

FieldTypeRequiredDescription
phonestringNo*Any format libphonenumber-js can parse. International format is most reliable.
emailstringNo*Email address to validate.
ipstringNo*IPv4 address. IPv6 is accepted but not checked against the hosting/VPN list.

* At least one of the three is required — omitting all three returns 400.

curl -X POST https://trustscore.titung.com/v1/check \
  -H "Authorization: Bearer YOUR_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "phone": "+14155552671",
    "email": "someone@example.com",
    "ip": "203.0.113.42"
  }'

Example response — 200 OK

{
  "verdict": "allow",
  "score": 10,
  "signals": {
    "phone": { "valid": true, "country": "US", "line_type": "MOBILE", "risk": "low" },
    "email": { "valid": true, "disposable": false, "has_mx": true, "risk": "low" },
    "ip":    { "valid": true, "country": "US", "hosting": false, "vpn": false, "risk": "low" }
  }
}

The signals object only contains keys for the fields you sent.

Response fields

FieldTypeDescription
verdictallow / review / blockThe single decision to act on.
score0–100Underlying risk score. Higher = riskier.
signals.phone.validbooleanWhether it's a real, parseable number.
signals.phone.countrystring | nullISO country code, if determinable.
signals.phone.line_typestringe.g. MOBILE, FIXED_LINE_OR_MOBILE, VOIP, PAGER, unknown.
signals.phone.risklow / medium / highPer-signal risk before combining.
signals.email.validbooleanSyntactically valid address.
signals.email.disposablebooleanKnown disposable/burner domain.
signals.email.has_mxbooleanDomain has mail-exchange DNS records.
signals.email.risklow / medium / highPer-signal risk before combining.
signals.ip.validbooleanSyntactically valid IP.
signals.ip.countrystring | nullGeoIP country, if available.
signals.ip.hostingbooleanFalls in a known cloud/hosting range.
signals.ip.vpnbooleanCurrently mirrors hosting — see Limitations.
signals.ip.risklow / medium / highPer-signal risk before combining.

How the verdict works

Each signal you send contributes a risk score (low = 10, medium = 55, high = 95). The final score averages only the signals you sent — an omitted signal is excluded, not treated as safe.

score ≥ 70 → block  ·  30 ≤ score < 70 → review  ·  score < 30 → allow

Errors

400 Bad Request — no signal fields present, or a field isn't a string:

{ "error": "at least one of phone, email, or ip is required" }

401 Unauthorized — missing or invalid API key:

{ "error": "unauthorized" }

Code examples

JavaScript

const res = await fetch("https://trustscore.titung.com/v1/check", {
  method: "POST",
  headers: {
    "Authorization": `Bearer ${process.env.TRUSTSCORE_API_KEY}`,
    "Content-Type": "application/json",
  },
  body: JSON.stringify({ email: "someone@example.com" }),
});
const { verdict, score } = await res.json();
if (verdict === "block") {
  // reject the signup
}

Python

import os, requests

res = requests.post(
    "https://trustscore.titung.com/v1/check",
    headers={"Authorization": f"Bearer {os.environ['TRUSTSCORE_API_KEY']}"},
    json={"email": "someone@example.com"},
)
verdict = res.json()["verdict"]

Limitations (MVP)

These are current, documented gaps — not hidden behavior.

Questions, a bug, or need a key? hello@titung.com