Payment Components
Every payment in MoneyHash moves through three layers. A single field - payment_status - always tells you where the money is, regardless of which layer you're inspecting.
| Layer 1 - Intent | Layer 2 - Transaction | Layer 3 - Operation |
|---|---|---|
| The payment session. Holds context, orchestrates retries, maps to your order. | A single attempt inside the intent. Each retry or provider switch is a new transaction. | A financial action inside a transaction - authorize, capture, void, purchase, refund. |
How they relate
One intent can have many transactions (retries, fallbacks). One transaction can have many operations (authorize → capture → refund). An intent stays UNPROCESSED until a transaction succeeds or it is closed/expired - only then does it reach a terminal state and lock.
As long as an intent is UNPROCESSED, new transactions can be created. Once it reaches a terminal status (Processed, Closed, Expired), no further transactions are allowed.
Intent lifecycle statuses
| Status | When | Further transactions? |
|---|---|---|
UNPROCESSED | Initial. No successful transaction yet. | Yes |
PROCESSED | A transaction succeeded. | No - locked |
CLOSED | Manually terminated. Any pending attempt moves to failed. | No - locked |
EXPIRED | Time limit exceeded. Any pending attempt moves to failed. | No - locked |
Transaction lifecycle statuses
A transaction represents one attempt. Its status is available in two places, with different formats:
active_transaction.statuson a GET Intent - uppercased, e.g.SUCCESSFUL,FAILED,PENDING_AUTHENTICATIONdata.statuson a GET Transaction - prefixed with operation type, e.g.purchase.successful,purchase.failed,purchase.pending_authentication
The webhook event follows the same pattern: transaction.{operation}.{status} - for example transaction.purchase.pending_authentication or transaction.purchase.successful.
| Status | Description |
|---|---|
PENDING_AUTHENTICATION | Awaiting 3DS authentication with the cardholder's bank. Not a failure - do not act on this. |
SUCCESSFUL | Attempt completed successfully. |
FAILED | Attempt failed. Intent remains UNPROCESSED if not yet terminal - a retry may follow. |
REFUNDED | Captured funds have been returned. |
VOIDED | Authorization was cancelled before capture. |
active_transaction.status can show PENDING_AUTHENTICATION even after a payment resolves successfully (e.g. after 3DS completes). Never use active_transaction.status alone as your source of truth. Always rely on payment_status.status and operations[].latest_status.value for the definitive outcome.
Operation lifecycle statuses
Each operation progresses through the following statuses:
| Status | Description |
|---|---|
pending | Operation initiated, awaiting provider response. |
pending_authentication | Awaiting 3DS authentication. Applies to purchase and authorize operations. |
successful | Operation completed successfully. |
failed | Operation failed. |
A webhook fires on every status change, following the pattern transaction.{operation}.{status} - for example transaction.purchase.pending_authentication, transaction.purchase.successful, or transaction.refund.failed.
The full status progression for a 3DS purchase is: pending → pending_authentication → successful (or failed). The statuses[] array on each operation contains the complete timeline.
Payment status reference
payment_status is present on every webhook and GET response. It is the single monetary indicator that cuts across all layers - always telling you where the money is.
| Customer event | payment_status | Intent status |
|---|---|---|
| Payment created, no attempts | no_authorize_attempts | UNPROCESSED |
| Authorization in progress (incl. 3DS) | authorize_attempt_pending | UNPROCESSED |
| Attempt failed | authorize_attempt_failed | UNPROCESSED |
| Authorized, not yet captured | authorized | PROCESSED |
| Money captured | captured | PROCESSED |
| Authorization voided | voided | PROCESSED |
| Payment refunded | refunded | PROCESSED |
| Expired or manually closed | aborted | EXPIRED / CLOSED |
Balances field
The balances object inside payment_status tracks monetary amounts across operations. Use it to detect partial captures, partial refunds, and available amounts without manual reconciliation.
| Scenario | Key balances |
|---|---|
| Full capture | total_authorized: 100 , total_captured: 100 , available_to_capture: 0 |
| Partial capture | total_authorized: 100 , total_captured: 60 , available_to_capture: 40 |
| Full refund | total_captured: 100 , total_refunded: 100 , available_to_refund: 0 |
| Partial refund | total_captured: 100 , total_refunded: 30 , available_to_refund: 70 |
| Full void | total_authorized: 100 , total_voided: 100 , available_to_void: 0 |
When is a payment done?
A payment has reached its terminal (eventual) state when:
- Intent status is
PROCESSED,CLOSED, orEXPIRED - payment_status is
captured,voided,refunded, oraborted
Once terminal, no new transactions or operations can be created on the intent.
Always use payment_status.status combined with operations[].latest_status.value as your source of truth — not active_transaction.status, which can be misleading after 3DS flows. Both fields are present on every webhook and API response at every layer.
Step-by-step flow
Updated about 1 month ago