All Docs
FeaturesCalmony PayUpdated March 15, 2026

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

StatusMeaning
requires_payment_methodPayment intent created, awaiting confirmation.
processingConfirmation submitted, awaiting gateway response.
succeededPayment was authorised and captured successfully.
canceledThe payment intent was deliberately canceled — either by your application or by the cardholder abandoning the flow.
failedThe 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}/confirm sets status: "failed".
  • A deliberate cancellation via /v1/payment_intents/{id}/cancel sets status: "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:

  1. Notify the customer that their payment was declined.
  2. Create a new Payment Intent or prompt the customer to update their payment method — a failed payment intent cannot be retried directly.
  3. Do not treat a failed intent the same as a canceled one 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
}