Integrate TrustBarrier in ten minutes.
TrustBarrier is a pre-charge fraud-screening API. You make one HTTPS call before you charge the card; you get back block or allow in under fifty milliseconds. There is no SDK to vendor and no PCI scope to revisit.
Quick start
From a brand-new account to a first decision:
-
1Generate an API keyIn your dashboard at app.trustbarrier.tech, go to Settings → API Keys and click Generate. The key is shown once; store it securely.
-
2Make your first checkBelow is the minimal request — IP and address are enough to get a decision. Replace YOUR_KEY with the key you just generated.
curl https://api.trustbarrier.tech/v1/check \ -X POST \ -H "Authorization: Bearer YOUR_KEY" \ -H "Content-Type: application/json" \ -d '{ "ip": "203.0.113.42", "address": "L.G. Smith Blvd 101", "reference_id": "order_8472" }'const res = await fetch("https://api.trustbarrier.tech/v1/check", { method: "POST", headers: { "Authorization": `Bearer ${process.env.TRUSTBARRIER_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ ip: "203.0.113.42", address: "L.G. Smith Blvd 101", reference_id: "order_8472", }), }); const { decision, reason_codes, score } = await res.json();$r = Http::withToken(env('TRUSTBARRIER_KEY')) ->timeout(2) ->post('https://api.trustbarrier.tech/v1/check', [ 'ip' => '203.0.113.42', 'address' => 'L.G. Smith Blvd 101', 'reference_id' => 'order_8472', ]) ->json(); [$decision, $reasons] = [$r['decision'], $r['reason_codes']];import httpx, os r = httpx.post( "https://api.trustbarrier.tech/v1/check", headers={"Authorization": f"Bearer {os.environ['TRUSTBARRIER_KEY']}"}, json={ "ip": "203.0.113.42", "address": "L.G. Smith Blvd 101", "reference_id": "order_8472", }, timeout=2.0, ).json() decision, reasons = r["decision"], r["reason_codes"] -
3Act on the responseIf decision is block, decline the customer politely. If allow, charge as normal. The score field gives you the soft-signal weight should you want a per-merchant threshold; the default behaviour is fine for most.
Authentication
Every request to api.trustbarrier.tech must include an Authorization: Bearer <key> header. Keys are generated per-merchant in the dashboard and may carry one or both of two scopes:
| Scope | Grants access to |
|---|---|
| check | POST /v1/check |
| report | POST /v1/report |
Keys are shown once at generation time, hashed at rest, and never recoverable thereafter — rotate via the dashboard if you suspect compromise. Requests with missing or invalid keys return 401 Unauthorized; valid keys lacking the required scope return 403 Forbidden.
/v1/check
The single endpoint your checkout calls. Returns a block or allow decision with the reason codes and soft-signal weights that produced it. At least one identifier is required; beyond that, the more fields you can pass, the sharper the engine's view.
Request body
| Field | Type | Notes |
|---|---|---|
| ip | string | IPv4 or IPv6, the customer's connecting IP. Do not pass your server's IP. |
| address | string | Free-form delivery address, ≤ 500 chars. Normalisation is automatic. |
| string | Customer email. Gmail dot/+ aliases are auto-canonicalised before matching. | |
| phone | string | E.164 or local format (we strip everything but digits before matching). |
| name | string | Customer name. Captured for forensics; no hard rule yet. |
| delivery_lat | number | Latitude (-90…90). Captured for upcoming geo-distance signals. |
| delivery_lng | number | Longitude (-180…180). |
| device_fingerprint | string | Browser fingerprint hash. Powers identity-drift / sock-puppet detection. |
| card | object | PCI-clean card fingerprint. See sub-fields below. |
| card.brand | string | visa · mastercard · amex · discover · diners · jcb · unionpay · maestro · other |
| card.bin | string | First 6 digits. |
| card.last4 | string | Last 4 digits. |
| card.exp_month | int | 1-12. Optional but improves matching precision. |
| card.exp_year | int | Four digit year, e.g. 2027. Optional. |
| reference_id | string | Your order or session ID, ≤ 120 chars. Echoed in the audit log. |
| metadata | object | Free-form key/value bag of your own annotations. |
Never send raw PAN, CVV, or track data. The card object accepts only the masked fields above — the same fields your processor already returns to you.
Example request
curl https://api.trustbarrier.tech/v1/check \
-X POST \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"ip": "203.0.113.42",
"address": "L.G. Smith Blvd 101 Unit 12",
"email": "customer@example.com",
"phone": "+297 555 1234",
"device_fingerprint": "d8b1f4a3c9e2…",
"card": {
"brand": "visa",
"bin": "411111",
"last4": "1111",
"exp_month": 8,
"exp_year": 2027
},
"reference_id": "order_8472"
}'
const r = await fetch("https://api.trustbarrier.tech/v1/check", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TRUSTBARRIER_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
ip: req.ip,
address: order.delivery_address,
email: customer.email,
phone: customer.phone,
device_fingerprint: req.body.fingerprint,
card: {
brand: card.brand,
bin: card.first6,
last4: card.last4,
exp_month: card.expMonth,
exp_year: card.expYear,
},
reference_id: order.id,
}),
});
const result = await r.json();
$result = Http::withToken(config('services.trustbarrier.key'))
->timeout(2)
->post('https://api.trustbarrier.tech/v1/check', [
'ip' => $request->ip(),
'address' => $order->delivery_address,
'email' => $customer->email,
'phone' => $customer->phone,
'device_fingerprint' => $request->input('fp'),
'card' => [
'brand' => $card->brand,
'bin' => $card->first6,
'last4' => $card->last4,
'exp_month' => $card->exp_month,
'exp_year' => $card->exp_year,
],
'reference_id' => $order->id,
])
->json();
import httpx, os
result = httpx.post(
"https://api.trustbarrier.tech/v1/check",
headers={"Authorization": f"Bearer {os.environ['TRUSTBARRIER_KEY']}"},
json={
"ip": request.client.host,
"address": order.delivery_address,
"email": customer.email,
"phone": customer.phone,
"device_fingerprint": request.headers.get("x-fp"),
"card": {
"brand": card.brand,
"bin": card.first6,
"last4": card.last4,
"exp_month": card.exp_month,
"exp_year": card.exp_year,
},
"reference_id": order.id,
},
timeout=2.0,
).json()
Response
Returns 200 OK with a JSON body. The shape is stable across the v1 contract.
{
"decision": "block",
"reason_codes": ["card_blocked"],
"score": 100,
"event_id": "ev_01KQRCH9417C5BF7ZW7WBMP8CF"
}
{
"decision": "allow",
"reason_codes": [],
"score": 50,
"event_id": "ev_01KQRCJ5AY2M9ASPJJXBA4FQCN"
}
{
"decision": "allow",
"reason_codes": [],
"score": 75,
"event_id": "ev_01KQRCKRZBC6KT9HC4GFY60BG1",
"signals": {
"disposable_email": {
"weight": 25,
"detail": { "domain": "mailinator.com" }
}
}
}
{
"decision": "block",
"reason_codes": ["score_threshold_block"],
"score": 85,
"event_id": "ev_01KQRCH9417C5BF7ZW7ZZP00CG",
"signals": {
"weak_card_match": {
"weight": 35,
"detail": { "source_account_count": 3 }
}
}
}
/v1/report
Used after a chargeback or confirmed fraud event to add the offending identifiers to your local blocklist (and, optionally, to the cross-merchant network). The same field set as /v1/check applies.
curl https://api.trustbarrier.tech/v1/report \
-X POST \
-H "Authorization: Bearer YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{
"reason": "chargeback_card_not_present",
"reference_id": "order_8472",
"share_with_network": true,
"identifiers": {
"email": "fraudster@example.com",
"card": {
"brand": "visa",
"bin": "411111",
"last4": "1111"
}
}
}'
share_with_network defaults to false. When true, the signature is reviewed by the platform team before propagating; reads from the network pool are included on every tier.
Auto-protection: if any reported address matches a known shared accommodation (hotel, vacation rental), it is silently dropped — punishing the property punishes the next legitimate guest, not the fraudster.
/v1/health
Unauthenticated machine-readable health check, intended for your own monitoring. Returns 200 OK with { "status": "ok" } when the API is up. Anything else is a problem on our end.
Reason codes
Returned in the reason_codes array. Hard-rule codes set the score to 100; score_threshold_block is the soft-rule fallback when accumulated signal weight crosses your tunable threshold.
| Code | Meaning |
|---|---|
| ip_blocked | Request IP is on your local blocklist (exact match). |
| ip_blocked_cidr | Request IP falls inside a blocklisted CIDR range. |
| address_blocked | Normalised address matches a blocklist entry. |
| email_blocked | Canonicalised email matches a blocklist entry. |
| phone_blocked | Digits-only phone matches a blocklist entry. |
| card_blocked | Card fingerprint (brand+bin+last4+exp) matches a blocklist entry. |
| score_threshold_block | Soft-signal weights crossed your merchant threshold (default 80). |
| merchant_suspended | Your merchant account is suspended; decision returns allow with this code as a notice. |
| merchant_closed | Your merchant account is closed. |
Soft signals
When no hard rule fires, the engine composes the signals below into a score from a fifty-point baseline. Each fired signal is returned in signals[name].weight with a .detail object you can render in your back office for explainability.
Default weights below; all are tunable per-merchant. Set any weight to zero in your dashboard to disable a signal.
| Signal | Weight | What it catches |
|---|---|---|
| velocity_ip_5m | 20 | >10 attempts from one IP in 5 minutes (per-merchant). |
| velocity_card_1h | 25 | >5 attempts on the same card fingerprint in 1 hour. |
| velocity_email_1h | 20 | >5 attempts from the same email in 1 hour. |
| disposable_email | 25 | Email domain is on the curated throwaway-mail registry. |
| weak_card_match | 25-45 | Card matches a BIN-less fraud signature (brand + last4 + exp). +5 per source account. |
| cross_merchant_card | 30 | Card is on another merchant's local blocklist. |
| cross_merchant_email | 25 | Email is on another merchant's local blocklist (gmail-alias resistant). |
| cross_merchant_phone | 25 | Phone (digits-only) is on another merchant's local blocklist. |
| address_fuzzy_match | 20 | Normalised address differs from a blocklisted address by ≤ Levenshtein(2, len/8). |
| negative_history | 30-50 | Identifier was used in any prior block on the network in the last 30 days. Alias-resistant. +5 per event. |
| ip_reputation | 25-45 | Request IP has 3+ blocks across the network in the last 24 hours. +5 per block over threshold. |
| device_drift | 30-50 | Device fingerprint paired with 2+ distinct emails/cards on this merchant in 30 days (sock puppet). |
| vpn_proxy | 25-35 | Request IP is inside a known cloud / VPN egress range. +10 for consumer VPNs (Cloudflare WARP, Nord, Mullvad, Proton). |
Score formula: score = clamp(50 + sum(fired_weights), 0, 100). If score ≥ merchant.score_threshold_block (default 80), decision flips to block and reason_codes includes score_threshold_block.
Idempotency
Network retries should never produce two distinct decisions for the same checkout attempt. Pass an Idempotency-Key header — any string up to 120 characters, typically your order ID — and we will return the cached response on retry.
curl https://api.trustbarrier.tech/v1/check \
-X POST \
-H "Authorization: Bearer YOUR_KEY" \
-H "Idempotency-Key: order_8472" \
-H "Content-Type: application/json" \
-d '{ "ip": "203.0.113.42" }'
Cached responses are returned for 24 hours. If you reuse the same key with a different request body, the response will include 409 Conflict — the key is bound to the original payload's hash.
Rate limits
Limits are enforced per merchant, not per API key. Headers on every response tell you where you stand.
| Header | Meaning |
|---|---|
| X-RateLimit-Limit | Your minute-window allowance. |
| X-RateLimit-Remaining | Calls remaining in the current window. |
| X-RateLimit-Reset | Unix timestamp at which the window resets. |
Exceeding the limit returns 429 Too Many Requests. Your SDK should treat this as a transport failure and follow the fail-open pattern below.
Error responses
Errors follow RFC 7807 Problem Details. The body is JSON with type, title, status, and detail fields.
| Status | Why |
|---|---|
| 400 | Malformed JSON. |
| 401 | Missing or invalid Bearer token. |
| 403 | Token lacks the required scope. |
| 409 | Idempotency-Key replay with mismatched body. |
| 422 | Validation error. errors map names the failing fields. |
| 429 | Rate limit exceeded. |
| 500-503 | Our problem. Treat as transport failure and proceed. |
Fail-open SDK pattern
TrustBarrier should never cost you a sale. Every recommended client implementation wraps the call in a hard timeout and treats any non-200 outcome as a pass-through. If the API is unreachable, your customer's transaction proceeds to your processor exactly as it would today.
async function screen(payload) {
try {
const r = await fetch("https://api.trustbarrier.tech/v1/check", {
method: "POST",
headers: {
"Authorization": `Bearer ${process.env.TB_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify(payload),
signal: AbortSignal.timeout(2000),
});
if (!r.ok) return { decision: "allow", trustbarrier_failed: true };
return await r.json();
} catch {
return { decision: "allow", trustbarrier_failed: true };
}
}
function screen(array $payload): array
{
try {
$r = Http::withToken(config('services.trustbarrier.key'))
->timeout(2)
->retry(0)
->post('https://api.trustbarrier.tech/v1/check', $payload);
if (! $r->ok()) {
return ['decision' => 'allow', 'trustbarrier_failed' => true];
}
return $r->json();
} catch (\Throwable $e) {
report($e);
return ['decision' => 'allow', 'trustbarrier_failed' => true];
}
}
async def screen(payload: dict) -> dict:
try:
async with httpx.AsyncClient(timeout=2.0) as c:
r = await c.post(
"https://api.trustbarrier.tech/v1/check",
headers={"Authorization": f"Bearer {os.environ['TB_KEY']}"},
json=payload,
)
if r.status_code != 200:
return {"decision": "allow", "trustbarrier_failed": True}
return r.json()
except (httpx.TimeoutException, httpx.HTTPError):
return {"decision": "allow", "trustbarrier_failed": True}
We recommend logging the trustbarrier_failed branch separately so you can monitor for sustained outages. We also publish API health at trustbarrier.tech/status.
Capturing identifiers well
The engine asks for everything but accepts as little as one identifier. The more you can pass, the sharper the verdict. Three quick guidelines:
- ✓Pass the customer's IP, not your server's. If your checkout sits behind Cloudflare or a load balancer, use the CF-Connecting-IP or X-Forwarded-For header.
- ✓Send the device fingerprint if you have one. Any deterministic browser fingerprint (FingerprintJS, ThumbmarkJS, your own canvas hash) feeds the sock-puppet detection signal. The string can be arbitrary length — we hash it server-side.
- ✓Include the card object whenever your processor exposes BIN+last4+exp. CyberSource Decision Manager, Stripe, Adyen, Worldpay all return these in their tokenisation step. Send them through; the engine never sees PAN.
- ✓Don't try to normalise the address yourself. Free-form text is fine — the engine handles lowercase folding, punctuation, and Aruban-Spanish variants for you.
Changelog
Need help integrating? Email hello@trustbarrier.tech with your slug and the integration platform you're working with — we'll send a reference implementation.