# Payment state machines: the states you will forget

URL: https://www.paydude.io/resources/payment-state-machines
Type: Guide
Published: 2026-09-28 · Updated: 2026-09-28
Summary: A reference to every payment intent, charge, refund, subscription, payout and dispute status, the transitions that surprise people, and how to model them.

## Summary

A payment integration tracks six state machines, not one: the payment intent, the charge, the refund, the subscription, the payout and the dispute. The states people forget are the ones between request and result — `requires_action`, `processing`, `requires_capture`, `past_due` — and the ones that look final but are not. A payout reported as `paid` can later change to `failed`, a declined payment intent returns to `requires_payment_method`, and a refund can fail, so none of those can be treated as the end of the story. Store the processor's status verbatim alongside your own order state, and derive one from the other with an explicit transition table rather than scattered if statements.

The first version of every payments integration has two states: paid and not paid. It works until the first bank payment sits in `processing` overnight, the first 3D Secure challenge leaves a payment in `requires_action`, or the first payout marked `paid` bounces three days later.

This is a reference to every status the Paydude API can report, taken from its specification, with the transitions we observed in the sandbox on 28 September 2026 marked as such.

## Payment intent

The payment intent is the object your code creates and the one your order should follow.

**Payment intent statuses**

| Status | Means | Leaves by |
| --- | --- | --- |
| `requires_payment_method` | Created, or a previous attempt was declined | Confirming with a payment method |
| `requires_confirmation` | Has a payment method, not yet confirmed | Confirm |
| `requires_action` | The customer must do something, such as a 3D Secure challenge | The customer completing it |
| `processing` | Submitted; the outcome is not known yet | An event, possibly the next day |
| `requires_capture` | Authorised, waiting for you to capture | Capture, or automatic cancellation after 7 days |
| `succeeded` | Money collected | — (terminal) |
| `canceled` | Abandoned; no further charges | — (terminal) |

Paths observed in the sandbox:

- **Card, approved:** `requires_payment_method` → `succeeded` on confirm, with `charge.succeeded` and `payment_intent.succeeded` events.
- **Card, declined:** `requires_payment_method` → confirm → `requires_payment_method` again, with the reason in `last_payment_error`. The status does not change; only the error does.
- **ACH debit:** `requires_payment_method` → `processing` → `succeeded` about fourteen and a half hours later, delivered as an event.

> **Authorised is not captured:** With manual capture, a payment intent waits in `requires_capture`, and the API cancels any intent still uncaptured exactly seven days after creation. If you authorise at checkout and capture at shipping, a shipment that slips past a week loses the authorisation.

## Charge

A charge is one attempt at moving money. A payment intent can own several — every declined attempt leaves a `failed` charge behind, and a retry adds a new one to the same intent's `charges` list.

**Charge statuses**

| Status | Means |
| --- | --- |
| `pending` | Submitted and not yet settled — seen on ACH, with a `charge.pending` event |
| `succeeded` | The attempt worked |
| `failed` | The attempt was declined; see `failure_code` |

Track the payment intent, not the charges. The charges are the history; the intent is the answer.

## Refund

**Refund statuses**

| Status | Means |
| --- | --- |
| `pending` | Requested, not yet complete |
| `requires_action` | Something is needed before it can proceed |
| `succeeded` | The money is on its way back |
| `failed` | The refund did not go through — the reference lists reasons such as `expired_or_canceled_card` |
| `canceled` | Withdrawn before completion |

The one to handle is `failed`. A customer whose card was closed after they paid cannot be refunded to that card, and an integration that marks the order refunded when it *requests* the refund will tell them the money is coming when it is not. Mark it refunded on success, and route failures to a person who can pay the customer another way.

## Subscription

**Subscription statuses**

| Status | Means | Leaves by |
| --- | --- | --- |
| `pending` | Created, waiting for the first payment at `billing_cycle_anchor` | The first payment |
| `requires_confirmation` | Needs confirming before it starts — used, for example, when a surcharge applies | `POST …/confirm` |
| `active` | The most recent payment succeeded | A failed payment, pause or cancellation |
| `past_due` | The latest payment failed or was not attempted; `next_payment_at` becomes null | **Only** your code updating the card and calling `…/retry` |
| `paused` | No payments until resumed | `…/resume`, or a `resume_at` date |
| `canceled` | No further payments | — (terminal) |

`past_due` is the one that costs money. It is not retried automatically: the subscription sits there until you act, and your app keeps granting access unless you decide otherwise. [Handling failed payments in code](https://www.paydude.io/resources/handling-failed-payments-in-code) covers the dunning loop it needs. And do not call `/confirm` on a `pending` subscription — pending is the healthy state, and confirm only applies to `requires_confirmation`.

## Payout

**Payout statuses**

| Status | Means |
| --- | --- |
| `pending` | Created, not yet sent to the bank |
| `in_transit` | Submitted to the bank |
| `paid` | Reported as delivered — **but see below** |
| `failed` | Rejected by the bank; see `failure_code`, such as `account_closed` or `invalid_account_number` |
| `canceled` | Withdrawn |

> **Paid is not final:** The API's own description of the field: "Some failed payouts may initially show as `paid` but then change to `failed`." A bank can return a transfer days after accepting it. Keep listening for `payout.failed` on payouts you have already booked as paid, and do not treat `paid` as proof the money arrived.

Two more things the sandbox showed. A payout can carry a **negative** amount — a debit from the bank rather than a deposit. And one `payout.created` event arrived already reporting `status: "paid"`, because the payout was created and paid in the same instant; the event name tells you what happened first, not the current state. [Reconciling payouts with your database](https://www.paydude.io/resources/reconciling-payouts-with-your-database) goes through a payout line by line.

## Dispute

Disputes have the richest state machine, because an inquiry can precede a formal chargeback.

**Dispute statuses**

| Status | Stage | Your move |
| --- | --- | --- |
| `warning_needs_response` | Inquiry | Respond before it escalates |
| `warning_under_review` | Inquiry | Wait |
| `warning_closed` | Inquiry | Closed without becoming a chargeback |
| `needs_response` | Chargeback | Submit evidence before `closing_at` |
| `under_review` | Chargeback | Wait |
| `won` | Resolved | Funds returned |
| `lost` | Resolved | Funds kept by the cardholder |
| `closed` | Resolved | Closed |

Evidence can only be submitted before `closing_at`; after it, the dispute closes with no response, which usually means losing it. That date is the one to alert on. The sandbox's testing guide shows inquiries moving from `warning_needs_response` to `warning_under_review`, and an inquiry that escalates continuing on to `needs_response` — so one dispute can pass through both halves of the table. [What is a chargeback](https://www.paydude.io/resources/what-is-a-chargeback) covers the process from the cardholder's side.

## Modelling it in your own database

Two columns do most of the work. Keep the processor's status exactly as the API reports it, and keep your own business state — what the order means to *you* — separately, derived from it by an explicit table.

```javascript
// Processor status -> what the order means to us. Every status the API can
// report appears here, so a new one fails loudly instead of falling through.
const ORDER_STATE = {
  requires_payment_method: "awaiting_payment",   // new, or declined — check last_payment_error
  requires_confirmation:   "awaiting_payment",
  requires_action:         "awaiting_customer",  // 3D Secure: prompt, do not time out silently
  processing:              "pending",            // ACH: do not fulfil yet
  requires_capture:        "authorised",         // capture within 7 days
  succeeded:               "paid",
  canceled:                "canceled",
};

const TERMINAL = new Set(["paid", "canceled"]);

function nextOrderState(order, intent) {
  const next = ORDER_STATE[intent.status];
  if (next === undefined) throw new Error(`Unmapped intent status: ${intent.status}`);
  if (TERMINAL.has(order.state)) return order.state;   // never reopen a closed order
  return next;
}
```

Throwing on an unmapped status is deliberate. The day the API adds a status, you want an alert, not an order that silently falls into the wrong branch.

- **Store the processor's status verbatim**, as `processor_status`, next to your own `state`. When they disagree, you can see why.
- **Keep a history table** of every transition with its timestamp and the event that caused it. Disputes and support tickets arrive weeks later, and "what did we know when" is the question they ask.
- **Terminal for the order is not terminal for the money.** An order can be `paid` and later partially refunded, disputed, or included in a payout that fails. Model those as separate records against the order, not as more order states.

## The states you will forget, in one list

**Checklist of easily missed states**

| Object | State | Why it gets missed |
| --- | --- | --- |
| Payment intent | `requires_action` | Never happens in a test without 3D Secure; happens in production |
| Payment intent | `processing` | Cards decide instantly, so it looks like an ACH-only edge case |
| Payment intent | `requires_capture` | Expires after 7 days without warning |
| Payment intent | `requires_payment_method` after confirm | Looks like "new" but means "declined" |
| Subscription | `past_due` | Sits indefinitely with no automatic retry |
| Refund | `failed` | Refunds feel like they cannot fail |
| Payout | `paid` → `failed` | `paid` looks final and is not |
| Dispute | `warning_needs_response` | An inquiry, easy to ignore until it becomes a chargeback |

**Build against the sandbox first** An open sandbox, SDKs and an MCP server, on every tier. — [View pricing](https://www.paydude.io/pricing)

## Frequently asked questions

### What are the payment intent statuses?

requires_payment_method, requires_confirmation, requires_action, processing, requires_capture, succeeded and canceled. Only succeeded and canceled are terminal. A declined payment returns to requires_payment_method with the reason in last_payment_error.

### Can a paid payout fail?

Yes. The API documents that some failed payouts initially show as paid and later change to failed, because a bank can return a transfer after accepting it. Keep handling payout.failed events for payouts you have already recorded as paid.

### What does past_due mean on a subscription?

The latest payment failed or was not attempted. The subscription is not retried automatically — it stays past_due until your code updates the payment method and calls the retry endpoint, or cancels it.

### How long can a payment stay authorised before capture?

Seven days. A payment intent left in requires_capture is canceled automatically exactly seven days after it was created.

### Can a refund fail?

Yes. A refund has its own status, and failed is one of them — for example when the original card has since been closed. Mark an order refunded when the refund succeeds, not when you request it.

### How should I store payment status in my database?

Keep the processor's status exactly as reported alongside your own order state, derive the order state through an explicit mapping that covers every status, and record each transition in a history table with the event that caused it.
