Submits a single email for verification. Returns a verdict computed from your custom rules over the underlying detection signals, plus batch-cluster scoring and reasoning, in under 100 ms.
Call /api/v1/verify when the user creates the account, not on every login. Persist the verdict and the signals you care about on your users row and reuse them. Re-verify only on a meaningful change — email update, suspected takeover, refund dispute, etc. Re-checking a known-good user on every request burns latency and budget.
This is the integration we recommend for most apps. A block verdict can be a false positive — a real customer with a disposable-shaped local part, a privacy relay user, an alias on top of a real mailbox. Hard-rejecting them at the signup screen costs you real revenue.
Treat the verdict as a dial, not a switch. Let everyone in, but scale what they get: full trial credits for allow, a smaller trial for review, zero credits + a manual review queue for block. The bots can’t monetise zero credits; the false positives still get to use your product.
// In your signup handler — give every user an account, scale the trial.
const { verdict } = await verifyAtSignup(form.email, req.ip);
const trialCredits =
verdict === "block" ? 0 : // suspected bot — no trial, manual review
verdict === "review" ? 50 : // borderline — small trial, watch closely
500; // looks human — full trial
await db.users.create({
...form,
trialCredits,
verifiedVerdict: verdict, // store it; you've already paid for the call
});Verification is a network call to a third party. Treat it the way you’d treat any other dependency: cap the budget with AbortController (3 seconds is a sane synchronous ceiling) and make sure a thrown error or timeout falls through to a safe default — never crash the signup.
// Synchronous: 3s budget, soft-degrade on any failure.
async function verifyAtSignup(email: string, ip: string) {
const ctl = new AbortController();
const timer = setTimeout(() => ctl.abort(), 3000);
try {
const res = await fetch("https://api.trueuser.dev/api/v1/verify", {
method: "POST",
headers: {
Authorization: `Bearer ${process.env.TU_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({ email, ip }),
signal: ctl.signal,
});
if (!res.ok) return { verdict: "allow" as const, degraded: true };
return await res.json();
} catch {
// Network error or timeout — never crash signup on verification.
return { verdict: "allow" as const, degraded: true };
} finally {
clearTimeout(timer);
}
}If your signup flow can’t spare even 3 seconds, don’t put the verification on the critical path at all. Create the user immediately with zero trial credits, then call verification from a fire-and-forget async function. Customers see an instant response; the verdict lands a moment later, before they can do anything expensive. No queue infrastructure needed for a small flow.
// Tight latency budget? Verify in the background. The async function runs
// after you've already responded to the user — no queue infrastructure
// needed for a small flow.
async function verifyInBackground(userId: string, email: string, ip: string) {
const { verdict } = await verifyAtSignup(email, ip);
await db.users.update(userId, {
verifiedVerdict: verdict,
verifiedAt: new Date(),
trialCredits: verdict === "block" ? 0
: verdict === "review" ? 50
: 500,
});
}
async function signup(form, req) {
const user = await db.users.create({
...form,
trialCredits: 0, // start at zero, raise once verified
verifiedAt: null,
});
// Fire-and-forget — don't await. The user gets an instant response;
// the verdict lands a moment later.
void verifyInBackground(user.id, form.email, req.ip);
return user;
}curl https://api.trueuser.dev/api/v1/verify \
-H "Authorization: Bearer $TU_KEY" \
-H "Content-Type: application/json" \
-d '{
"email": "[email protected]",
"username": "alex_42",
"ip": "203.0.113.42"
}'{
"id": "ver_01HK8X...",
"verdict": "block",
"signals": {
"format_valid": true,
"disposable": true,
"public_domain": false,
"relay_domain": false,
"alias": false,
"role_account": false,
"spam": false,
"suspicious_pattern": 0.84,
"batch_cluster": 0.91
},
"reasoning": "Disposable provider with batch-shaped local part.",
"latency_ms": 68
}