Mono Colombia

PSE

Collect COP payments from Colombian bank accounts through PSE: the bank redirect, the payer data, and timing.

PSE (Pagos Seguros en Línea) is Colombia's standard system for paying online from a bank account, operated by ACH Colombia. The payer picks their bank, is sent to that bank's own website, signs in, and approves the payment there. co_pse is the payin method that collects over that rail: Mono generates the PSE payment link, sends the payer to their bank to approve it, and credits the subaccount once the bank confirms.

It fits the classic Colombian online checkout, where customers expect to pay from their savings or checking account. It settles exclusively in COP.

This method credits Core subaccounts. To collect PSE payments into a Banking account instead, use Banking collection links. See Payins vs other Mono products.

Before you start

You will need:

  • co_pse enabled in your program. Like every method, it is set up with Mono. See Program configuration.
  • A way to collect the payer's details: Mono's hosted checkout, or a form of your own.

How a PSE payin works

  1. The payer chooses PSE and their bank in your checkout. You create an intent with the bank, the payer's details, and the IP address of the payer's device.
  2. Mono creates the payin at the same time, in created. At this point it records a payment attempt, not money received.
  3. Mono generates the PSE payment link. The intent reports it in payment_tokens.pse_url: the page where the payer approves the payment at their bank. It arrives with payin_intent_ready_to_pay, the same event that delivers the Bre-B key. Whatever the method, the payment token is ready when the intent reaches ready_to_pay.
  4. You redirect the payer to that page. They sign in to their bank and approve or decline the payment. Mono never sees their banking credentials.
  5. The bank sends the payer back to your site, and the outcome reaches you through the payin webhooks.

This is the opposite of Bre-B. With Bre-B, the payer pays to a key and their identity arrives with the money. With PSE, you tell Mono who pays and from which bank before the payment starts.

What you collect from the payer

A PSE intent carries the payer in instrument, and the payer's IP address in method_params:

POST /v1/core/payins/intents
{
  "external_id": "order-1042",
  "template_id": "cpitpl_5tgliBmzjZ6mpQPRbQjfKj",
  "method": "co_pse",
  "merchant_amount": { "amount": 8990000, "currency": "COP" },
  "method_params": { "ip_address": "190.24.10.1" },
  "instrument": {
    "kind": "co_pse_bank",
    "data": { "bank_code": "1051" },
    "payer": {
      "kind": "co_pse_person",
      "data": {
        "document_type": "CC",
        "document_number": "1023711432",
        "names": "Lupita",
        "surnames": "Pérez Gómez",
        "email": "lupita.perez@example.com",
        "phone_number": "+573001234567",
        "person_type": "natural",
        "address": "Calle 100 # 45-23, Bogotá"
      }
    }
  },
  "payin_data": { "redirect_url": "https://shop.example.com/orders/1042/return" }
}
FieldWhat it is
instrument.data.bank_codeThe payer's bank, as its PSE code from the bank catalog (GET /v1/banks?kind=pse). A code outside the catalog is refused with payin_unsupported_bank.
instrument.payer.dataWho pays. All eight fields are required, because PSE generates the payment link with them. email must be a valid address, and phone_number a Colombian number with its +57 prefix.
person_typenatural for a person, legal for a company.
document_typeOne of CC, CE, TI, NUIP, TE, NIT, PASS, PEP, PPT, FDO, or RC. document_number takes 5 to 32 characters.
method_params.ip_addressThe IPv4 address of the payer's device, as your checkout sees it. The PSE network requires it.
payin_data.redirect_urlWhere the payer goes after their bank. Optional: without it, the payer lands on Mono's receipt page.

A missing or malformed field, such as no instrument or an IPv6 address, answers 422 with a path that points at the field. A null instrument answers 400 with payin_instrument_required, and method params PSE cannot use answer 400 with payin_invalid_method_params.

On the hosted checkout you send none of this: the payer picks their bank and fills in the same details on Mono's page. A template can suggest a payer in method_params.co_pse.payer to pre-fill that form, and the payer can change any field. See Templates.

The return to your site

When the payer leaves their bank, they come back to your redirect_url with two query parameters added: payin_id and intent_id. Use them to show the right order on screen.

Do not read the outcome from the URL. Anyone can edit a URL, which is why Mono does not put the payment state in it. Read the payin through the API, or wait for its webhooks.

What the payer pays to

PSE issues no key or QR: the payer pays on their bank's page. Each intent gets one payment token with a single field, pse_url. A template has no PSE token of its own. That is why a template that offers only PSE becomes ready as soon as it is created.

Limits and timing

  • One attempt per intent. A PSE intent does not re-arm after a failed payment. If the payment fails, the intent ends failed with the payin's reason, such as payer_insufficient_funds. To let the payer try again, create a new intent.
  • 20-minute window. When the payment link is generated, expires_at is set 20 minutes ahead: the time the payer has to approve at their bank. The intent does not expire at that time: it waits for PSE to report the outcome.
  • On the hosted checkout, the payer has those same 20 minutes to send the form. If they never do, the intent ends expired.
  • No method cap from Mono. Mono applies no per-transaction cap of its own to PSE, and your template's limits apply as usual. The payer's bank may still limit what their account can pay online.
  • No expected_payers. The payer's identity comes from your form, not from their bank, so Mono cannot enforce an allowlist on it. A PSE intent that names expected payers is refused with payin_expected_payers_not_supported.

Settlement: two separate movements

A PSE payin moves money twice, and each movement runs on its own clock. The first is the balance your user sees. The second is the real money reaching your bank account.

Crediting the subaccount

This is the virtual money: the credit on the Core ledger that shows up as balance in the subaccount. You decide when it happens, with subaccount_disbursement_trigger:

  • on_approval: the subaccount is credited as soon as the payer's bank approves the payment. The real money has not arrived yet.
  • on_funds_delivery: the subaccount is credited only once the real money reaches its destination account.

payin_subaccount_credited announces it. See Crediting the subaccount.

Delivering the real money

This is the real money moving between banks, and Mono runs it. With PSE, it moves on the ACH cycle: the interbank clearing that runs at set times on business days.

Depending on your program, the money lands directly in your account, or reaches your funding account in a settlement batch. Track it with funds_delivery_status on the payin, and with the payin_settlement_batch_* events when a batch delivers it. See Delivering to your funding account.

With on_approval, your user can spend the balance before the real money is yours. With the ACH cycle, that gap can last until the next business day. Choose on_funds_delivery if your product must not show balance until the money arrives.

Why a PSE payment fails

A PSE payin never ends rejected. That state is for payments Mono refuses as they arrive. A PSE payin does not arrive: Mono creates it with the intent, before the payer pays. When the payment does not go through, the payin ends failed, and the intent ends with the same reason.

PSE reports why in terms the payer can act on: try again, pay from another account, or talk to their bank. It has 16 reasons of its own, in three groups:

Where the payment stoppedstate_reason
The payer did not finishpayer_did_not_authorize, payer_did_not_complete, invalid_payer_data, payer_browser_unsupported
The payer's account cannot paypayer_insufficient_funds, payer_limit_exceeded, payer_pse_disabled, payer_account_restricted, payer_account_inactive, payer_account_not_found, payer_account_closed
The payer's bank or PSE did not approve itpayer_bank_declined, authorization_denied, payer_bank_unavailable, pse_timeout, pse_rejected

Other outcomes reuse the reasons every method shares, such as risk_control, provider_unavailable, or unknown. PSE does not report bank_rejected. Every code, with its suggested fix, is in Payin errors.

Next steps

On this page