Payment Intent Statuses: Understanding `failed` vs `canceled`
Payment Intent Statuses: Understanding failed vs canceled
As of v1.0.95, Calmony Pay distinguishes between a gateway-declined payment and a user-canceled payment using two separate status values on the Payment Intent object. This aligns with the Stripe-compatible REST API contract.
Status Values
| Status | Meaning |
|---|---|
requires_payment_method | Payment intent created, awaiting confirmation. |
processing | Confirmation submitted, awaiting gateway response. |
succeeded | Payment was authorised and captured successfully. |
canceled | The payment intent was deliberately canceled — either by your application or by the cardholder abandoning the flow. |
failed | The payment was declined by the payment gateway (e.g. Cardstream declined the card, insufficient funds, fraud check failure). |
Why This Matters
Before v1.0.95, both a user-initiated cancellation and a Cardstream card decline would result in status: "canceled". This made it impossible to distinguish the two outcomes by inspecting the payment intent resource alone.
Now:
- A card decline or gateway failure during
/v1/payment_intents/{id}/confirmsetsstatus: "failed". - A deliberate cancellation via
/v1/payment_intents/{id}/cancelsetsstatus: "canceled".
Webhooks
The payment_intent.failed webhook event is emitted when a payment intent transitions to status: "failed". You should use the status field on the payment intent object as the canonical source of truth, with the webhook event as the real-time trigger.
{
"type": "payment_intent.failed",
"data": {
"object": {
"id": "pi_xxxxxxxxxxxx",
"status": "failed",
"last_payment_error": {
"code": "card_declined",
"message": "Your card was declined."
}
}
}
}
Handling Failed Payment Intents
When you receive a payment_intent.failed event or retrieve a payment intent with status: "failed", the recommended flow is:
- Notify the customer that their payment was declined.
- Create a new Payment Intent or prompt the customer to update their payment method — a failed payment intent cannot be retried directly.
- Do not treat a
failedintent the same as acanceledone in your business logic (e.g. do not record it as a voluntary churn event in your subscription system).
Subscription Billing
The subscription billing engine (inngest/functions/subscription-billing) correctly handles the failed status as of v1.0.95. Failed subscription renewal attempts are tracked separately from intentional subscription cancellations, enabling accurate retry logic and dunning workflows.
Migration Notes
If your integration was previously using status === "canceled" combined with webhook event type to infer a decline, you should update your logic:
Before v1.0.95:
// Unreliable — cancellations and declines were indistinguishable by status alone
if (paymentIntent.status === 'canceled') {
// Could be a decline OR a cancellation
}
After v1.0.95:
if (paymentIntent.status === 'failed') {
// Card was declined or gateway error — prompt for new payment method
}
if (paymentIntent.status === 'canceled') {
// Deliberately canceled — no retry needed
}