Mono Colombia

Intent errors

Why an intent ends failed or expired, and which reasons it inherits from the payment that closed it.

An intent carries state_reason in its two unsuccessful states:

  • failed — there is nothing left to pay. Either the payment token never made it, or a payment closed the intent for good.
  • expired — the window closed with nobody paying.

You read the code in the payin_intent_failed and payin_intent_expired webhooks, and on the intent itself via get or list.

Example webhook payload

{
  "timestamp": "2026-02-11T14:22:07.913204Z",
  "event": {
    "type": "payin_intent_failed",
    "data": {
      "id": "cpii_034EASMIAFVq06j2NxhnIJ",
      "state": "failed",
      "state_reason": "payment_token_registration_failed",
      "external_id": "order-88213",
      "template_id": "cpit_034EASMHlq36oZISbc9W4b"
    }
  }
}

The intent's own reasons

What can happen to an intent before a payment exists, plus the catch-all for a payment that failed without an explanation worth handing the payer.

CodeDescriptionCauseSuggested fix
expiredWindow closedNobody paid before expires_at. For Bre-B the window is 30 minutes.Create a new intent — the previous one releases its external_id when it expires.
key_registration_failedKey registration failedRegistering the intent's own payment key with the network exhausted its retries.Create the intent again. If it repeats, contact support with the intent id.
payment_token_registration_failedPayment token setup failedThe key registered, but the payment token could not be assembled from it.Create the intent again. If it repeats, contact support with the intent id.
payment_failedPayment failedA payment closed the intent for a reason that says nothing to the payer — an infrastructure fault such as a timeout, a ledger failure or a provider outage.Read the payin's own state_reason for the detail; create a new intent to retry.
unknownUnknownThe payment that closed the intent failed without a known reason.Contact support with the intent id.

Reasons inherited from the payment

When a payment closes an intent for a reason the payer or the merchant can act on, the intent adopts the payin's reason under the same name instead of flattening it into payment_failed. Each of these means exactly what it means on the payin, so the table there is the reference:

GroupCodes
Amount limitsamount_below_minimum, amount_exceeds_maximum, amount_below_total_minimum, amount_exceeds_total_maximum, intent_amount_mismatch
Payer restrictionspayer_not_allowed
Bank and riskbank_rejected, risk_control
Template conditionstemplate_paid, template_expired, template_disabled, template_deleted, template_not_found, template_invalid_state

The split is deliberate: what the payer can act on (the amount, being the wrong payer) and what the merchant needs to tell apart (which way the collection stopped taking payments, or that the receiving account cannot take money) keep their name; the infrastructure faults do not, because they mean nothing to whoever is looking at a checkout.

Not reachable with Bre-B today

These inherited reasons only appear for payment methods that do not re-arm an intent after a failed payment. Bre-B — the only method available today — re-arms: a failed payment returns the intent to ready_to_pay so the payer can retry with the same key while the window lasts, and the intent is not closed at all. Until another method ships, an intent that ends failed will carry one of its own reasons. Handle the inherited ones defensively rather than expecting them.

A failed payment is not always a failed intent

Most payment failures do not end the intent. With a re-arming method the intent goes back to ready_to_pay and emits payin_intent_attempt_failed — the failed attempt itself is reported by the payin's own payin_rejected or payin_failed webhook, which carries the real reason.

So there are two different questions, and two different events:

QuestionEventWhere the reason lives
Why did this attempt fail?payin_rejected / payin_failedThe payin's state_reason — see Payin errors.
Why can this order never be paid?payin_intent_failed / payin_intent_expiredThe intent's state_reason — this page.

Handling them

app.post('/webhook', (req, res) => {
  const { event } = req.body;

  // The order can still be paid — the payer just has to retry
  if (event.type === 'payin_intent_attempt_failed') {
    // Keep the checkout open; the key is still valid
  }

  // The order is over
  if (event.type === 'payin_intent_failed' || event.type === 'payin_intent_expired') {
    const { external_id, state_reason } = event.data;

    switch (state_reason) {
      case 'expired':
        // Nobody paid in time — offer a new checkout
        break;
      case 'key_registration_failed':
      case 'payment_token_registration_failed':
        // The intent never became payable — create it again
        break;
      default:
      // Payment closed it: read the payin for the detail, then issue a new intent
    }
  }

  res.status(200).send();
});

Checking whether an order was paid

Do not infer payment from the absence of a failure. GET /v1/core/payins/intents/find_successful_by?external_id=... answers it directly: at most one successful intent exists per identifier.

Next steps

On this page