📘 Public beta · Endpoints are stable; OpenAPI specs and SDKs ship monthly. See changelog →
Products
AML Platform
Channels (ingest)

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
}
FieldMeaning
laneonboarding (customer creation events) or transaction (per-event monitoring).
statusactive (accepting events) · paused (returns 409) · pending (created but not yet activated).
samplingRate0.0–1.0. Transaction lane only — fraction of inbound events evaluated. Onboarding always evaluates 100%.
config.fieldMappingObject mapping canonical field path → JSONPath into your raw payload.
config.samplePayloadOptional. 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

FieldReq'dTypeDescription
customer.nikrequiredstring16-digit Indonesian national ID.
customer.full_namerequiredstringAs printed on the KTP.
customer.dobrecommendeddateYYYY-MM-DD. Used for SAR + watchlist disambiguation.
customer.genderoptionalenumM or F. Cross-checks NIK structure.
customer.nationalityrecommendedstringISO-3166-1 alpha-2. Default ID. Triggers WNA-specific rules.
customer.emailrecommendedstringAdverse-media cross-check.
customer.phonerecommendedstring+62 / 08 prefix mobile.
customer.occupationrecommendedstringFree-text. Higher-risk professions score higher.
customer.income_idroptionalnumberSelf-declared annual income. Drives affordability + unusual-activity rules.
customer.kyc_leveloptionalnumber1–4 (basic → enhanced). Routes per-tier rule pack.
address.cityrecommendedstringKota / Kabupaten of residence.
address.provincerecommendedstringProvinsi (e.g. DKI Jakarta, Jawa Barat).
address.countryoptionalstringISO-3166-1 alpha-2. FATF high-risk-country rules fire on non-ID values.

Transaction lane

FieldReq'dTypeDescription
customer.idrequiredstringInternal customer / CIF — must already exist in the AML customer table.
transaction.amount_idrrequirednumberWhole rupiah. Triggers PER-11 cash-threshold rules above the org threshold.
transaction.directionrequiredenumcredit (incoming) or debit (outgoing).
transaction.channelrequiredenumbi_fast · rtgs · swift · qris · internal · card · atm · topup_ewallet.
transaction.currencyrecommendedstringISO-4217. Default IDR.
transaction.timestamprecommendeddateISO-8601 of the event. Drives velocity windows.
transaction.external_idrecommendedstringYour transaction ID. Used for idempotency (24h dedup window).
transaction.is_cross_borderrecommendedbooleantrue when funds cross IDN border. Triggers FATF / corridor rules.
transaction.countryoptionalstringISO-3166-1 alpha-2. Geo-risk + FATF.
transaction.cityoptionalstringCity of origin.
transaction.purposeoptionalstringFree-text berita. Indexed for SAR narrative.
counterparty.namerecommendedstringTriggers counterparty screening if enabled.
counterparty.accountrecommendedstringBeneficiary account / wallet ID.
counterparty.nikoptionalstringStrong dedup signal for SAR.
counterparty.bankoptionalstringBank code or name (BCA, MANDIRI, BRI, BNI).
counterparty.countryoptionalstringISO-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_ktpcustomer.nik, nominaltransaction.amount_idr, bank_tujuancounterparty.bank, etc.). Hand-edit the rest.

Create + list channels

GET/api/channels
Auth · API keyScope · read

Filters: lane (onboarding · transaction). Legacy channels without a lane bind appear in both lanes — set a lane via PATCH before enabling ingest.

POST/api/channels
Auth · API keyScope · write
{
  "name": "Core banking — transfers",
  "lane": "transaction",
  "samplingRate": 1.0,
  "bundleId": "id.bankacme.coresys"
}
PATCH/api/channels/{id}
Auth · API keyScope · write

Use to set/edit fieldMapping, toggle status, or tune samplingRate.

Ingest

POST/api/channels/{id}/ingest
Auth · API keyScope · writeRate limit · 600/min

Body:

FieldTypeRequiredNotes
payloadobjectyesYour raw bank event — any shape. The channel's fieldMapping resolves it.
persistbooleannoTransaction lane only. Default true. When false, the engine evaluates without writing a Transaction / Alert row. Use for dry-run sanity checks.
tracebooleannoWhen true, the response includes a resolution block showing how each canonical field was resolved. Use during integration.
externalIdstringnoOnboarding 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

StatusBodyWhen
400Invalid JSON bodyBody wasn't JSON.
400Validation failed + Zod error treepayload missing or wrong type.
403Maker role or higher required to ingest eventsAPI key's role is viewer.
404Channel not foundWrong channel ID for this org.
409Channel is paused; only active channels accept ingestChannel is paused or pending.
422Channel is not lane-boundChannel has no lane set — open in dashboard and bind to a lane.
422channel field mapping is missing required canonical fields + structured detailsOne or more required canonical fields aren't mapped or resolved.
422canonical 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:

ActionWhen
channel.ingest.alert_emittedTransaction-lane ingest produced an alert.
channel.ingest.customer_createdOnboarding-lane ingest created a new customer.
channel.ingest.screening_runOnboarding-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.