Channels (ingest)
A channel is a named ingest endpoint that translates your payload shape into the canonical fields the AML engine understands. You configure a channel once (field mapping + sampling + status) and then POST your raw bank events to a single URL — no need to pre-shape the JSON in your service.
This is the bank-friendly alternative to POST /api/transactions/evaluate (which requires the canonical body shape) and POST /api/screenings (which requires a pre-existing customer row).
Why channels
Most banks already emit transaction or onboarding events in a shape dictated by their core banking system: BSS, T24, Finacle, Equation, or in-house systems. The fields look like no_rek_tujuan, nominal, tgl_transaksi, nik_ktp — not the canonical transaction.amount_idr / customer.nik shape.
With a channel:
- You don't rewrite your event shape — keep emitting what your core system already emits.
- The mapping lives in AML config, not in your code, so compliance can tune it without a deploy.
- One channel can be turned off in seconds (
status: paused) without a kill switch in your service. - Sampling (probabilistic event drop) is configurable per channel, useful for shadow rollouts.
The shape of a channel
{
"id": "ch_01HXY...",
"name": "Core banking — transfers",
"lane": "transaction",
"status": "active",
"samplingRate": 1.0,
"apiKeyPrefix": "qe_ch_",
"bundleId": "id.bankacme.coresys",
"config": {
"fieldMapping": {
"transaction.amount_idr": "$.payload.nominal",
"transaction.direction": "$.payload.tipe_transaksi",
"transaction.channel": "$.payload.saluran",
"transaction.external_id": "$.payload.id_trx",
"transaction.timestamp": "$.payload.tgl_transaksi",
"customer.id": "$.payload.cif",
"counterparty.name": "$.payload.nama_tujuan",
"counterparty.account": "$.payload.no_rek_tujuan",
"counterparty.bank": "$.payload.bank_tujuan"
},
"samplePayload": { /* ... */ }
},
"lastEventAt": "2026-05-25T08:14:22Z",
"eventsLast24h": 12483
}| Field | Meaning |
|---|---|
lane | onboarding (customer creation events) or transaction (per-event monitoring). |
status | active (accepting events) · paused (returns 409) · pending (created but not yet activated). |
samplingRate | 0.0–1.0. Transaction lane only — fraction of inbound events evaluated. Onboarding always evaluates 100%. |
config.fieldMapping | Object mapping canonical field path → JSONPath into your raw payload. |
config.samplePayload | Optional. A captured raw payload used by the dashboard's resolution preview. |
Canonical fields
The destination side of the mapping is fixed. These are the canonical fields the engine reads — the same fields the rule DSL references, and the same fields the SAR XML envelope is built from.
Onboarding lane
| Field | Req'd | Type | Description |
|---|---|---|---|
customer.nik | required | string | 16-digit Indonesian national ID. |
customer.full_name | required | string | As printed on the KTP. |
customer.dob | recommended | date | YYYY-MM-DD. Used for SAR + watchlist disambiguation. |
customer.gender | optional | enum | M or F. Cross-checks NIK structure. |
customer.nationality | recommended | string | ISO-3166-1 alpha-2. Default ID. Triggers WNA-specific rules. |
customer.email | recommended | string | Adverse-media cross-check. |
customer.phone | recommended | string | +62 / 08 prefix mobile. |
customer.occupation | recommended | string | Free-text. Higher-risk professions score higher. |
customer.income_idr | optional | number | Self-declared annual income. Drives affordability + unusual-activity rules. |
customer.kyc_level | optional | number | 1–4 (basic → enhanced). Routes per-tier rule pack. |
address.city | recommended | string | Kota / Kabupaten of residence. |
address.province | recommended | string | Provinsi (e.g. DKI Jakarta, Jawa Barat). |
address.country | optional | string | ISO-3166-1 alpha-2. FATF high-risk-country rules fire on non-ID values. |
Transaction lane
| Field | Req'd | Type | Description |
|---|---|---|---|
customer.id | required | string | Internal customer / CIF — must already exist in the AML customer table. |
transaction.amount_idr | required | number | Whole rupiah. Triggers PER-11 cash-threshold rules above the org threshold. |
transaction.direction | required | enum | credit (incoming) or debit (outgoing). |
transaction.channel | required | enum | bi_fast · rtgs · swift · qris · internal · card · atm · topup_ewallet. |
transaction.currency | recommended | string | ISO-4217. Default IDR. |
transaction.timestamp | recommended | date | ISO-8601 of the event. Drives velocity windows. |
transaction.external_id | recommended | string | Your transaction ID. Used for idempotency (24h dedup window). |
transaction.is_cross_border | recommended | boolean | true when funds cross IDN border. Triggers FATF / corridor rules. |
transaction.country | optional | string | ISO-3166-1 alpha-2. Geo-risk + FATF. |
transaction.city | optional | string | City of origin. |
transaction.purpose | optional | string | Free-text berita. Indexed for SAR narrative. |
counterparty.name | recommended | string | Triggers counterparty screening if enabled. |
counterparty.account | recommended | string | Beneficiary account / wallet ID. |
counterparty.nik | optional | string | Strong dedup signal for SAR. |
counterparty.bank | optional | string | Bank code or name (BCA, MANDIRI, BRI, BNI). |
counterparty.country | optional | string | ISO-3166-1 alpha-2. High-risk-country rules. |
Auto-detect handles the common Indonesian names
The dashboard's Configure Channel modal includes an Auto-detect button. Paste a sample raw payload and it'll suggest mappings for ~140 known Indonesian banking field aliases (nik_ktp → customer.nik, nominal → transaction.amount_idr, bank_tujuan → counterparty.bank, etc.). Hand-edit the rest.
Create + list channels
/api/channelsFilters: lane (onboarding · transaction). Legacy channels without a lane bind appear in both lanes — set a lane via PATCH before enabling ingest.
/api/channels{
"name": "Core banking — transfers",
"lane": "transaction",
"samplingRate": 1.0,
"bundleId": "id.bankacme.coresys"
}/api/channels/{id}Use to set/edit fieldMapping, toggle status, or tune samplingRate.
Ingest
/api/channels/{id}/ingestBody:
| Field | Type | Required | Notes |
|---|---|---|---|
payload | object | yes | Your raw bank event — any shape. The channel's fieldMapping resolves it. |
persist | boolean | no | Transaction lane only. Default true. When false, the engine evaluates without writing a Transaction / Alert row. Use for dry-run sanity checks. |
trace | boolean | no | When true, the response includes a resolution block showing how each canonical field was resolved. Use during integration. |
externalId | string | no | Onboarding lane only. Propagated to Customer.externalId — your application / case ID. |
Transaction lane
curl -X POST https://sandbox.quantumelixir.tech/aml/api/channels/ch_01HXY.../ingest \
-H "Authorization: Bearer $QE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"id_trx": "TRX-2026-05-25-018734",
"cif": "cus_01HXY...",
"nominal": 550000000,
"tipe_transaksi": "kredit",
"saluran": "bi_fast",
"tgl_transaksi": "2026-05-25T08:14:22Z",
"nama_tujuan": "PT Maju Bersama",
"no_rek_tujuan": "1234567890",
"bank_tujuan": "BCA"
},
"trace": true
}'Response (transaction lane, with trace: true):
{
"ok": true,
"data": {
"channelId": "ch_01HXY...",
"channelName": "Core banking — transfers",
"lane": "transaction",
"decision": "review",
"riskScore": 65,
"hits": [
{ "ruleId": "rul_01HXY...", "ruleCode": "TX-100M", "ruleName": "PER-11 cash threshold", "severity": "high", "scoreContribution": 35 }
],
"alertId": "alt_01HXY...",
"transactionId": "txn_01HXY...",
"resolution": {
"resolvedFields": ["amountIdr", "direction", "channel", "externalId", "timestamp", "customerId", "counterpartyName", "counterpartyAccount", "counterpartyBank"],
"defaultedFields": ["currency"],
"canonical": {
"transaction.amount_idr": 550000000,
"transaction.direction": "credit",
"transaction.channel": "bi_fast"
},
"resolutions": [
{ "canonical": "transaction.amount_idr", "jsonPath": "$.payload.nominal", "status": "ok", "value": 550000000 },
{ "canonical": "transaction.direction", "jsonPath": "$.payload.tipe_transaksi", "status": "ok", "value": "credit", "note": "alias 'kredit' → 'credit'" }
]
}
}
}data.decision is what you use to gate the transaction in your core banking system. The optional resolution block is your debugging aid — once your integration is stable, drop trace: true.
Sampling drops are explicit, not silent
When a channel's samplingRate is below 1.0 and the event is dropped, the response includes sampled: true plus decision: "allow", riskScore: 0. Your service can log this as "skipped" without confusing it with a "passed" decision.
Onboarding lane
The onboarding lane does double duty in a single call: it upserts the customer (by NIK) and runs an onboarding-lane screening. No need to call POST /api/customers then POST /api/screenings.
curl -X POST https://sandbox.quantumelixir.tech/aml/api/channels/ch_01HXZ.../ingest \
-H "Authorization: Bearer $QE_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"payload": {
"no_app": "APP-2026-05-25-009281",
"nama_lengkap": "Umar Patek",
"nik_ktp": "3201234567890099",
"tgl_lahir": "1970-07-20",
"warga_negara": "ID",
"email": "umar@example.id",
"no_hp": "+6281234567890",
"pekerjaan": "Trader",
"kota": "Jakarta",
"provinsi": "DKI Jakarta"
},
"externalId": "APP-2026-05-25-009281"
}'Response:
{
"ok": true,
"data": {
"channelId": "ch_01HXZ...",
"channelName": "Loan onboarding — webform",
"lane": "onboarding",
"customer": {
"id": "cus_01HXY...",
"fullName": "Umar Patek",
"nik": "3201234567890099",
"kycLevel": 1,
"externalId": "APP-2026-05-25-009281",
"isNew": true
},
"screening": {
"screeningId": "scr_01HXY...",
"status": "review",
"topMatchScore": 0.95,
"matchCount": 1
},
"screeningError": null
}
}Re-ingesting with the same NIK is non-destructive: the customer row is updated (only filling in nulls + refreshing income/kyc), isNew flips to false, and a new screening row is created. Use this to backfill a customer's KYC fields without overwriting analyst edits.
Onboarding ingest can fail screening — payload still succeeds
If the screening call errors out (e.g. transient downstream issue), the customer is still persisted and the response returns screening: null + screeningError: "...". Your service should retry the screening separately (POST /api/screenings) — don't retry the whole ingest, you'll duplicate audit-log entries.
Validation: mapping completeness
When required canonical fields aren't covered by the channel's mapping, ingest returns HTTP 422 with structured details — no silent failures:
{
"ok": false,
"error": "channel field mapping is missing required canonical fields",
"details": {
"channelId": "ch_01HXY...",
"channelName": "Core banking — transfers",
"missingRequired": [
{ "canonical": "transaction.direction", "reason": "no jsonPath configured" },
{ "canonical": "transaction.channel", "reason": "jsonPath resolved to null" }
],
"mappingHint": "Open the channel in /dashboard/aml/transaction/channels and add JSONPath mappings for the listed canonical fields, then re-ingest.",
"resolutions": [ /* full resolution table */ ]
}
}The same channel-vs-rule completeness check is available offline via GET /api/rules/{id}/coverage — use it in CI to catch a rule activation that would silently underfire.
Error codes
| Status | Body | When |
|---|---|---|
| 400 | Invalid JSON body | Body wasn't JSON. |
| 400 | Validation failed + Zod error tree | payload missing or wrong type. |
| 403 | Maker role or higher required to ingest events | API key's role is viewer. |
| 404 | Channel not found | Wrong channel ID for this org. |
| 409 | Channel is paused; only active channels accept ingest | Channel is paused or pending. |
| 422 | Channel is not lane-bound | Channel has no lane set — open in dashboard and bind to a lane. |
| 422 | channel field mapping is missing required canonical fields + structured details | One or more required canonical fields aren't mapped or resolved. |
| 422 | canonical adapter rejected the resolved payload: ... | Mapping resolved but a value coerced incorrectly (e.g. non-numeric amount). |
Audit log
Every ingest that produces a side effect writes an audit-log entry:
| Action | When |
|---|---|
channel.ingest.alert_emitted | Transaction-lane ingest produced an alert. |
channel.ingest.customer_created | Onboarding-lane ingest created a new customer. |
channel.ingest.screening_run | Onboarding-lane screening succeeded. |
Each entry records actor kind (user · api-key), apiKeyId (when applicable), and the channel ID + name. Filter the audit log by channel.ingest.* actions to reconcile your service's outbound events against AML's view.