Skip to Content
Core ConceptsPayment Rails

Payment Rails

rail is an optional field on a destination account that states which local payment scheme reaches that destination. It exists because one country runs more than one: in Colombia the same 10-digit number can be a bank account or its owner’s document-based Bre-B key, and those are two different destinations.

Everywhere else the bank rail is the only option, so the field carries no signal and you should leave it out.

You declare it when you create the destination; k3 stores it on the account and forwards it with every payout, so the settlement provider does not have to work it out for itself. Nothing else changes: the rail is a declaration you make and Koywe passes along. The Koywe dashboard already declares it for every Colombian destination created there, so what follows is for API callers.

Optional everywhere, and backwards compatible: the field is never required. Accounts created before it existed have no rail, and a bank destination created without one behaves exactly as before. Where it does matter is a Colombian Bre-B key, which needs rail: "BREB" to be stored and settled as a key — see Creating a Bre-B destination.


Values

ValueMeaningWhere
BREBaccountNumber carries a Bre-B key, not a bank account number.Colombia only
BANKaccountNumber carries a bank account number.Any country
(omitted)Undeclared — no rail is stored. See Omitting the field.Any country

The field is accepted on both destination-creation endpoints. It appears in account responses only when a rail is declared: when there is none, it is left out of the JSON rather than returned as null.

POST /api/v1/organizations/{orgId}/merchants/{merchantId}/contacts/{contactId}/accounts POST /api/v1/organizations/{orgId}/merchants/{merchantId}/accounts

Omitting the field

Leaving rail out is the default, and it is what every account created before the field existed carries. What happens then:

  • At creation, accountNumber is validated as a bank account number: the bank must exist, it must support the account type, and the number length must fit that bank’s range.
  • At payout, k3 sends no rail and the provider resolves it from the destination itself — the behaviour that predates this field. A destination carrying an @, a + or any letter cannot be a Colombian account number, so it is read as a Bre-B key; one made only of digits is read as a bank account.

For bank destinations, in any country, that is exactly right and there is nothing to declare.

The case to watch is a Bre-B key made only of digits — a NIT, a cédula, a phone without the +57 prefix. Neither k3 nor the provider can tell it from an account number, so an undeclared one is stored as a bank account and settles over the bank rail. Keys carrying an @, a + or a letter are usually rejected at creation instead (BAA00020 with type: "VIRTUAL", BAA00021 on length), and recognised by the provider when they do get through. Declaring rail: "BREB" is what makes the outcome the same in every case.


Creating a Bre-B destination

Declare the rail for a Bre-B key. rail: "BREB" states that accountNumber is a key: k3 skips the Colombian bank-number checks, stores the rail on the account, and forwards it on every payout so the provider settles over Bre-B. The field stays optional, but without it the destination is validated — and then settled — as a bank account number.

The shape of an account number cannot always tell a Bre-B key from a bank account: a key that carries an @, a + or a letter is recognisable, but a 10-digit NIT or a phone without the +57 prefix looks exactly like an account number, and guessing wrong sends money to a stranger. Declaring the rail is what removes the guess.

curl -X POST 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/contacts/cnt_abc123/accounts' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "name": "Bre-B Juan Pérez", "kind": "BANK", "isDefault": true, "countrySymbol": "CO", "currencySymbol": "COP", "accountNumber": "@juanperez", "rail": "BREB" }'
const brebDestination = await axios.post( `https://api-sandbox.koywe.com/api/v1/organizations/${orgId}/merchants/${merchantId}/contacts/${contactId}/accounts`, { name: 'Bre-B Juan Pérez', kind: 'BANK', isDefault: true, countrySymbol: 'CO', currencySymbol: 'COP', accountNumber: '@juanperez', // the Bre-B key (here an @alias), as the beneficiary registered it rail: 'BREB' // required for a Bre-B key; exempts it from `entity` and `type` }, { headers: { Authorization: `Bearer ${token}`, 'Content-Type': 'application/json' } } ); console.log(brebDestination.data.rail); // 'BREB' console.log(brebDestination.data.entity); // 'UNKNOWN_BANK' — a key has no bank console.log(brebDestination.data.type); // 'VIRTUAL'

With rail: "BREB", entity and type are optional. A key has no bank and no account type, so there is nothing to invent. Leave them out and the destination is stored with entity: "UNKNOWN_BANK" and type: "VIRTUAL" — that is what you get back on creation and on every later GET. Send them and they are stored as given and never checked against the Colombian bank catalogue; neither one is used to route a Bre-B payout. Without rail: "BREB", Colombia still requires both (BAA00040 / BAA00025).

Bre-B key requirements

  • Length: 6 to 21 characters — the range every provider we settle Colombia through accepts. The API does not validate length at creation: a key outside that range is stored anyway and the rejection only surfaces on the first payout, on the provider’s side. A longer key (a long email address, typically) can be refused by the network depending on who settles the payout, so prefer a short form and check it before going live.
  • Accepted forms: @alias (@juanperez), email (jperez@example.co), phone (+573001234567) or document number (1234567890). Spaces are stripped; a purely numeric key keeps its digits only.
  • The key must belong to the beneficiary. At payout time the key is resolved with the network and the holder’s document number is compared against the contact’s documentNumber (a NIT verification digit on either side is tolerated). A key registered to someone else is rejected, not redirected — so create the contact with documentType and documentNumber filled in.

Colombian bank destinations

For a real bank account in Colombia, rail: "BANK" is optional but worth sending: it states explicitly that accountNumber is an account number rather than a key. Validation is unchanged either way — the bank must exist, and the account type and number length must be valid for it.

{ "name": "Bancolombia COP", "kind": "BANK", "countrySymbol": "CO", "currencySymbol": "COP", "accountNumber": "1234567890", "rail": "BANK", "entity": "BANCOLOMBIA", "type": "SAVINGS" }

Every other country

Leave rail out. BREB outside Colombia is a contradiction and is rejected with BAA00045; BANK is accepted at creation but says nothing, since it is the only rail those markets run.


Declaring the rail on an existing account

PUT the account with the rail you want to declare:

curl -X PUT 'https://api-sandbox.koywe.com/api/v1/organizations/YOUR_ORG_ID/merchants/YOUR_MERCHANT_ID/contacts/cnt_abc123/accounts/acc_def456' \ -H 'Authorization: Bearer YOUR_TOKEN' \ -H 'Content-Type: application/json' \ -d '{ "rail": "BREB" }'

Four rules apply on update:

  • Colombia only. The update surface rejects a rail declared for a country that runs no rail choice, with BAA00046 — there, declaring the bank rail would state the default as if it were a decision.
  • It cannot be cleared. Omitting rail leaves the stored value untouched, and null is rejected. There is no path back to an undeclared rail.
  • It re-runs duplicate detection. The rail is part of a destination’s identity, so declaring or changing one can collide with an account you already have — BAA00012.
  • Moving the rail to BANK revalidates the destination. When a Colombian account’s rail moves to BANK — from BREB or from undeclared — the bank validations run again against the stored account number: the bank must exist (BAA00019), support the account’s type (BAA00020) and accept the number’s length (BAA00021). This is what stops a Bre-B key from being turned into a bank destination, which would settle against whoever holds that number at that bank. Changing entity on a Colombian account that is not Bre-B triggers the same revalidation, since it moves the destination to a different bank.

What it does not revalidate: changing only the name or the swiftCode, repeating the entity already stored, or moving the rail to BREB. The destination does not change in those cases, and a Colombian account predating this check whose stored data would fail it today stays editable.


What each surface validates

Creating and editing do not validate the same things, because they do not carry the same risk. Creation sees a whole destination for the first time; an update only has to account for what the request changes.

ActionOn create (POST)On update (PUT)
Declaring BREBSkips the bank validations on the number, and entity and type stop being required.Accepted in Colombia. Does not revalidate the number: a key is validated by the Bre-B network, not by our catalogue.
Declaring BANKValidates the number against the bank and the account type.Revalidates the stored number (BAA00019 / BAA00020 / BAA00021), as long as the rail actually changes. Re-sending the rail already stored revalidates nothing.
Changing the bank (entity)Revalidates the number against the new bank, unless the destination is Bre-B.
Changing only the name or swiftCodeRevalidates nothing: the destination did not change.
A rail outside the country that runs itBAA00045BAA00045, and BAA00046 in countries with a single rail.
Removing the railJust leave it out.Not possible: null is rejected and omitting it leaves the value untouched.

Do not convert a Bre-B key into a bank destination. That change makes the payout settle against whoever holds that number at that bank. Revalidation stops the usual case — a key that is not a valid account number there — but it cannot stop every one: a NIT whose length fits the bank’s range passes validation, because it is indistinguishable from a real account. If you picked the wrong rail when creating the destination, create a new one rather than converting the existing one.


What the rail does at payout time

The declared rail travels with the payout, so the settlement provider does not have to infer it:

  • BREB — neither the bank code nor the account type is sent, and the account number travels as the key. A key has neither, and the provider validates the destination on the key alone.
  • BANK — the bank code is resolved from entity, as always.
  • Undeclared — nothing is sent, and the provider works the rail out from the destination itself, exactly as before this field existed. This is the path taken by accounts created before the field and by any bank destination that leaves it out. It reaches Bre-B only when the key is visibly not an account number (it carries an @, a + or a letter); an all-digit key settles over the bank rail instead, which is the failure this field exists to prevent.

A stored rail that does not exist in the destination country (a BREB account somehow sitting on a non-Colombian country) refuses the payout with BAA00045 rather than settling it as a guess.


Errors

CodeStatusWhen
BAA00045400The declared rail does not exist in this bank account country — e.g. BREB outside Colombia. Also refuses a payout whose stored rail contradicts the destination country.
BAA00046400Declaring a rail is not supported for this country. Only returned by the update endpoints, for countries that run a single rail.
BAA00012409The declared rail would duplicate an existing destination.
BAA00019404The bank is not in the country’s catalogue. On creating a bank destination, and on moving a Colombian account to rail: "BANK" or changing its bank.
BAA00020400Account type not supported by bank — the usual symptom of a Bre-B key sent without rail: "BREB". Also raised when an update revalidates the destination.
BAA00021400Account number length invalid for bank and account type — same cause as above.
BAA00040400entity missing. Required for Colombia except when you declare rail: "BREB".
BAA00025400type missing. Required for Colombia except when you declare rail: "BREB".

A 201 is not proof the key was stored as a key. An undeclared Bre-B key whose length matches a real account is saved as a bank account. Read rail back from the response, and if it is absent on a destination you meant to be Bre-B, declare it with the PUT above before creating an order against it.

Full list in Error Codes.


Next Steps

Last updated on