Webhook Notifications
Use webhooks to get automatic, real-time updates on Colombia Payments activity. Register a callback URL with Bitso; when a resource status changes, Bitso sends an HTTP POST to your endpoint.
Important Guidelines
- We don't guarantee the order of event delivery. Your application should process events that may arrive out of sequence.
- The same event might be delivered multiple times. Your application must be idempotent.
- Your endpoint must respond in under five seconds to avoid a timeout. Store the notification and process it asynchronously.
- For transport, signing, and subscription details, see Webhooks.
Colombia Event Types
Subscribe to these event types for Colombian payment resources:
| Event type | Resource | When it fires |
|---|---|---|
payout | Payout (CO_BREB or CO_ACH) | Any status change (PENDING → PROCESSING → COMPLETED / FAILED). |
payin | Pay-in (CO_BREB, CO_PSE, or compensatory CO_ACH) | A deposit lands on a Bre-B key, a PSE intent settles into a pay-in, or a failed payout is returned as a compensatory deposit. |
payin_intent | Pay-in intent (CO_PSE) | Any status change (PENDING → COMPLETED / FAILED / EXPIRED). |
Branch on payload.type for rail-specific logic. A payin with type: CO_ACH is the refund of a CO_ACH payout. See Get payin for the fields that link it to that payout. Inbound COP is CO_BREB or CO_PSE.
Pay-in keys do not emit webhooks — creation returns ACTIVE synchronously. The payin webhook for funds received on a key carries sender identity and, when present, payload.co_breb.beneficiary_key (id is always set). Match beneficiary_key.id to the pay-in key. When that object is omitted, treat the pay-in as a first-party deposit recorded before this snapshot existed.
How to Register Your Webhook
Subscribe with Create webhook subscriptions. For Colombia payments, pass payout, payin, and payin_intent in events (any subset). See Managing Subscriptions. Fetch signing keys from List webhook public keys.
Webhook Payload Structure
Every webhook body has this envelope:
{
"event": "payout|payin|payin_intent",
"payload": { }
}payload is byte-identical to the response of the matching GET for the same id:
event | GET endpoint |
|---|---|
payout | GET /payments/v1/payouts/{id} |
payin | GET /payments/v1/payins/{id} |
payin_intent | GET /payments/v1/payin_intents/{id} |
Reuse the same parsing code for webhooks and GET responses. Nested rail objects use lowercase keys (co_breb, co_pse, co_ach) that match type.
Payload Examples
Examples below wrap the GET response shapes from the API contract.
{
"event": "payout",
"payload": {
"id": "89325a16-669e-4118-a8f0-f4236db92503",
"status": "PROCESSING",
"amount": "50000",
"asset": "COP",
"type": "CO_BREB",
"co_breb": {
"key_type": "PHONE",
"key_value": "3120000000",
"beneficiary": {
"document_type": "CC",
"document_id": "12345678",
"display_name": "JAN*** SMI***",
"bank_name": "Bancolombia"
}
},
"external_ref": "client-op-def456",
"created_at": "2026-02-26T14:00:00Z"
}
}A FAILED payout may carry the same error object as Get Payout. When present, branch on error.code. That field is one of the error classes on the payout. The table is the client action for each code, with the current error.message so you can match it. The message can be reworded later; do not parse it. An unrecognized code should be treated like payout_failed.
| 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. The same table is in Payment Flow.
{
"event": "payin",
"payload": {
"id": "550238a2_2eed_440e_a6d0_a227a0520f00",
"asset": "COP",
"amount": "500000.00",
"type": "CO_BREB",
"status": "COMPLETED",
"co_breb": {
"sender_name": "Jane Smith",
"sender_document_type": "CC",
"sender_document_id": "123456789",
"beneficiary_key": {
"id": "f9a23c11-44b2-4e7a-bc13-2d5f6a7b8c9d",
"key": "@KEYALIAS",
"external_ref": "client-key-abc123"
}
},
"created_at": "2026-02-26T14:00:00Z",
"updated_at": "2026-02-26T14:05:00Z"
}
}When payload.co_breb.beneficiary_key is present, id is 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.
{
"event": "payin",
"payload": {
"id": "fd857aec_fc24_4d93_907c_c0e2edeb7495",
"status": "COMPLETED",
"amount": "50000.00",
"asset": "COP",
"type": "CO_ACH",
"trace_id": "5b04eff7-91e0-43ff-843e-773646646415",
"refund": {
"payout_id": "9ff0516e_3e14_4ce5_a847_843c333021a4",
"reason": "DESTINATION_ACCOUNT_UNAVAILABLE",
"payout_external_ref": "clien1t-op-abc123"
},
"created_at": "2026-08-04T20:10:24.637Z",
"updated_at": "2026-08-04T20:10:24.654Z",
"co_ach": {}
}
}When payload.refund is present, the pay-in is a compensatory deposit for a failed payout. A CO_ACH pay-in is that case for a refunded Colombian ACH payout. See Get payin to reconcile it with the original payout.
{
"event": "payin_intent",
"payload": {
"id": "be6185ad-afdf-d682-2ca7-d447250f5e96",
"status": "PENDING",
"asset": "COP",
"amount": "500000.00",
"type": "CO_PSE",
"co_pse": {
"bank_code": "007",
"success_redirect_url": "https://example.com/success",
"error_redirect_url": "https://example.com/failure",
"payment_link": "https://api.bitso.com/payments/pse/start?token=abc123"
},
"external_ref": "client-payin-abc123",
"expires_at": "2026-02-26T23:59:59Z",
"created_at": "2026-02-26T14:00:00Z"
}
}When a PSE intent reaches COMPLETED, the same GET/webhook shape includes payin_id pointing at the settled pay-in. Fulfil on the webhook (or a follow-up GET), not on the browser redirect alone.
Updated 14 days ago
