Payment and Order State Model
This page documents the lifecycle of payments and orders on the Aghanim platform: every status, every transition, and how payment status changes drive order status. Use it to understand which webhook fires when, when a player can retry, and how to avoid duplicate charges or premature entitlement revocation.
The webhook events and API status values described here are part of the public integration contract. The specific intermediate transitions between statuses described on this page are documented for technical clarity but are an implementation detail. They may evolve as the platform grows. Build your integration against the webhook events and API status values, not against a specific transition graph.
Overview
Aghanim separates two entities in the purchase lifecycle:
- Payment — a single attempt to charge the player through a payment provider. A single order may have multiple payment attempts (for example, after a card decline). Each attempt is a separate payment record.
- Order — the logical purchase record (a player buying an item, bundle, or subscription). The platform updates the order status in response to payment transitions.
An order terminates only when one of its payments succeeds, when a refund completes, or when a dispute is resolved against the player.
Before a new payment record is created, the platform locks the order by moving it from created (or reattempted) to captured. This lock prevents two concurrent payment attempts on the same order.
Payment lifecycle
Payment statuses
| Status | Terminal | Webhook event | Meaning |
|---|---|---|---|
created | No | payment.pending | Payment initiated. The player has started the transaction and Aghanim is waiting for the payment provider to confirm the outcome. payment.pending is emitted at this point. |
done | No | payment.succeeded | Payment captured successfully. May still move to dispute or refund_requested later. The payment.succeeded webhook is also emitted on entering done via dispute resolution or failed refund recovery — inspect the payload to distinguish. |
dispute | No | payment.dispute | A chargeback has been opened against a previously successful payment. Resolution moves it back to done (the merchant wins, chargeback reversed) or to chargeback (the player wins, chargeback accepted). |
refund_requested | No | — | A refund has been filed but not yet confirmed by the payment provider. No webhook is emitted on entering this status. Wait for payment.refunded (refund completes) or payment.succeeded (refund could not be processed and payment returns to done). |
refunded | Yes | payment.refunded | Refund confirmed by the payment provider, although funds may not have returned to the player's account yet. |
failed | Yes | payment.declined | Payment declined or errored during processing. |
expired | Yes | payment.expired | The payment provider reported the payment as expired because it wasn't completed in time. Aghanim moves a payment to expired only when the provider reports it (via webhook or status poll); otherwise the payment stays in created. |
voided | Yes | payment.voided | Payment was authorized but voided before capture, typically due to a post-authorization check (country mismatch, anti-fraud). |
rejected | Yes | payment.rejected | Payment rejected before submission, typically by the anti-fraud system. |
abandoned | Yes | payment.abandoned | The player explicitly stopped the payment process (closed the page, canceled, or hit back). Distinct from expired, which the payment provider reports when the payment is not completed in time. |
chargeback | Yes | payment.chargeback | The payment was canceled because the player won the chargeback dispute. The funds have been or will be returned to the player by their bank. |
Payment transitions
| From | To | Cause | Terminal |
|---|---|---|---|
created | done | Payment provider confirms successful capture | No |
created | failed | Payment provider declines the payment or returns an error | Yes |
created | rejected | Anti-fraud declines the payment | Yes |
created | expired | Payment provider reports the payment as expired (via webhook or status poll) | Yes |
created | voided | Post-authorization check fails (country mismatch, anti-fraud) | Yes |
created | abandoned | Player explicitly stopped the payment process | Yes |
done | dispute | Chargeback initiated or refund requested against a successful payment | No |
done | refund_requested | Refund filed but pending payment provider confirmation | No |
done | refunded | Payment provider confirms refund immediately | Yes |
refund_requested | refunded | Payment provider confirms the pending refund | Yes |
refund_requested | done | Refund could not be processed | No |
dispute | done | Chargeback reversed (merchant defended the dispute) | No |
dispute | chargeback | Chargeback accepted by the bank | Yes |
Payment state diagram
Order lifecycle
The order status values visible to your integration:
| Status | Terminal | Meaning |
|---|---|---|
created | No | Order created. No payment attempt started yet. |
captured | No | A payment attempt is in progress. The order is locked against parallel payment attempts. |
reattempted | No | The previous payment attempt failed in a non-terminal way. The order is open for a new payment attempt. |
paid | No | A payment on this order succeeded. The order proceeds to delivery. |
disputed | No | A payment on this order is under dispute. |
refund_requested | No | A refund has been filed. |
refunded | Yes | Refund completed. |
canceled | Yes | Reached via a resolved dispute. |
How payment status drives order status
Order status changes are triggered by payment transitions:
| Payment transition | Order status | Effect |
|---|---|---|
| (before payment record is created) | created or reattempted → captured | Order locked against concurrent payment attempts |
created → done | captured → paid | item.add fired |
created → failed | captured → reattempted | Player can retry |
created → rejected | captured → reattempted | Player can retry |
created → expired | captured → reattempted | Player can retry |
created → voided | captured → reattempted | Player can retry |
created → abandoned | captured → reattempted | Player can retry |
done → dispute | paid → disputed | Dispute workflow |
done → refund_requested | paid → refund_requested | Refund pending |
done / refund_requested → refunded | → refunded | Terminal; item.remove fired |
dispute → chargeback | disputed → canceled | Terminal; item.remove fired |
dispute → done | disputed → paid | Dispute resolved; entitlement remains |
Order state diagram
Webhooks emitted during the lifecycle
The transitions above drive the webhooks delivered to your integration:
payment.pending— emitted when a payment enterscreated(the player starts the transaction).payment.succeeded— fires on three distinct transitions: the normal success path (created → done), dispute resolved against the player (dispute → done), and a refund that could not be processed (refund_requested → done). Integrations that aggregate revenue from this event should inspect the payload to avoid double-counting recoveries.payment.chargeback— fires ondispute → chargebackwhen the player wins the chargeback dispute. The funds have been or will be returned to the player by their bank.payment.abandoned— fires when a payment entersabandoned(the player explicitly stopped the payment process). Distinct frompayment.expired, which the payment provider reports when the payment is not completed in time.- No webhook is emitted when a payment enters
refund_requested. Listen for the eventual resolution:payment.refundedif the refund completes, orpayment.succeededif it could not be processed. item.add— emitted only on the success path: when a payment transitionscreated → doneand the order moves topaid.item.remove— emitted on two terminal order outcomes: when an order reachesrefunded(coversrefund_requested → refunded) and whendisputed → canceled(chargeback accepted). Opening a dispute (paid → disputed) and filing a refund (paid → refund_requested) do not emititem.remove— only the terminal resolution does.- A payment that ends in
failed,rejected,expired, orvoideddoes not produceitem.add. The order returns toreattemptedand the player may retry.
For a player to complete a purchase, one of their payment attempts must reach done. All other terminal statuses leave the order open for another attempt. Once a payment is done, only → refunded or disputed → canceled revoke the entitlement via item.remove.
Practical guidance
Some payment methods — telco billing, certain wallets, and PayPal in specific flows — can keep a payment in created for minutes or longer before the payment provider confirms the outcome. To handle these flows correctly and avoid duplicate charges or premature entitlement revocation, your integration should:
- Treat
payment.pendingas the signal to show the player that the purchase is in progress and to block repeated purchase attempts on the same item in your client. - If
item.adddoes not arrive within a reasonable time (10–20 minutes for most methods), call Get Order with theorder_idreturned at checkout to inspect the current order status. - Release the block when one of the following happens:
item.addarrives — the payment succeeded.- A failure webhook arrives —
payment.declined,payment.expired,payment.voided,payment.rejected, orpayment.abandoned. The attempt is over and the order returns toreattempted, so the player can retry. - The order reaches a terminal status (
refundedorcanceled).
- Treat
item.removeas the only signal to revoke entitlement. Do not revoke onpaid → disputedorpaid → refund_requested— both can be reversed (a dispute may be won, a refund may be declined) and the player keeps the items. - All Aghanim webhook deliveries carry an
idempotency_key. Use it to deduplicate repeated deliveries on your side. See Idempotency for the full contract.
Need help?
Contact our integration team at [email protected]