Payment Flow
Overview
This guide shows how to integrate Colombia COP flows. You create payouts, pay-in intents, or pay-in keys with a Colombia type, Bitso settles against the rail, and you track progress with webhooks or GET by id.
Core Resources
| Resource | Role |
|---|---|
| Payout | Outbound COP payment (CO_BREB or CO_ACH). |
| Pay-in | Read-only record of funds already received (CO_BREB or CO_PSE), or the refund of a CO_ACH payout (CO_ACH). |
| Pay-in Intent | Expected inbound payment for a single charge (CO_PSE). |
| Pay-in Key | Standing Bre-B alias so payers can send you COP (CO_BREB). |
Every resource uses a top-level type discriminator and a matching nested object whose key is the lowercase form of type (for example type: CO_PSE with a co_pse block).
Step-by-Step
1. Send a Bre-B payout
- Call Preview Payout with
type: CO_BREBto resolve the alias. - Call Create Payout with the preview result (
co_breb.preview_payout_id) to commit the transfer. For Bre-B, do not rely on theIdempotency-Keyheader —preview_payout_idis the idempotency key (one preview commits at most one payout). - Track
PENDING→PROCESSING→COMPLETEDorFAILEDvia webhooks or Get Payout.
2. Send an ACH payout
- Call Create Payout with
type: CO_ACHand the beneficiary bank details. - Track status the same way as Bre-B payouts.
Third-party ACH payouts (co_ach.beneficiary_type: THIRD_PARTY) send the bank account and the beneficiary document in co_ach.third_party. First-party and saved-beneficiary payloads have no document fields. Invalid document_id values return 400 invalid_field.
Use document_type: NIT only for a legal person. Every other type is a natural person.
document_type | document_id |
|---|---|
CC | Digits only, length 3–10. |
CE | Digits only, length 6–10. |
TI | Digits only, length 10–11. |
NIT (legal person) | Digits only, length 9 or 10. Prefer 9 digits without the verifier. 10 digits are accepted and the last digit is omitted. |
PA | Alphanumeric [A-Za-z0-9], length 3–15. |
When using NIT, set given_name and family_name to the same company name. Both fields remain required.
3. Charge a payer with PSE
- Call Create Payin Intent with
type: CO_PSEand aco_pse.bank_code. - Redirect the payer to the
payment_linkfrom the response. - Fulfill on the
payin_intentwebhook (COMPLETED,FAILED, orEXPIRED) — not on the browser redirect alone. - When completed, use
payin_id(or Get Payin) to reconcile the settled deposit.
PSE bank_code:
- Stage: Use
1022so you can complete the PSE test flow in stage. - Production: Use a bank
codefrom the Colombia catalog — GET https://api.bitso.com/v3/banks/co.
4. Receive COP on a Bre-B alias
- Call Create Payin Key with
type: CO_BREBandco_breb.key_type: ALIAS. See Pay-in Keys for first-party omit, natural-person, and legal-person examples. Creation is synchronous and returnsstatus: ACTIVE. - Share the registered alias with payers. When a payer sends funds to the alias, you receive a
payinwebhook and can list or get the pay-in. Whenco_breb.beneficiary_keyis present,idis always set; match it to the pay-in key. When omitted, treat the pay-in as a first-party deposit recorded before this snapshot existed.
5. Match a refunded ACH payout
A returned CO_ACH payout appears as a CO_ACH pay-in. See List payins and Get payin for the refund fields and response shape.
Inbound COP arrives as CO_BREB or CO_PSE. A COP pay-in listing can also contain returned CO_ACH payouts, so branch on type before treating a row as new inbound funds.
PSE Pay-in Diagram
sequenceDiagram
participant M as Merchant
participant B as Bitso
participant P as Payer
participant Bank as Payer bank
M->>B: POST /payments/v1/payin_intents (CO_PSE)
B-->>M: payment_link + Intent ID (PENDING)
M->>P: Redirect to payment_link
P->>Bank: Authenticate and authorize
Bank-->>B: Settlement notification
B-->>M: Webhook payin_intent (COMPLETED)
Status Lifecycle
Payouts (CO_BREB, CO_ACH)
CO_BREB, CO_ACH)PENDING: Accepted, not yet dispatched.PROCESSING: Sent to the rail.COMPLETED: Funds confirmed. Terminal.FAILED: Terminal failure. The resource may include anerrorblock. When present, branch onerror.code. Each code has oneerror.message. The message can be reworded later; do not parse it. An unrecognized code should be treated likepayout_failed.
A limit rejected when you create the payout comes back as an HTTP error (payout_amount_above_maximum, payout_amount_below_minimum, or payout_limits_exceeded) and no payout is created. A limit rejected after the payout was accepted is a FAILED payout with amount_above_maximum, amount_below_minimum, or monthly_amount_limit_exceeded.
The same error block may be present on Get Payout, List Payouts, and the payout webhook. That field is one of the error classes on the payout. Branch on error.code. The table is the client action for each code, with the current error.message so you can match it. Do not parse the message.
| Code | Message | What to do |
|---|---|---|
destination_account_invalid | The destination account number is invalid. Verify it and retry. | Correct the account number and retry. |
destination_account_blocked | The destination account is inactive or blocked. Verify the account status with the beneficiary and retry. | Check the account status with the beneficiary and retry. |
destination_account_credit_limit_reached | The destination account has reached its credit limit. Use another account or contact the beneficiary. | Use another account, or contact the beneficiary. |
destination_account_closed | The destination account is closed or cancelled. Use another account. | Use another account. |
destination_account_not_open | The destination account is not open. Verify the account number and retry. | Correct the account number and retry. |
destination_account_unavailable | The destination account is unavailable. Please verify the account details and try again. | Correct the account details and retry. |
amount_above_maximum | The amount exceeds the maximum allowed. Lower it and retry. | Lower the amount and retry. |
amount_below_minimum | The amount is below the minimum allowed. Raise it and retry. | Raise the amount and retry. |
monthly_amount_limit_exceeded | The monthly deposit limit has been exceeded. Retry with a lower amount or wait until next month. | Retry with a lower amount, or wait until next month. |
beneficiary_phone_not_found | No beneficiary was found for that phone number. Verify it and retry. | Correct the phone number and retry. |
beneficiary_details_mismatch | The beneficiary details do not match. Verify the document type, document number, account type, and account number, then retry. | Correct the beneficiary details and retry. |
beneficiary_identification_mismatch | The identification does not match the destination account. Verify it and retry. | Correct the identification and retry. |
invalid_account_type | The account type is not valid for the destination bank. Correct it and retry. | Correct the account type and retry. |
invalid_transaction_type | The transaction type is invalid. Correct it and retry. | Correct the transaction type and retry. |
invalid_request | The request could not be processed. Please verify the payout details and try again. | Correct the payout details and retry. |
duplicate_transaction | This payout may be a duplicate. Confirm the previous attempt, then retry in a few minutes with a new reference. | Confirm the previous attempt, then retry later with a new reference. |
transaction_declined | The recipient's bank returned the payout. Retry, and contact that bank if it fails again. | Retry. If it fails again, contact the recipient's bank. |
security_rejection | The payout was cancelled for security reasons. Retry it. | Retry. |
technical_error_retry | A technical error occurred. Please retry the payout in a few minutes. | Retry in a few minutes. |
transaction_timed_out | The receiving bank timed out. Please retry the payout. | Retry. |
service_temporarily_unavailable | The service is temporarily unavailable. Please retry the payout in a few minutes. | Retry in a few minutes. |
insufficient_funds | The payout could not be completed due to insufficient funds. | Add funds and retry. |
security_review_required | The transaction requires additional security review. | Contact Bitso support. |
service_unavailable | The payout could not be completed. Please contact support. | Contact Bitso support and include this code. |
fund_reservation_failed | The payout could not be completed. Please contact support. | Contact Bitso support and include this code. |
request_not_accepted | The payout request was not accepted. Please contact support. | Contact Bitso support. |
payout_rejected | The payout was rejected by the provider. | Contact Bitso support. |
payout_failed | The payout has failed. | Contact Bitso support. |
service_unavailable and fund_reservation_failed share one message. The code is what distinguishes them.
Pay-in Intents (CO_PSE)
CO_PSE)PENDING: Intent created;payment_linkis usable.COMPLETED: Deposit created (payin_idset). Terminal.FAILED/EXPIRED: Terminal; no deposit is created on expiry.
Pay-in Keys (CO_BREB)
CO_BREB)ACTIVE: Registered and can receive funds. Returned on successful create.INACTIVE: Cannot receive funds.
See Pay-in Keys.
Best Practices
- Idempotency: For Bre-B payouts, the
Idempotency-Keyheader is not supported — reuse the samepreview_payout_idfrom Preview Payout; that id is the idempotency key for Create Payout. - Reconciliation: Store
external_reffor your own order id; it is searchable on list endpoints for payouts and pay-in intents. - Webhooks first: Treat webhooks as the source of truth for terminal states; use GET polling as fallback.
- Validation: Always verify webhook signatures before updating orders. See Webhook Signatures.
- Testing: Exercise the full status lifecycle in stage before going live. For PSE in stage, set
co_pse.bank_codeto1022; in production, pick a code from https://api.bitso.com/v3/banks/co.
Updated 14 days ago
