Mono Colombia

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_pse enabled for your program. See Methods: program configuration.
  • Credentials with the payins scope, which covers both intents and the PSE bank catalog. See Authentication.
  • A template that offers co_pse in available_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

  1. List the PSE banks. GET /v1/banks?kind=pse returns 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).

  2. 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.

  3. 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 201 with the intent in created and payment_tokens set to null. The payin already exists: payin_created arrives right away, as a payment attempt, not money received. Send amounts in whole pesos, because PSE settles whole pesos only. With a templateless intent, replace template_id with subaccount_id.

  4. Wait for payin_intent_ready_to_pay and redirect. The webhook delivers payment_tokens.pse_url, the payer's bank page. Send the payer there: they have 20 minutes. If you poll GET /v1/core/payins/intents/{id} instead, read pse_url as soon as it is set. By then the intent may already show awaiting_payment, because the payer is about to leave for their bank.

  5. The payer approves at their bank. They sign in and approve the payment, or decline it. Meanwhile, payin_awaiting_payment brings the payment's transaction_data.cus: the reference on the payer's bank receipt. Store it for support.

  6. Handle the return. The bank sends the payer back to your redirect_url, with payin_id and intent_id added 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 still awaiting_payment, show a pending screen and update it when the webhook arrives.

  7. Handle the endings.

    • Paid: payin_approved_in_provider and payin_intent_successful. Confirm the order. The subaccount is credited according to your trigger: right away with on_approval, on the ACH cycle with on_funds_delivery.
    • Not paid: payin_failed and payin_intent_failed. Both carry the same state_reason, such as payer_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 its external_id, so the new intent can reuse your order ID.
    • Link never generated: payin_failed and payin_intent_failed arrive before any redirect, with risk_control, provider_unavailable, internal_error, or unknown. Show an error, and let the payer try again with a new intent.
  8. Reconcile without storing Mono IDs. GET /v1/core/payins/intents/find_successful_by?external_id=order-1042 returns 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:

  1. Create a template that offers co_pse. Optionally, add a suggested payer (method_params.co_pse.payer) to pre-fill the form.
  2. Share its checkout_url. The payer picks PSE and their bank, fills in their details, and goes to their bank to approve.
  3. 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.
  4. 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.

On this page