Collection channels
Quick start

OneView documentation

Collection channels

Static account, wallet and mobile money collections, the wallet counterparty, and the collection account.

Collection channels

Use these examples when you collect money on a merchant's behalf: a payer pays, and you credit the merchant. Three payment methods describe the rails this happens on, and they do not collect the money the same way. A static account transfer arrives in an account issued for collections. A wallet or mobile money payment is collected through the provider and credited to the merchant, with no account in between.

paymentMethodCollection channelCounterparty type
STATIC_ACCOUNTA transfer into a static bank account issued for collections.EXTERNAL_BANK
WALLETA payment collected through a wallet provider's own integration, off the bank rails, such as OPay.WALLET
MOBILE_MONEYA payment collected through a mobile money provider, such as MTN Mobile Money.WALLET

They sit alongside the ten existing methods: NIP, NEFT, RTGS, SWIFT, INTERNAL_TRANSFER, CARD, CASH, CHEQUE, DIRECT_DEBIT and STANDING_ORDER.

Collections are INBOUND: money arrives. Every value on this page is synthetic.

The payment method is the rail, the provider is data

Every payment method is a rail, never a provider. WALLET means the money was collected through a wallet provider's own integration, off the bank rails. MOBILE_MONEY means it came from a mobile money wallet. Neither value names the provider that moved the money.

The provider is data, sent on the counterparty: counterparty.institutionCode carries its code and counterparty.institutionName carries the readable name. An OPay collection is paymentMethod: "WALLET" with your code for OPay in counterparty.institutionCode. An MTN collection is paymentMethod: "MOBILE_MONEY" with your code for MTN.

That split is what keeps the contract stable as you add providers. A new wallet provider needs a provider code, not a new payment method. When you start collecting through a second provider, send its code in counterparty.institutionCode and keep paymentMethod as WALLET: there is no new enum value to wait for, no contract change, and no OneView release. Rules, blacklist entries and reports written per provider work from that provider's first transaction, because they all match on the code. Agree the codes before you integrate: see Agree an institution code convention below.

The two wallet rails stay separate rather than collapsing into one because regulatory report codes are mapped per payment method, and a wallet provider's own integration does not necessarily map the same way as a mobile money wallet.

Three parties on a static account transfer, two on the wallet rails

How many parties a collection carries depends on the rail. A static account transfer carries three, and rules can be written against each of them independently.

PartyWhere it goesWhat it is
The merchantaccountHolderYour own customer, the one you collect for. accountHolder.accountNumber is the merchant's settlement account.
The collection accountcollectionAccountThe account the payer paid into, which you hold on the merchant's behalf.
The payercounterpartyWhoever paid in. counterparty.accountNumber is the payer's own account.

Keep those three apart. The collection account is neither the merchant's settlement account nor the payer's account, which is why it has an object of its own rather than a key inside either.

A wallet or mobile money collection carries two parties: the merchant in accountHolder and the payer in counterparty. The payer pays through the provider, so no account of yours sits between them and there is no collection account to describe. That is why collectionAccount belongs to STATIC_ACCOUNT alone.

Static account transfers

A payer transfers from their bank into a static account you issued for collections. This is the only one of the three channels that carries a bank account on both sides, so the payer is an ordinary EXTERNAL_BANK counterparty, with accountName required as usual.

collectionAccount is required on this combination.

POST /api/transactions
Authorization: Bearer <oneview-service-account-api-key>
Content-Type: application/json

{
  "transactionReference": "TXN-STC-20260922-000412",
  "externalReference": "CBS-STC-000412",
  "direction": "INBOUND",
  "accountHolderType": "CUSTOMER",
  "paymentMethod": "STATIC_ACCOUNT",
  "channel": "API",
  "status": "POSTED",
  "isCrossBorder": false,
  "amount": "250000.00",
  "currency": "NGN",
  "narration": "Order 20260922-0412",
  "executedAt": "2026-09-22T10:15:04Z",
  "accountHolder": {
    "customerUniqueId": "MRC-4471",
    "accountNumber": "9988776655",
    "accountName": "Brightpath Demo Ventures Limited",
    "entityType": "CORPORATE",
    "riskLevel": "LOW"
  },
  "collectionAccount": {
    "accountNumber": "7712340098",
    "accountName": "Demo Payer Adebayo",
    "accountType": "STATIC"
  },
  "counterparty": {
    "counterpartyType": "EXTERNAL_BANK",
    "accountName": "Demo Payer Adebayo",
    "accountNumber": "0123456789",
    "institutionCode": "999999",
    "country": "NG",
    "counterpartyFingerprint": "PAYER-0123456789-999999"
  },
  "telemetry": {
    "ipAddress": "198.51.100.17",
    "deviceId": "device-demo-77",
    "country": "NG"
  }
}
  • accountHolder is the merchant, and accountHolder.accountNumber is the merchant's settlement account, not the account the payer paid into.
  • collectionAccount.accountName is usually the name the static account was issued in, which is often the payer's own name. It is free text for analysts, not a stable identifier.
  • counterpartyFingerprint is your own stable token for the payer. It lets payer rules follow the same person even when account details vary.

The collection account object

FieldRequiredWhat it means
accountNumberYesThe collection account the money landed in. Up to 64 characters: letters, numbers, ., _, :, / and -, starting with a letter or number. A blank value is rejected as if it were missing.
accountNameNoThe name the account was issued in.
accountTypeNoYour own label for the kind of collection account, such as STATIC.

Extra keys are stored as sent, like the other party objects. The object can hold up to 64 keys, with string values of at most 512 characters.

collectionAccount is accepted only when paymentMethod is STATIC_ACCOUNT and direction is INBOUND. It is required there and rejected on every other combination, so a missing one and a misplaced one are both validation failures. Sending "collectionAccount": null counts as not sending it at all: it does not satisfy the requirement on a static account transfer, and it does not trip the rejection on any other channel. See Errors specific to a collection below for each message.

Wallet collections, such as OPay

A payer pays through a wallet provider's own integration, outside the bank rails, so there is no payer bank account to send. This example is an OPay collection: the rail is WALLET and OPay itself is identified by its institution code in counterparty.institutionCode. The payer is a wallet counterparty, counterpartyType: "WALLET", identified by their email address. See The wallet counterparty below. Do not send collectionAccount: this is not a static-account transfer.

POST /api/transactions
Authorization: Bearer <oneview-service-account-api-key>
Content-Type: application/json

{
  "transactionReference": "TXN-OPY-20260922-000517",
  "direction": "INBOUND",
  "accountHolderType": "CUSTOMER",
  "paymentMethod": "WALLET",
  "channel": "API",
  "status": "POSTED",
  "isCrossBorder": false,
  "amount": "42500.00",
  "currency": "NGN",
  "narration": "Order 20260922-0517",
  "executedAt": "2026-09-22T11:02:36Z",
  "accountHolder": {
    "customerUniqueId": "MRC-4471",
    "accountNumber": "9988776655",
    "accountName": "Brightpath Demo Ventures Limited",
    "entityType": "CORPORATE",
    "riskLevel": "LOW"
  },
  "counterparty": {
    "counterpartyType": "WALLET",
    "email": "demo.payer@example.com",
    "accountName": "Demo Payer Chidi",
    "institutionCode": "OPAY",
    "institutionName": "OPay",
    "country": "NG"
  },
  "telemetry": {
    "ipAddress": "198.51.100.44",
    "country": "NG"
  }
}
  • paymentMethod is WALLET, the rail. institutionCode and institutionName identify the provider, OPay here. A second wallet provider is a second institutionCode on this same rail, not a second payment method.
  • The email address is the payer's identity here. A phone number captured when a payment starts can differ from the one that completes it, so it is not a reliable key.
  • If you send email, OneView derives the same fingerprint that emailFingerprint would give for the lowercased, trimmed address, so payer rules behave the same either way. Send emailFingerprint alone if you would rather the address never reach the database; send email if analysts need to see it while investigating.
  • accountName is optional here. Send it as soon as you can capture it.

Mobile money

A payer pays from a mobile money wallet. The payer's phone number in international format is both their identity and, in practice, their account, so it goes in counterparty.accountNumber. Do not send collectionAccount.

POST /api/transactions
Authorization: Bearer <oneview-service-account-api-key>
Content-Type: application/json

{
  "transactionReference": "TXN-MOM-20260922-000318",
  "direction": "INBOUND",
  "accountHolderType": "CUSTOMER",
  "paymentMethod": "MOBILE_MONEY",
  "channel": "API",
  "status": "POSTED",
  "isCrossBorder": true,
  "amount": "1850.00",
  "currency": "GHS",
  "usdExchangeRate": "15.5",
  "narration": "Order 20260922-0318",
  "executedAt": "2026-09-22T12:44:10Z",
  "accountHolder": {
    "customerUniqueId": "MRC-4471",
    "accountNumber": "9988776655",
    "accountName": "Brightpath Demo Ventures Limited",
    "entityType": "CORPORATE",
    "riskLevel": "LOW"
  },
  "counterparty": {
    "counterpartyType": "WALLET",
    "accountNumber": "233201234567",
    "accountName": "Demo Payer Ama",
    "institutionCode": "MTN_MOMO_GH",
    "institutionName": "MTN Mobile Money Ghana",
    "country": "GH"
  },
  "telemetry": {
    "deviceId": "device-demo-88",
    "country": "GH"
  }
}
  • paymentMethod is MOBILE_MONEY, the rail. institutionCode and institutionName identify the provider, MTN Mobile Money Ghana here, exactly as they do on the wallet rail. Use the same code for the same provider on every transaction. See Agree an institution code convention below.
  • currency is GHS, which differs from the NGN base currency in these examples, so usdExchangeRate is required. The rate is how many units of the transaction currency buy 1 USD: 1850.00 / 15.5 = 119.3548 USD. Sending the inverse rate is accepted and silently wrong. See The exchange rate direction in External bank counterparty.
  • isCrossBorder is your own fact about whether the money crossed a national border. It is not derived from the currency.

The wallet counterparty

WALLET covers an e-money or mobile money payer, and paymentMethod says which rail they paid on.

WALLET is both a payment method and a counterparty type. That is deliberate, and it works the same way as CARD, which is already both: the payment method says how the money moved, the counterparty type says what the other side is. The two are not always the same word, because a mobile money collection is paymentMethod: "MOBILE_MONEY" with counterpartyType: "WALLET".

The type inverts two rules that hold for every other counterparty type, so read both carefully.

Its name is optional. Every other counterparty type requires its own name field: accountName, merchantName or billerName. WALLET does not, because a wallet payer's name is not always captured at the point of payment. accountName is still accepted and still used, so send it as soon as your system can capture it. Nothing else has to change when you start.

An identifier is required. A WALLET counterparty must carry at least one of accountNumber, email, emailFingerprint or counterpartyFingerprint. Without one of them the payer cannot be tracked from one transaction to the next, so every rule that counts a payer's activity would quietly never match instead of failing. A blank string does not count.

A request that carries none of them is rejected:

{
  "message": "Transaction payload validation failed",
  "status": false,
  "statusCode": 400,
  "code": "VALIDATION_FAILED",
  "errors": [
    {
      "field": "counterparty",
      "code": "required",
      "message": "a WALLET counterparty requires at least one of: accountNumber, email, emailFingerprint, counterpartyFingerprint"
    }
  ]
}

A payer identified on the wallet rail by email is not linked to the same person paying by mobile money, because the two rails identify people differently. Plan your rules per rail.

Before you integrate

Send a USD exchange rate for every non-base-currency transaction

Any transaction in a currency other than your configured base currency needs usdExchangeRate, on every transaction, not once per day or per batch. The same applies to any cross-border transaction that is not in USD. A request that needs a rate and does not carry one is rejected with usdExchangeRate is required for cross-border or foreign-currency transactions.

Agree an institution code convention

counterparty.institutionCode is what identifies the provider on both wallet rails, so it is what your rules, blacklist entries and reports match on. Nigerian institutions have published NIBSS codes. Several other countries have no equivalent registry for wallet or mobile money providers, so agree a stable code for each provider before you integrate, and use it consistently. If the same provider arrives as more than one code, it is split across every rule and every blacklist entry, and each fragment looks less active than the provider really is. Changing the convention later does not repair transactions already stored.

Two things to settle when you pick the codes. Case and surrounding spaces do not matter: opay and OPay, with or without spaces around them, are the same provider as OPAY, which is the form stored and shown to analysts. And keep each code to 64 characters or fewer: a longer one is still stored with the transaction, but rules that count activity per provider will not count it.

Response

This is the response to the static account transfer above:

{
  "status": true,
  "message": "Transaction evaluated",
  "data": {
    "transaction": {
      "id": "6b0f4d27-91ac-4e53-8c72-3f5a1d80b4e9",
      "transactionReference": "TXN-STC-20260922-000412",
      "direction": "INBOUND",
      "receivedAt": "2026-09-22T10:15:05.146Z",
      "replayed": false
    },
    "decision": {
      "outcome": "PASS",
      "riskScore": 0,
      "evaluatedAt": "2026-09-22T10:15:05.159Z",
      "evaluationMs": 8.271,
      "complete": true,
      "rulesEvaluated": 18,
      "rulesetSnapshot": "9ffa3b395a9799b1a1104ab1b536ed1667b487510829ceefc27593941fafb35e",
      "gradingVersion": 3,
      "triggeredRules": []
    }
  }
}

Status is 201 for a new transaction, 200 for a replay. The wallet and mobile money examples return the same shape: a collection is an ordinary transaction once it is accepted, and a rule can hold or block one exactly as it can any other. See Decision responses for every outcome and what your system must do with it.

The response never echoes collectionAccount back, and it never carries the usdAmount derived from usdExchangeRate on the mobile money example. The collection account is stored: when a collection is read back in detail, or opened by an analyst on the transaction detail page, it appears alongside the account holder and counterparty snapshots. It is null for every transaction that never carried one.

Errors specific to a collection

Causefieldcodemessage
A WALLET counterparty carries none of accountNumber, email, emailFingerprint and counterpartyFingerprintcounterpartyrequireda WALLET counterparty requires at least one of: accountNumber, email, emailFingerprint, counterpartyFingerprint
A STATIC_ACCOUNT INBOUND transaction has no collectionAccount, or sends null in its placecollectionAccountrequiredcollectionAccount is required when paymentMethod is STATIC_ACCOUNT and direction is INBOUND
collectionAccount is sent on any other payment method or direction, such as a wallet or mobile money collectioncollectionAccountnot_acceptedcollectionAccount is only accepted when paymentMethod is STATIC_ACCOUNT and direction is INBOUND
collectionAccount has no accountNumber, or it is blankcollectionAccount.accountNumberrequiredcollectionAccount.accountNumber is required
collectionAccount.accountNumber has a disallowed character, or is longer than 64 characterscollectionAccount.accountNumberformatcollectionAccount.accountNumber has an unsupported format
The static account payer has no accountNamecounterpartyrequiredcounterparty.accountName is required for this counterparty type
A collection in a currency other than your base currency has no usdExchangeRateusdExchangeRaterequiredusdExchangeRate is required for cross-border or foreign-currency transactions

A misspelled rail is an ordinary enum failure, not a collection-specific one: MOMO is rejected with paymentMethod must be one of: NIP, NEFT, RTGS, SWIFT, INTERNAL_TRANSFER, CARD, CASH, CHEQUE, DIRECT_DEBIT, STANDING_ORDER, STATIC_ACCOUNT, WALLET, MOBILE_MONEY. See Errors for the complete catalogue.

Idempotency

Deduplication is by transactionReference and direction, the same as every transaction type. Collections are always INBOUND, so in practice the reference alone identifies one. This matters more here than on most pages because a collection usually reaches you from a provider notification rather than from your own core banking system, and those are retried: resending the identical body returns 200 with replayed: true and the original decision, without crediting the merchant a second time.

A resubmitted different body under the same reference and direction is rejected with 409 IDEMPOTENCY_CONFLICT. Two versions of that are easy to walk into on a collection: resending with collectionAccount.accountName filled in once the payer's name arrives, and resending with a corrected usdExchangeRate. Neither is a replay. Send the complete body the first time, and give a genuinely new collection a new reference. Omitting collectionAccount and sending "collectionAccount": null are the same request, so a retry that switches between the two is still a replay. See Idempotency in Submitting transactions.

A wallet payer is screened only when you send a name

PEP and sanction screening resolves one name per party from firstName/lastName, then companyName, then accountName (see How PEP and sanction screening picks the name in Customer account holder). WALLET is the only counterparty type with no required name field, so a wallet payer sent with an identifier and nothing else carries nothing for screening to read, and is not screened by name at all. That is not an error, and rules and blacklist matching on the identifier are unaffected. Sending accountName, as both wallet examples above do, is what puts the payer in front of screening: it is the practical reason to capture a payer's name even though the field is optional.

Only the account holder and the counterparty are screened. The collection account is not, even though collectionAccount.accountName usually holds a person's name. It is the name the static account was issued in, held as free text for analysts, and OneView does not treat it as a party.