Collect with PSE
Charge a customer from their Colombian bank account through PSE: pick the bank, redirect, and confirm.
This flow collects a payment from the customer's own bank account. The customer picks their bank in your checkout, approves the payment on the bank's website, and comes back to your site. Each payment is one intent, so you always know which order was paid. It is the classic Colombian online checkout, run on PSE, with the money credited to a Core subaccount.
Use it for e-commerce checkouts, bill payments, or any payment where the customer expects to pay from their savings or checking account.
Mono's hosted checkout can run most of this flow for you: share the template's checkout_url,
and Mono asks for the bank and the payer's details. See Hosted checkout
instead.
Before you start
You will need:
- A Mono account with Core and
co_pseenabled for your program. See Methods: program configuration. - Credentials with the
payinsscope, which covers both intents and the PSE bank catalog. See Authentication. - A template that offers
co_pseinavailable_methods, or the decision to create templateless intents directly against a subaccount. - A webhook endpoint subscribed to the payin events.
- A return page on your site, where the payer lands after their bank (
payin_data.redirect_url).
Sequence overview
Steps
-
List the PSE banks.
GET /v1/banks?kind=psereturns every bank the payer can choose, with the code that identifies it:{ "kind": "pse", "banks": [{ "name": "BANCO DAVIVIENDA", "code": "1051" }] }Show the names in your checkout and keep the codes. A code outside this list is refused when you create the intent (
payin_unsupported_bank). -
Collect the payer's details. Ask for the bank, the person type, the document type and number, names, surnames, email, phone number, and address. Capture the IP address of the payer's device on your server, as an IPv4 address. PSE: what you collect from the payer describes every field.
-
Create one intent per payment. Creation is idempotent on
external_id. Use your order ID, so a double-click cannot create two intents: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" } }Mono answers
201with the intent increatedandpayment_tokensset tonull. The payin already exists:payin_createdarrives right away, as a payment attempt, not money received. Send amounts in whole pesos, because PSE settles whole pesos only. With a templateless intent, replacetemplate_idwithsubaccount_id. -
Wait for
payin_intent_ready_to_payand redirect. The webhook deliverspayment_tokens.pse_url, the payer's bank page. Send the payer there: they have 20 minutes. If you pollGET /v1/core/payins/intents/{id}instead, readpse_urlas soon as it is set. By then the intent may already showawaiting_payment, because the payer is about to leave for their bank. -
The payer approves at their bank. They sign in and approve the payment, or decline it. Meanwhile,
payin_awaiting_paymentbrings the payment'stransaction_data.cus: the reference on the payer's bank receipt. Store it for support. -
Handle the return. The bank sends the payer back to your
redirect_url, withpayin_idandintent_idadded to the query. Use them to find the order, but read the outcome from the API or the webhooks, never from the URL. The payer can land before the bank's answer reaches Mono. If the payin is stillawaiting_payment, show a pending screen and update it when the webhook arrives. -
Handle the endings.
- Paid:
payin_approved_in_providerandpayin_intent_successful. Confirm the order. The subaccount is credited according to your trigger: right away withon_approval, on the ACH cycle withon_funds_delivery. - Not paid:
payin_failedandpayin_intent_failed. Both carry the samestate_reason, such aspayer_insufficient_funds, which tells the payer what to change. See Reported by PSE. PSE allows one attempt per intent, so offer a new one. A failed intent frees itsexternal_id, so the new intent can reuse your order ID. - Link never generated:
payin_failedandpayin_intent_failedarrive before any redirect, withrisk_control,provider_unavailable,internal_error, orunknown. Show an error, and let the payer try again with a new intent.
- Paid:
-
Reconcile without storing Mono IDs.
GET /v1/core/payins/intents/find_successful_by?external_id=order-1042returns the successful intent for your order, or nothing. To trace a payment the payer reports, filter payins by its CUS:GET /v1/core/payins?method_data[co_pse][transaction][cus]=3155894.
Hosted checkout instead
With Mono's hosted checkout, the payer-facing steps happen on Mono's page:
- Create a template that offers
co_pse. Optionally, add a suggested payer (method_params.co_pse.payer) to pre-fill the form. - Share its
checkout_url. The payer picks PSE and their bank, fills in their details, and goes to their bank to approve. - Mono creates the intent, and the bank sends the payer back to your
payin_data.redirect_url. Without one, the payer lands on Mono's receipt page, which shows the bank, the CUS, and the reason when the payment failed. - You handle the endings and the reconciliation (steps 7 and 8) from the same webhooks.
On the hosted checkout, a payer who does not send the form within 20 minutes leaves the intent expired.
Next steps
- PSE: the method behind this flow, its limits, and its failure reasons.
- Intent errors: every reason a PSE intent can end with.
- Settlement: how the money reaches your bank account on the ACH cycle.