API Reference · v1

Screen every transaction before it settles.

Vantrace evaluates a transaction against 20+ AML rules — structuring, velocity, sanctions, PEP exposure, mule networks — and returns a decision your core banking system acts on synchronously, before funds move.

One endpoint: POST /v1/engine/evaluate Target SLA: <500ms

Environments

Staging runs the exact same rule set as production against synthetic but structurally realistic data. Nothing is relaxed or mocked — if a rule fires on staging, it fires the same way in production. Going live only changes your base URL and credentials; no code changes required.

EnvironmentBase URLData
Staging{{apiurl}}Synthetic customers, accounts and transaction history, structurally identical to production
Production{{apiurl}}Live customer and transaction data

{{apiurl}} is a placeholder throughout this reference — your onboarding contact provides the actual staging and production values, which differ from each other.

Credentials Every tenant is issued its own API key and HMAC secret during onboarding — separate credentials per environment. Treat both as machine secrets: never commit them to source control, log them, or return them in error messages. This reference uses placeholder values throughout.

Authentication

Three layers. Layers 1 and 3 are required in both staging and production. Layer 2 is not currently enforced server-side, but you should still send it — here's why.

1 · API key required

Send your key in the X-API-Key header on every request. Vantrace stores only a SHA-256 hash of the key — the raw value is never persisted server-side.

Header
X-API-Key: <your_api_key>

2 · Timestamp send it anyway

Send a UTC ISO 8601 timestamp in X-Timestamp.

What "recommended" actually means here This header exists to stop replay attacks: without it, a captured request-plus-signature pair could be resent indefinitely and would still pass HMAC verification, because the signature only covers the body, not the time it was sent. The engine does not currently reject a request for a missing or stale timestamp — so nothing breaks if you skip it. But skipping it is what leaves the replay window open. Send it on every call as if it were enforced; it costs one line of code and closes a real gap that Layer 3 alone doesn't cover.
Header
X-Timestamp: 2026-07-18T11:00:00Z

3 · HMAC-SHA256 body signature required

Sign the raw request body with your HMAC secret and send the hex digest in X-Signature. Vantrace compares it using constant-time comparison — a mismatch returns 401. This is enforced on staging and production alike — there is no unsigned path in either environment.

Serialize once Compute the signature over the exact bytes you send. Serialize the JSON a single time, sign that string, and transmit the same string as the body — re-serializing before sending (which can reorder keys or change whitespace) invalidates the signature.
sign.py
import hmac, hashlib, json

body = json.dumps(payload, separators=(',', ':')).encode('utf-8')
signature = hmac.new(secret.encode('utf-8'), body, hashlib.sha256).hexdigest()
sign.js
const crypto = require('crypto');
const body = JSON.stringify(payload);
const signature = crypto.createHmac('sha256', secret).update(body).digest('hex');
Sign.java
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
byte[] raw = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
String signature = HexFormat.of().formatHex(raw);
sign.go
h := hmac.New(sha256.New, []byte(secret))
h.Write(body)
signature := hex.EncodeToString(h.Sum(nil))

The endpoint

There is one integration point. Every transaction — transfer, deposit, or withdrawal, across every channel — is submitted here before it settles.

POST Evaluate a transaction
{{apiurl}}/v1/engine/evaluate
Content-Type: application/json
X-API-Key: <your_api_key>
X-Timestamp: <iso8601>
X-Signature: <hmac_sha256_hex>
Synchronous by design The call must complete before funds move. Block on the response — do not fire-and-forget. On a timeout or 5xx, fail open (allow the transaction, flag it internally) rather than fail closed. A Vantrace outage should never be the reason a legitimate customer's transfer fails.

Request schema

Three objects: what happened (transaction), who did it (customer), and what else Vantrace should know (context). The first two are required; context is optional but unlocks the rules that depend on real-time signals your system already has.

transaction

FieldTypeReq.Notes
amountfloatTransaction amount. Keep units consistent across every call (e.g. always NGN major units).
currencystringISO 4217, 3-letter. Defaults to NGN.
trx_typestringtransfer · deposit · withdrawal
channelstringmobile_app · USSD · POS · ATM · teller · online
txn_directionstringinbound or outbound
is_cashboolOnly true for physical cash. Never for NIP/RTGS/card rails — the cash-transaction-report rule keys off this being accurate.
is_cross_borderboolCounterparty is outside Nigeria. Default false.
source_account_idintYour internal ID for the sending account.
beneficiary_account_idintYour internal ID for the receiving account.
counterparty_namestringCounterparty bank or individual name.
counterparty_bank_codestringCBN sort code of the counterparty institution.
narrationstringFree-text narration / payment reference.
statusstringcompleted · pending · failed. Default completed.

customer

FieldTypeReq.Notes
customer_idintYour internal customer ID — used to link every alert and case back to this person.
entity_typestringindividual or corporate
kyc_tierintCBN tier 1–3. Drives the daily transaction limit rule (E3).
is_pepboolPolitically exposed person. Default false.
aml_risk_ratingstringlow · medium · high · critical. Default low.
sanctions_flagboolCustomer appears on a sanctions watchlist. Default false.
adverse_media_flagboolAdverse media hits on file. Default false.
risk_flagsstring[]Free-form labels from your own risk system, e.g. ["dormant_reactivation"].

context

Every field defaults to a safe empty value. A rule that depends on a missing field simply doesn't fire — roll these out incrementally, starting with whichever unlocks the rule that matters most to you.

FieldTypeUnlocksDescription
transactions_24hrobject[]B1 B5This customer's transactions in the last 24h.
beneficiary_transactions_72hrobject[]B2Inbound transactions this beneficiary received in the last 72h.
customer_avg_txn_count_24hrfloatB5Historical average 24h transaction count.
customer_avg_txn_amountfloatB6Historical average single-transaction amount.
daily_spend_so_farfloatE3Total outbound amount already sent today.
recent_large_inboundfloatB7Largest inbound credit in the last 30 minutes.
round_trip_accountsstring[]B3Account IDs forming a potential A→B→C→A chain.
device_accountsobject[]B4Other accounts sharing this device fingerprint.
sanctions_matchesobject[]C1Sanctions list hits for the counterparty.
is_pep_transactionboolC3Transaction involves a PEP counterparty.
adverse_media_hitsstring[]C4Adverse media source identifiers.
cpf_risk_indicatorsstring[]C5Proliferation-financing risk indicator codes.
open_cases_countintE1Number of open AML cases already on this customer.
is_high_risk_jurisdictionboolE2Counterparty sits in a FATF high-risk jurisdiction.
is_shell_companyboolE2Counterparty is a known shell company.
last_review_days_agointE4Days since this customer's last AML review.
has_complete_cddboolE5Customer due diligence file is complete. Default true.

Response schema

Every response carries a decision plus the reasoning behind it. should_block is the single field your settlement logic needs to branch on — everything else is context for compliance.

FieldTypeDescription
trx_idintVantrace's internal transaction ID. Store it against your own record for reconciliation.
should_blockboolThe decision gate. true → decline before funds move. false → allow.
final_actionstringnoneflagalertblock, escalating. Only block implies should_block: true.
highest_severitystringlow · medium · high · critical — the ceiling across every rule that fired.
triggered_countintHow many rules fired. 0 means clean.
triggered_rulesobject[]One entry per fired rule — see below.
alert_idsint[]IDs of the alerts Vantrace created. Reference these in any conversation with compliance.
mule_risk_summaryobjectMule network exposure for this transaction — see Mule network risk.
behavioural_data_statusstringok or insufficient_history — whether behavioural rules (B5/B6) had enough baseline data to score with full confidence.

triggered_rules[] shape

FieldTypeDescription
rule_idstringCatalog code — see Rule catalog.
rule_namestringHuman-readable rule name.
severitystringlow · medium · high · critical
actionstringflag · alert · block
report_typestring · nullSTR, CTR, or null if this rule doesn't feed a regulatory report.
detailsobjectRule-specific evidence — thresholds, multipliers, the exact numbers that tripped it. Shape varies per rule; always render it in your compliance UI even if you don't parse it.
Behavioural rules and cold-start customers A brand-new customer or account has no baseline yet. Rather than staying silent, B5/B6 still evaluate against whatever history exists and label the result data_status: "insufficient_history" inside details, with a plain-language data_caveat. Treat these as lower-confidence signals — worth a look, not worth an automatic block on their own.

Mule network risk

Mule detection doesn't live at a separate endpoint — it rides inside the same /v1/engine/evaluate response, in mule_risk_summary. It graphs this transaction against the wider Vantrace network — shared devices, fan-out/fan-in patterns, known mule clusters — to catch coordinated laundering that no single transaction would flag on its own.

FieldTypeDescription
enabledboolWhether mule network scoring is switched on for your tenant.
statusstringdisabled · scored · partial
risk_scoreint · null0–100 network risk score. null when disabled or unscored.
risk_levelstring · nulllow · medium · high · critical
triggered_rule_countintHow many mule-network rules fired for this transaction.
evaluation_idint · nullReference ID for this network evaluation, for cross-referencing with compliance.
reasonstringPlain-language note — e.g. why scoring was skipped.

Real example — captured live, tenant with network scoring off

mule_risk_summary
{
  "enabled": false,
  "status": "disabled",
  "risk_score": null,
  "risk_level": null,
  "enabled_rule_count": 0,
  "evaluated_rule_count": 0,
  "triggered_rule_count": 0,
  "evaluation_id": null,
  "reason": "No mule rules are enabled for this tenant."
}

Ask your Vantrace onboarding contact to enable mule network scoring for your tenant — once on, risk_score and risk_level populate on every call and feed directly into the escalation guidance below.

Rule catalog

Every rule belongs to one of four families. The letter tells you the category before you've even read the name.

RuleNameFamilyDefault action
A1Cash transaction report thresholdCurrency reportingalert · CTR
B1Structuring / smurfing detectionBehaviouralalert · STR
B2Inbound smurfing (beneficiary-side)Behaviouralalert
B3Round-trip layeringBehaviouralalert
B4Mule network device linkageBehaviouralalert
B5Velocity spikeBehaviouralalert · STR
B6Amount anomalyBehaviouralalert
B7Rapid outbound after large inboundBehaviouralalert
C1Sanctions list matchCustomer riskblock · STR
C3PEP transaction monitoringCustomer riskalert
C4Adverse media hitCustomer riskalert
C5Proliferation financing (CPF)Customer riskalert
E1Dynamic risk scoringRegulatory / KYCflag
E2Auto high-risk jurisdictionRegulatory / KYCalert
E3KYC tier daily limit enforcementRegulatory / KYCblock
E4Periodic review overdueRegulatory / KYCflag
E5Incomplete CDD, occasional thresholdRegulatory / KYCflag

"Default action" is what fires under typical conditions — highest_severity in the response can still shift per-call based on amount, customer risk rating, and how many rules co-triggered.

Account holds & PND

should_block stops one transaction. Sometimes the right response is broader — freezing the account itself so nothing moves until compliance has looked at it. Vantrace exposes this as a separate action from transaction screening, distinct from declining a single call.

Two different levers Block (should_block: true) stops the transaction in front of you. Post-No-Debit / account hold stops every future debit on the account until it's lifted. They're independent — a transaction can be allowed through while its pattern still justifies freezing the account for everything that comes after.

When to reach for an account hold

  • Confirmed sanctions match (C1) — pair the transaction block with a hold while compliance verifies identity and files any required report.
  • Repeated high/critical alerts on one customer_id within a short window — structuring (B1), velocity (B5), or rapid-outbound (B7) patterns that individually stayed under the block threshold but read as coordinated when stacked.
  • Elevated mule network riskmule_risk_summary.risk_level at high or critical is a network-level signal, not a single-transaction one; it usually means the account itself is compromised or complicit, not just this transfer.
  • Open case count climbingcontext.open_cases_count rising across calls on the same customer, even if each individual transaction cleared.
Talk to your integration contact Placing and lifting an account hold is a separate authenticated action from transaction screening, issued per-tenant during onboarding. The guidance above tells you when to reach for it; your Vantrace integration contact will walk you through wiring up the hold action itself against your specific tenant configuration.

Errors & reliability

StatusCauseWhat to do
200SuccessRead should_block and act.
400Malformed body or an invalid field value (e.g. kyc_tier outside 1–3)Fix the request — don't retry as-is.
401Missing/invalid API key, expired timestamp, or HMAC mismatchCheck credentials; rotate the key if you suspect it leaked.
422Body fails schema validation — missing required field or wrong typeFix the payload — see the real shape below.
429Rate limit exceededBack off exponentially. Contact Vantrace if this is sustained, not a spike.
500Internal engine errorFail open — allow the transaction, flag internally, alert Vantrace.
503Engine temporarily unavailableFail open; retry with exponential backoff.

422 — real shape, captured live

Response body
{
  "detail": [
    {
      "type": "missing",
      "loc": ["body", "transaction", "amount"],
      "msg": "Field required"
    }
  ]
}

What we observed on 5xx, and what we think should change

Right now a 5xx can come back as a bare text body — literally Internal Server Error, no JSON envelope at all. That's fine for your fail-open handling (status code alone is enough to trigger it) but it's a dead end for debugging: no error code, no correlation ID, nothing to hand to Vantrace support beyond "it failed around 2:14pm." We think this is worth standardizing — every error, 4xx or 5xx, should return the same structured envelope:

Recommended error envelope
{
  "error": {
    "code": "ENGINE_UNAVAILABLE",
    "message": "The evaluation engine could not process this request.",
    "request_id": "req_8f3a1c2e9b4d",
    "retryable": true
  }
}

A stable error.code lets you branch programmatically instead of string-matching. A request_id on every response — success or failure — turns "it was slow around 2pm" into a ten-second lookup for Vantrace support. This isn't live yet; treat the table above as current behavior and this envelope as the direction we'd recommend standardizing toward.

Defensive parsing, until then Don't assume every non-200 response is parseable JSON. Check the status code first; if the body doesn't parse, treat it the same as a timeout — fail open.

Worked examples

Two versions of the same call. Most integrations don't start with rich context — you build up transaction history, behavioural baselines, and case counts over time. Start with the minimum below, ship it, then layer in context fields as your own system accumulates the data to populate them.

Day 1 — minimum viable request

context is optional — omit it entirely until you have something real to put in it. Rules that depend on a missing field simply don't evaluate; nothing errors, nothing degrades.

POST {{apiurl}}/v1/engine/evaluate — request body, no context
{
  "transaction": {
    "amount": 25000.00,
    "currency": "NGN",
    "trx_type": "transfer",
    "channel": "mobile_app",
    "txn_direction": "outbound",
    "is_cash": false
  },
  "customer": {
    "customer_id": 5001,
    "entity_type": "individual",
    "kyc_tier": 2
  }
}
Response · 200
{
  "trx_id": 12301,
  "should_block": false,
  "final_action": "none",
  "highest_severity": "low",
  "triggered_count": 0,
  "triggered_rules": [],
  "alert_ids": []
}

Action: allow. Rules that need context — B1, B5, B6, E3 and others — simply sat out this evaluation rather than firing false positives on missing data.


Once context is flowing

₦50,000 online transfer from a sanctioned, tier-1 individual, carrying a 30× velocity spike against a thin baseline. Captured live during integration testing — every top-level field populated, including mule network and behavioural sufficiency.

POST {{apiurl}}/v1/engine/evaluate — request body
{
  "transaction": {
    "amount": 50000.00,
    "currency": "NGN",
    "trx_type": "transfer",
    "channel": "online",
    "txn_direction": "outbound",
    "is_cash": false,
    "is_cross_border": false,
    "source_account_id": 7323712900,
    "beneficiary_account_id": null,
    "counterparty_name": null,
    "counterparty_bank_code": null,
    "narration": "Business payment",
    "status": "completed"
  },
  "customer": {
    "customer_id": 6,
    "entity_type": "individual",
    "kyc_tier": 1,
    "is_pep": false,
    "aml_risk_rating": "critical",
    "sanctions_flag": true,
    "adverse_media_flag": false,
    "risk_flags": []
  },
  "context": {
    "transactions_24hr": [
      { "amount": "50000.00", "account_id": 5, "counterparty_id": null }
    ],
    "beneficiary_transactions_72hr": [],
    "daily_spend_so_far": 0,
    "customer_avg_txn_amount": 1666.67,
    "customer_avg_txn_count_24hr": 0.0333,
    "recent_large_inbound": 0
  }
}
Response · 200 · captured live
{
  "trx_id": 29,
  "should_block": false,
  "final_action": "none",
  "highest_severity": "low",
  "triggered_count": 0,
  "triggered_rules": [],
  "alert_ids": [],
  "mule_risk_summary": {
    "enabled": false,
    "status": "disabled",
    "risk_score": null,
    "risk_level": null,
    "enabled_rule_count": 0,
    "evaluated_rule_count": 0,
    "triggered_rule_count": 0,
    "not_triggered_rule_count": 0,
    "not_evaluated_rule_count": 0,
    "evaluation_id": null,
    "reason": "No mule rules are enabled for this tenant.",
    "persistence_status": null
  },
  "mule_risk": null,
  "behavioural_data_status": "insufficient_history",
  "behavioural_sufficiency": {
    "status": "insufficient_history",
    "reasons": ["No velocity baseline exists for this customer (new customer or velocity aggregates not yet computed)."],
    "has_velocity_row": false,
    "observations_30d": 0,
    "min_observations_30d": 10,
    "lifecycle_status": "new",
    "baseline_scope": "account_and_customer"
  }
}

Action: allow. triggered_count: 0 — clean on this call, though behavioural_data_status flags that B5/B6 are running on a thin baseline for this customer, so confidence rises as more history accumulates.

Two more scenarios, condensed Regulatory block (E3 only — daily limit breach) returns should_block: true, final_action: "block", one entry in triggered_rules, a populated alert_ids. Stacked critical block (C1 sanctions + B5 velocity together) returns the same shape with two entries in triggered_rules and highest_severity: "critical" — see the rule catalog for what each code means on its own.

Integration checklist

Context fields can land incrementally after go-live — ship the core loop first.

  • Call POST /v1/engine/evaluate before every transaction settles
  • Block on the response — synchronous, not fire-and-forget
  • Branch on should_block; decline before funds move when true
  • Store trx_id against your own transaction record
  • Store alert_ids for compliance reference
  • Sign every request — HMAC is required in staging and production alike
  • Send X-Timestamp on every call, even though it isn't enforced yet
  • Implement fail-open handling for every 5xx and timeout
  • Set is_cash accurately — only true for physical cash channels
  • Populate context.transactions_24hr — unlocks B1 and B5
  • Populate context.daily_spend_so_far — unlocks E3
  • Populate context.customer_avg_txn_amount — unlocks B6
  • Wire up account-hold escalation for stacked high/critical alerts