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.
| Code | Description | Cause | Suggested fix |
|---|---|---|---|
expired | Window closed | Nobody 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_failed | Key registration failed | Registering 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_failed | Payment token setup failed | The 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_failed | Payment failed | A 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. |
unknown | Unknown | The 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:
| Group | Codes |
|---|---|
| Amount limits | amount_below_minimum, amount_exceeds_maximum, amount_below_total_minimum, amount_exceeds_total_maximum, intent_amount_mismatch |
| Payer restrictions | payer_not_allowed |
| Bank and risk | bank_rejected, risk_control |
| Template conditions | template_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:
| Question | Event | Where the reason lives |
|---|---|---|
| Why did this attempt fail? | payin_rejected / payin_failed | The payin's state_reason — see Payin errors. |
| Why can this order never be paid? | payin_intent_failed / payin_intent_expired | The 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
- Payin errors — the reason behind every failed attempt.
- Template errors — why the template behind the intent stopped being payable.
- Collect with intents — the end-to-end flow.