Documentation

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.

Base URL
api.trustbarrier.tech
API version
v1
Auth
Bearer token
Format
JSON

Quick start

From a brand-new account to a first decision:

  1. 1
    Generate an API key
    In your dashboard at app.trustbarrier.tech, go to Settings → API Keys and click Generate. The key is shown once; store it securely.
  2. 2
    Make your first check
    Below 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"]
  3. 3
    Act on the response
    If 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:

ScopeGrants access to
checkPOST /v1/check
reportPOST /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.

POST

/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

FieldTypeNotes
ipstringIPv4 or IPv6, the customer's connecting IP. Do not pass your server's IP.
addressstringFree-form delivery address, ≤ 500 chars. Normalisation is automatic.
emailstringCustomer email. Gmail dot/+ aliases are auto-canonicalised before matching.
phonestringE.164 or local format (we strip everything but digits before matching).
namestringCustomer name. Captured for forensics; no hard rule yet.
delivery_latnumberLatitude (-90…90). Captured for upcoming geo-distance signals.
delivery_lngnumberLongitude (-180…180).
device_fingerprintstringBrowser fingerprint hash. Powers identity-drift / sock-puppet detection.
cardobjectPCI-clean card fingerprint. See sub-fields below.
card.brandstringvisa · mastercard · amex · discover · diners · jcb · unionpay · maestro · other
card.binstringFirst 6 digits.
card.last4stringLast 4 digits.
card.exp_monthint1-12. Optional but improves matching precision.
card.exp_yearintFour digit year, e.g. 2027. Optional.
reference_idstringYour order or session ID, ≤ 120 chars. Echoed in the audit log.
metadataobjectFree-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 }
    }
  }
}
POST

/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.

GET

/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.

CodeMeaning
ip_blockedRequest IP is on your local blocklist (exact match).
ip_blocked_cidrRequest IP falls inside a blocklisted CIDR range.
address_blockedNormalised address matches a blocklist entry.
email_blockedCanonicalised email matches a blocklist entry.
phone_blockedDigits-only phone matches a blocklist entry.
card_blockedCard fingerprint (brand+bin+last4+exp) matches a blocklist entry.
score_threshold_blockSoft-signal weights crossed your merchant threshold (default 80).
merchant_suspendedYour merchant account is suspended; decision returns allow with this code as a notice.
merchant_closedYour 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_5m20>10 attempts from one IP in 5 minutes (per-merchant).
velocity_card_1h25>5 attempts on the same card fingerprint in 1 hour.
velocity_email_1h20>5 attempts from the same email in 1 hour.
disposable_email25Email domain is on the curated throwaway-mail registry.
weak_card_match25-45Card matches a BIN-less fraud signature (brand + last4 + exp). +5 per source account.
cross_merchant_card30Card is on another merchant's local blocklist.
cross_merchant_email25Email is on another merchant's local blocklist (gmail-alias resistant).
cross_merchant_phone25Phone (digits-only) is on another merchant's local blocklist.
address_fuzzy_match20Normalised address differs from a blocklisted address by ≤ Levenshtein(2, len/8).
negative_history30-50Identifier was used in any prior block on the network in the last 30 days. Alias-resistant. +5 per event.
ip_reputation25-45Request IP has 3+ blocks across the network in the last 24 hours. +5 per block over threshold.
device_drift30-50Device fingerprint paired with 2+ distinct emails/cards on this merchant in 30 days (sock puppet).
vpn_proxy25-35Request 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.

HeaderMeaning
X-RateLimit-LimitYour minute-window allowance.
X-RateLimit-RemainingCalls remaining in the current window.
X-RateLimit-ResetUnix 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.

StatusWhy
400Malformed JSON.
401Missing or invalid Bearer token.
403Token lacks the required scope.
409Idempotency-Key replay with mismatched body.
422Validation error. errors map names the failing fields.
429Rate limit exceeded.
500-503Our 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

v1 — current
Stable contract for /v1/check, /v1/report, and /v1/health. Sixteen soft signals, alias-resistant matching, network-effect intelligence across the tenant base. Backwards-compatible additions to the response (new signals entries, new reason_codes) may appear in any release; field removal will only happen at a major version bump.

Need help integrating? Email hello@trustbarrier.tech with your slug and the integration platform you're working with — we'll send a reference implementation.