Intents
A single expected payment, declared before the funds arrive.
An intent is one expected payment, declared before the money moves. Where a template says "I accept payments under these rules", an intent says "I am expecting this payment": one payer, one amount, one window of time, one payment token of its own. When the payment arrives, Mono links it back to the intent, so you know exactly which order, invoice, or checkout session was paid, before reading a single payin field.
Use intents when each payment must be pre-identified: e-commerce checkout, invoice payment, or any flow where "who is paying what" matters more than "money arrived".
With or without a template
An intent is created in one of two ways (template_id and subaccount_id are mutually exclusive):
- From a template: the intent inherits the template's rules (limits, expected payers,
payin_data) and can override some for itself. For Bre-B, the template must mint its keys per intent (method_params.co_breb.key_per: "intent"); a template with a shared key cannot link payments back to individual intents. For PSE, any template that offersco_psecan host intents, and every PSE payment goes through one. - Templateless: the intent is created directly against a subaccount and carries its own configuration. Mono provisions a dedicated single-use payment token for it.
Creation is idempotent on external_id: repeating the request returns the existing intent. When an intent expires, its external_id is freed for a new one, which is what makes the retry loop of a checkout safe.
PSE intents
A PSE intent works like any other, with three differences:
- It carries the payer upfront. Send
instrument(the payer's bank and details) andmethod_params.ip_address. The PSE page lists every field. - Its payin exists from the start. Mono creates the payin together with the intent, in
created, and announces it withpayin_created. - It allows one attempt. A PSE intent never re-arms: if the payment fails, the intent ends
failed. Create a new intent to let the payer try again.
When something PSE needs is missing or invalid, creation fails:
| Response | When |
|---|---|
422, with a path to the field | A field is missing or malformed: no instrument, no method_params, an IPv6 address, an invalid email or phone number. |
400 payin_instrument_required | instrument is null. |
400 payin_invalid_method_params | method_params holds values PSE cannot use. |
400 payin_unsupported_bank | bank_code is not in the PSE bank catalog. |
400 payin_expected_payers_not_supported | The intent names expected payers, which PSE cannot enforce. |
Amounts
Provide exactly one of merchant_amount or payer_amount; the other is derived. With the two methods available today (Bre-B and PSE, both in COP), both amounts are equal. The two fields exist so that cross-currency collection can land later without changing the contract. The amount is enforced: a Bre-B payment for a different amount is rejected (intent_amount_mismatch) and the intent re-arms for a retry. With PSE, the payment link is generated for the intent's amount, so the payer cannot pay a different one. Send PSE amounts in whole pesos, a multiple of 100 in the API's cents: the PSE network settles whole pesos only.
Lifecycle
created: accepted and persisted; the payment token is still being prepared, sopayment_tokensisnulland the payer cannot pay yet.ready_to_pay: the intent is payable:payment_tokenscarries the Bre-B key (and single-use QR, when requested) andexpires_atis set. For Bre-B the window is 30 minutes. For PSE,payment_tokens.pse_urlcarries the payment link, and the window is 20 minutes.awaiting_payment: a payment for the intent was authorized and is in flight. A PSE intent moves here right afterready_to_pay, once its payment link is generated. It does not expire in this state: it waits for PSE to report the outcome.successful: the payment was confirmed; the intent is spent, its tokens are torn down, and it can never be paid again.failed: nothing left to pay: the payment token never made it (key_registration_failed,payment_token_registration_failed) or a payment failed on a method that does not re-arm, such as PSE. In that case, the intent keeps the payin's reason, for examplepayer_insufficient_funds.expired: the window closed with nobody paying.state_reasonisexpired.
Every reason an intent can end on, and what to do about each, is in Intent errors.
Re-arming: when a payment attempt fails after authorization but the intent is still alive, the intent returns to ready_to_pay. The payer can retry against the same key while the window lasts. The failed attempt itself is reported by the payin's own payin_rejected or payin_failed webhook; the intent emits payin_intent_attempt_failed. PSE does not re-arm: one intent, one attempt.
Answering "was this paid?"
GET /v1/core/payins/intents/find_successful_by?external_id=... returns the single successful intent for a tenant-side identifier. At most one exists per client. It lets your reconciliation ask about your own order ID without storing Mono IDs.
Hosted checkout
url is the Mono-hosted page where the payer completes that specific intent. Share it with the payer when you would rather not build the payment screen yourself. With PSE, the checkout creates the intent before the payer fills in the form, so instrument stays null until they send it. If they do not send it within 20 minutes, the intent expires. See Hosted checkout or API-only.
Fees
An intent's calculated_fees arrives empty. The fees actually charged appear on the payin.
Data model
The full schema lives in the Intents API reference.
Next steps
- Payment tokens: per-intent keys, dynamic QR codes, and PSE payment links.
- Collect with intents: the end-to-end flow.
- Webhooks: the seven intent events.