Skip to main content

Transactions & monitoring

Fiat transaction screening, Web3 transaction screening and continuous customer monitoring.

Transaction Screening

Submit transaction data for AML risk assessment. Use eventType: "TRANSACTION_EVENT".

TRANSACTION_EVENT is for fiat transaction screening (bank transfers, payments and other off-chain transactions). To screen a blockchain wallet address or an on-chain transaction, use WEB3_TRANSACTION_EVENT instead — see Web3 Transaction Screening.

Endpoint: workflow.apply.submit POST

Request

{
  "eventType": "TRANSACTION_EVENT",
  "source": "API",
  "data": {
    "clientTransactionId": "TXN-20260416-001",
    "transactionType": "WITHDRAW",
    "amount": 199999.98,
    "transactionCurrencyId": "SGD",
    "altCurrencyId": "SGD",
    "amountInAltCurrency": 199999.99,
    "customerId": "C_abc123",
    "customerType": 1,
    "cpAId": "SENDER_001",
    "cpABank": "Standard Chartered",
    "cpACountry": "SINGAPORE",
    "cpBId": null,
    "cpBBank": null,
    "cpBCountry": null,
    "transactionTimestamp": "2026-04-16T10:30:00Z"
  }
}

Non-blocking Response (Async)

Returns immediately with executionId. Poll workflow.execution.status for the final result — read caseStatus / riskScore as described in Workflow Lifecycle API → Status & Case Decision.

{
  "apiCode": 200,
  "data": {
    "executionId": "exec-7a3f1b2c4d5e6f78",
    "requestId": "req-1776243366618",
    "status": "RUNNING"
  }
}

Blocking Response (Sync)

Waits for workflow completion and returns the decision inline:

{
  "apiCode": 200,
  "data": {
    "executionId": "exec-7a3f1b2c4d5e6f78",
    "status": "COMPLETED",
    "durationMs": 12,
    "output": {
      "action": "approve",
      "code": 100000
    }
  }
}
Field Description
status COMPLETED = decision made, TIMEOUT = exceeded time limit, FAILED = error
durationMs Processing time in milliseconds
output.action approve / reject / review
output.code Decision code (defaults: approve=100000, reject=100010; configurable)

Web3 Transaction Screening

Submit an on-chain (Web3) transaction event and run blockchain KYT (Know Your Transaction) screening keyed on a wallet address (walletAddress) or a transaction hash (txHash) — provide at least one, or both. The response returns the screening result and a decision. Use eventType: "WEB3_TRANSACTION_EVENT".

Endpoint: workflow.apply.submit POST

Web3 screening uses the same gateway base URL, the same query-string request envelope (api / apiVersion / apiKey / sign) plus the x-domain header, and the same HMAC-SHA256 request-body signing as every other OpenAPI call. See OpenAPI Reference → Getting Started for the base URL and request format, and Authentication for the full signing rules.

workflow.apply.submit supports apiVersion 2.0.0 with an optional workflowId naming a published workflow. Leave workflowId empty for automatic eventType + source routing. For a tenant with multiple published Web3 workflows, confirm the intended published workflow before integration.

After submitting, poll workflow.execution.status and read the Case decision (caseStatus / riskScore) as described in Workflow Lifecycle API → Status & Case Decision.

Request

Variant 1 — walletAddress only

{
  "eventType": "WEB3_TRANSACTION_EVENT",
  "source": "API",
  "data": {
    "chain": "TRON",
    "token": "USDT",
    "walletAddress": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
    "customerId": "demo-customer-001",
    "clientTransactionId": "tx-demo-001",
    "amount": 1000,
    "transactionCurrencyId": "USDT"
  }
}

Variant 2 — txHash only

{
  "eventType": "WEB3_TRANSACTION_EVENT",
  "source": "API",
  "data": {
    "chain": "TRON",
    "token": "USDT",
    "txHash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
    "customerId": "demo-customer-001",
    "clientTransactionId": "tx-demo-001",
    "amount": 1000,
    "transactionCurrencyId": "USDT"
  }
}

Variant 3 — both walletAddress and txHash

{
  "eventType": "WEB3_TRANSACTION_EVENT",
  "source": "API",
  "data": {
    "chain": "TRON",
    "token": "USDT",
    "walletAddress": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
    "txHash": "a1b2c3d4e5f6789012345678901234567890123456789012345678901234abcd",
    "customerId": "demo-customer-001",
    "clientTransactionId": "tx-demo-001",
    "amount": 1000,
    "transactionCurrencyId": "USDT"
  }
}

Top-level Fields

Field Type Required Description
eventType String Yes Fixed value: WEB3_TRANSACTION_EVENT
source String Yes Fixed value: API
data Object Yes Transaction data (see below)
workflowIdStringNoapiVersion 2.0.0 only; published workflow ID. Empty means automatic routing.

data Fields

Field Type Required Description
chain String Yes Chain identifier, e.g. TRON, ETH, BTC, BSC, POLYGON
token String Yes Token symbol, e.g. USDC, USDT, ETH, BTC
walletAddress String At least one Counterparty wallet address
txHash String At least one On-chain transaction hash
clientTransactionId String No The ID that identifies this transaction in your system, for audit trails
customerId String Pending confirmationStable customer identifier. The Web3 guide marks this optional; the general submit contract requires it. Include it until engineering confirms the Web3 requirement.
amount Number No Transaction amount (denominated in transactionCurrencyId)
transactionCurrencyId String No Currency code (e.g. USDC, USDT, USD)
Constraints
  • Provide at least one of walletAddress and txHash; both may be provided together.
  • When both are provided, they must belong to the same chain and match chain.
  • The length of kytScreenResults in the response follows from this: one entry when a single value is provided, two entries when both are provided.

Response

The endpoint returns synchronously. Only HTTP 200 with apiCode: 200 means the request was accepted; an HTTP 4xx/5xx means the gateway did not accept the request and the body carries no data. Once accepted, the business result is distinguished by data.output.code:

code action Meaning Client handling
100000 approve No risk hit Pass as normal
100001 reject Risk hit Follow your hit-handling flow (block, log a risk event, etc.)
100002 reject KYT screening did not complete; reason in data.output.data.message Does not indicate a risk hit

Two core fields in the response:

Approve (100000)

{
  "apiCode": 200,
  "apiMsg": "OK",
  "data": {
    "executionId": "exec-7208f03b931e4897",
    "requestId": "req-1780120490039",
    "sync": true,
    "status": "COMPLETED",
    "output": {
      "code": 100000,
      "action": "approve",
      "data": {
        "kytScreenResults": [
          {
            "address": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
            "chain_name": "TRON",
            "risk_level": "low",
            "risk_score": 0,
            "is_detected": false
          }
        ]
      }
    }
  }
}

Reject — risk hit (100001)

{
  "apiCode": 200,
  "apiMsg": "OK",
  "data": {
    "executionId": "exec-8e7624be1d8d439a",
    "requestId": "req-1780120982607",
    "sync": true,
    "status": "COMPLETED",
    "output": {
      "code": 100001,
      "action": "reject",
      "data": {
        "kytScreenResults": [
          {
            "address": "TJJLiCdAEN3Ve4aHdgDeUgPnccJki3qCR7",
            "chain_name": "TRON",
            "risk_level": "critical",
            "risk_score": 90,
            "is_detected": true
          }
        ]
      }
    }
  }
}

KYT screening not completed (100002)

kytScreenResults is an empty array and data.output.data.message gives the reason (only present in this case).

{
  "apiCode": 200,
  "apiMsg": "OK",
  "data": {
    "executionId": "exec-417cec75a77046d6",
    "requestId": "req-1780138193703",
    "sync": true,
    "status": "COMPLETED",
    "output": {
      "code": 100002,
      "action": "reject",
      "data": {
        "message": "Wallet address does not match the specified chain",
        "kytScreenResults": []
      }
    }
  }
}

Response Fields

Field Type Description
apiCode Number Gateway-layer return code; 200 means the request was accepted. Other values: see Error Codes in the OpenAPI Reference.
apiMsg String Gateway-layer message
data.executionId String Unique ID of the workflow execution instance
data.requestId String Trace ID for this single request
data.sync Boolean Whether the response is synchronous; always true for this endpoint
data.status String Workflow execution status; COMPLETED on normal completion
data.output.action String Decision: approve / reject (note: reject does not necessarily mean a risk hit — see the business-code table above)
data.output.code Number Business code; see the table above (100000 / 100001 / 100002)
data.output.data.message String Returned only when code = 100002; explains why KYT screening did not complete
data.output.data.kytScreenResults Array KYT on-chain screening results; length rule as above (empty array when code = 100002)

kytScreenResults[] fields

Field Type Description
address String The wallet address that was screened (when both walletAddress + txHash are provided, each record's address differs — use this field to tell them apart)
chain_name String Chain identifier, matching data.chain in the request
risk_level String Risk level, e.g. low, critical
risk_score Number Risk score
is_detected Boolean Whether this address hit a risk

Code Examples

All examples follow the same principle: serialize the request body to a string once, compute the signature on that string, then send the very same string as the HTTP body. The example body corresponds to Variant 1 (walletAddress only); to call with Variant 2 or 3, just swap the walletAddress / txHash fields — the rest of the code is unchanged.

Java:

import java.net.URI;
import java.net.URLEncoder;
import java.net.http.HttpClient;
import java.net.http.HttpRequest;
import java.net.http.HttpResponse;
import java.nio.charset.StandardCharsets;
import java.util.HexFormat;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;

public class Web3TransactionDemo {
    private static final String GATEWAY = "https://api-gateway-platform.width.com/gateway";
    private static final String API_KEY = System.getenv("WIDTH_API_KEY");
    private static final String API_SECRET = System.getenv("WIDTH_API_SECRET");
    private static final String DOMAIN_ID = System.getenv("WIDTH_DOMAIN_ID");

    public static void main(String[] args) throws Exception {
        String body = """
                {"eventType":"WEB3_TRANSACTION_EVENT","source":"API","data":{\
                "chain":"TRON",\
                "token":"USDT",\
                "walletAddress":"TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",\
                "customerId":"demo-customer-001",\
                "clientTransactionId":"tx-demo-001",\
                "amount":1000,\
                "transactionCurrencyId":"USDT"}}""";

        String sign = sign(body, API_SECRET);

        String url = GATEWAY + "?api=workflow.apply.submit"
                + "&apiVersion=1.0.0"
                + "&apiKey=" + URLEncoder.encode(API_KEY, StandardCharsets.UTF_8)
                + "&sign=" + sign;

        HttpRequest request = HttpRequest.newBuilder(URI.create(url))
                .header("Content-Type", "application/json")
                .header("x-domain", DOMAIN_ID)
                .POST(HttpRequest.BodyPublishers.ofString(body, StandardCharsets.UTF_8))
                .build();

        HttpResponse<String> response = HttpClient.newHttpClient()
                .send(request, HttpResponse.BodyHandlers.ofString(StandardCharsets.UTF_8));

        System.out.println("HTTP " + response.statusCode());
        System.out.println(response.body());
    }

    private static String sign(String body, String secret) throws Exception {
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(new SecretKeySpec(secret.getBytes(StandardCharsets.UTF_8), "HmacSHA256"));
        byte[] hash = mac.doFinal(body.getBytes(StandardCharsets.UTF_8));
        return HexFormat.of().formatHex(hash);
    }
}

Python:

import hmac
import hashlib
import json
import os
import requests

GATEWAY = "https://api-gateway-platform.width.com/gateway"
API_KEY = os.environ["WIDTH_API_KEY"]
API_SECRET = os.environ["WIDTH_API_SECRET"]
DOMAIN_ID = os.environ["WIDTH_DOMAIN_ID"]

payload = {
    "eventType": "WEB3_TRANSACTION_EVENT",
    "source": "API",
    "data": {
        "chain": "TRON",
        "token": "USDT",
        "walletAddress": "TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw",
        "customerId": "demo-customer-001",
        "clientTransactionId": "tx-demo-001",
        "amount": 1000,
        "transactionCurrencyId": "USDT",
    },
}

# Serialize once. Sign and send use the same bytes.
body = json.dumps(payload, separators=(",", ":"), ensure_ascii=False)

sign = hmac.new(
    API_SECRET.encode("utf-8"),
    body.encode("utf-8"),
    hashlib.sha256,
).hexdigest()

response = requests.post(
    GATEWAY,
    params={
        "api": "workflow.apply.submit",
        "apiVersion": "1.0.0",
        "apiKey": API_KEY,
        "sign": sign,
    },
    headers={
        "Content-Type": "application/json",
        "x-domain": DOMAIN_ID,
    },
    data=body.encode("utf-8"),  # same bytes used when signing
)

print(response.status_code, response.text)

Node.js:

const crypto = require('crypto');

const GATEWAY = 'https://api-gateway-platform.width.com/gateway';
const API_KEY = process.env.WIDTH_API_KEY;
const API_SECRET = process.env.WIDTH_API_SECRET;
const DOMAIN_ID = process.env.WIDTH_DOMAIN_ID;

const payload = {
  eventType: 'WEB3_TRANSACTION_EVENT',
  source: 'API',
  data: {
    chain: 'TRON',
    token: 'USDT',
    walletAddress: 'TZ73Ue3KUP4PZwZJoj9v71skBShdCXksSw',
    customerId: 'demo-customer-001',
    clientTransactionId: 'tx-demo-001',
    amount: 1000,
    transactionCurrencyId: 'USDT',
  },
};

// Serialize once. Sign and send use the same bytes.
const body = JSON.stringify(payload);

const sign = crypto
  .createHmac('sha256', API_SECRET)
  .update(body, 'utf8')
  .digest('hex');

const url = new URL(GATEWAY);
url.searchParams.set('api', 'workflow.apply.submit');
url.searchParams.set('apiVersion', '1.0.0');
url.searchParams.set('apiKey', API_KEY);
url.searchParams.set('sign', sign);

(async () => {
  const response = await fetch(url, {
    method: 'POST',
    headers: {
      'Content-Type': 'application/json',
      'x-domain': DOMAIN_ID,
    },
    body,
  });
  console.log(response.status, await response.text());
})();

Error Codes

Gateway errors use apiCode: 999001 means a signature error; 999302 means an invalid API name/version; 999305 means an invalid or disabled API key; 999401 means service timeout. Correct credentials or signing before retrying, and query recovery after a potentially accepted submit. Business outcomes use data.output.code: 100000 (no risk hit), 100001 (risk hit), and 100002 (KYT screening incomplete). A 100002 outcome must not be treated as a completed clean screening.

Gateway-layer error codes (apiCode, the 9999xx series) are listed under OpenAPI Reference → Error Codes and apply to every endpoint. Business-layer result codes (data.output.code, the 10000x series) are specific to this endpoint — see the table at the top of Response above.

FAQ

Q: My signature keeps failing — what should I check first?

A: 99% of SIGN_ERROR cases come from a mismatch between the body bytes used to sign and the body bytes actually sent. Make sure you: build the JSON string → compute the signature on it → send the same string as the HTTP body. Between signing and sending, do not pretty-print, do not serialize again, do not append a trailing newline.

Q: Must the request body be minified (whitespace stripped)?

A: No. The body may keep any newlines/indentation, as long as the bytes you sign and the bytes you send are exactly the same.

Q: Can I screen an address before the transaction is broadcast on-chain (pre-transaction screening)?

A: Yes. Use Variant 1 — provide walletAddress only, without txHash. Screening is keyed on the address itself, so no on-chain transaction is required: screen the counterparty address first, then decide whether to proceed with the transfer.

Web3 customerId requirement remains pending engineering confirmation. Keep a stable customerId in requests so recovery can use the same eventType, source and customerId. The examples above include it.

Continuous Monitoring

Asynchronous review for existing customers. Always non-blocking. Use eventType: "PERIODIC_FLOW_EVENT".

Endpoint: workflow.apply.submit POST

Request

{
  "eventType": "PERIODIC_FLOW_EVENT",
  "source": "API",
  "data": {
    "customerId": "C_abc123",
    "customerType": "INDIVIDUAL",
    "name": "John Smith",
    "email": "john@example.com",
    "dateOfBirth": "1990-01-15",
    "countryOfNationality": "SG",
    "idType": "NATIONAL_ID",
    "idNumber": "S9012345A",
    "riskLevel": "LOW",
    "startTime": 1776243366000,
    "lastStartTime": 1776243366000

  }
}

Response

Returns immediately with executionId. Poll workflow.execution.status until terminal state; read the Case decision (caseStatus / riskScore) as described in Workflow Lifecycle API → Status & Case Decision.

{
  "apiCode": 200,
  "data": {
    "executionId": "exec-7a3f1b2c4d5e6f78",
    "requestId": "req-1776243366618",
    "status": "RUNNING"
  }
}

Full data field reference

Field Type Description
customerId String Your unique customer identifier
customerType String INDIVIDUAL / CORPORATE
name String Full name or company name
phone String E.164 format
email String Customer email
salutation String Mr / Ms / Mrs
aliasName String Known alias or trading name
formerRegisteredName String Previous registered name
sex String MALE / FEMALE
dateOfBirth String YYYY-MM-DD
countryOfNationality String ISO 2-letter
countryOfResidence String ISO 2-letter
countryOfBirth String ISO 2-letter
address String Full address
idType String NATIONAL_ID / INTERNATIONAL_PASSPORT / DRIVING_LICENCE
idNumber String Document number
idIssuedDate String YYYY-MM-DD
idExpiryDate String YYYY-MM-DD
companyName String Corporate only
entityType String From config avira_entity_types
incorporationNumber String Corporate only
incorporationDate String YYYY-MM-DD, corporate only
incorporationExpiry String YYYY-MM-DD, corporate only
countryOfIncorporation String ISO 2-letter, corporate only
countryOfOperations String ISO 2-letter, corporate only
imoNumber String IMO number for shipping entities
isIncorporated Boolean Whether entity is incorporated
isRegulatedEntity Boolean Whether entity is regulated
ownershipStructureLayer String From config ownership_structure_layer
industry String From config avira_ssic
occupation String From config avira_ssoc
website String Company website URL
onboardingMode String ONLINE / OFFLINE
paymentMode String BANK_TRANSFER / CASH
productComplexity String From config product_service_complexity
bankAccounts String Associated bank account info
transactionBehaviour String From config transaction_behaviour
riskLevel String LOW / MEDIUM / HIGH
startTime Long Epoch ms — current review start
customerCategory String RETAIL / CORPORATE
lastStartTime Long Epoch ms — previous review start
Monitoring event-list confirmation: the guide documents PERIODIC_FLOW_EVENT through workflow.apply.submit, but omits it from the general submit eventType table. This monitoring example follows the monitoring section; confirm tenant support and the authoritative accepted-value list with engineering before enabling it in production.