Order Lifecycle
Every payment order goes through a defined sequence of states. Understanding this lifecycle helps you build reliable integrations.
State machine
POST /payments
│
▼
┌───────┐
│pending│ ← waiting for customer payment
└───┬───┘
│ USDT Transfer detected on-chain
▼
┌─────────┐
│detected │ ← seen on chain, counting confirmations
└────┬────┘
│ MIN_CONFIRMATIONS reached (default: 19)
▼
┌──────────┬──────────┬──────────┐
│confirmed │underpaid │ overpaid │ ← webhook fired
└────┬─────┴────┬─────┴────┬─────┘
│ │ │
│ Webhook acknowledged (2xx)
▼ ▼ ▼
┌──────────────┐
│callback_sent │ (transient)
└──────┬───────┘
▼
┌──────────┐
│completed │ ← done
└──────────┘
(from pending, if expires_at is reached)
│
▼
┌─────────┐
│ expired │ ← order timed out
└─────────┘
(if webhook delivery exhausts all attempts)
confirmed/underpaid/overpaid → callback_failed
(if admin cancels a pending order)
pending → cancelledState transitions are strictly forward-only (Iron Rule 5). Every transition is appended to the order_events table, so current status is always a projection of an append-only event stream.
State descriptions
pending
The order has been created and a unique HD-derived payment address has been allocated from the pool. PayWarden is polling the blockchain for any incoming USDT transfer to this address.
The order expires ORDER_TTL seconds after creation (default: 1800 — 30 minutes) if no payment has been detected.
detected
A USDT Transfer matching the payment address has been seen on-chain. PayWarden now waits for MIN_CONFIRMATIONS blocks (default: 19).
At this point the payment is visible but not yet final — do not fulfill the order in this state.
confirmed
MIN_CONFIRMATIONS has been reached and the received amount equals the expected amount. PayWarden fires the payment.confirmed webhook. This is the state where you should fulfill the order.
underpaid
Confirmations reached, but amount_received is less than amount_expected. PayWarden fires the payment.underpaid webhook. Business logic for partial payments is up to your backend.
overpaid
Confirmations reached, but amount_received is greater than amount_expected. PayWarden fires the payment.overpaid webhook. The full received amount is swept.
callback_sent
Internal transient state used while the webhook is in flight / being recorded. Orders do not dwell here.
completed
Your webhook endpoint returned a 2xx response acknowledging a payment.confirmed / payment.underpaid / payment.overpaid event. This is the happy-path terminal state. After completed, fund sweeping is queued.
expired
The order's expires_at was reached before any payment was detected. The payment address is returned to the pool. Terminal.
If a customer pays an expired order's address afterwards, the transfer will be seen on-chain but the order status will not change — contact the customer to resolve manually.
callback_failed
Webhook delivery exhausted all retry attempts without a 2xx response. The payment was received on-chain — only the notification failed. Terminal from the order's point of view, but the admin dashboard exposes a Retry webhook action and sweep is auto-queued so funds are not blocked by a dead callback endpoint.
cancelled
An admin cancelled a still-pending order before any payment was detected. Terminal.
Event sourcing
PayWarden never mutates order status without also writing an event. Every state change is recorded as an immutable row in order_events:
order_events
────────────
id uuid
order_id uuid
event_type text -- order_created | payment_detected | payment_confirmed
-- | order_expired | order_cancelled
-- | webhook_sent | webhook_failed
-- | sweep_queued | sweep_completed | sweep_failed
-- | admin_webhook_retry | admin_sweep_retry
payload jsonb
created_at timestamptzReplaying the event stream reconstructs the full audit trail — when each transition happened, what triggered it, and what payload accompanied it.
Webhook events emitted
Only three webhook event names are sent to your callback URL:
| Event | Fired when |
|---|---|
payment.confirmed | Confirmations reached, amount matches |
payment.underpaid | Confirmations reached, amount < expected |
payment.overpaid | Confirmations reached, amount > expected |
Internal state changes like detection, expiry, and sweeping are recorded in order_events but are not sent as webhooks.
Confirmation count
TRON produces blocks roughly every 3 seconds. With the default MIN_CONFIRMATIONS=19:
- Detection latency: roughly
SCAN_INTERVAL(default3000ms) after the tx appears in a block - Confirmation latency: ~60 seconds after first detection
19 matches TRON's practical finality threshold. For very high-value transactions, consider raising MIN_CONFIRMATIONS.
Expiry and amount mismatch
| Scenario | Behavior |
|---|---|
| Payment arrives before expiry, exact amount | confirmed → webhook → completed |
| Payment arrives before expiry, amount < expected | underpaid → webhook → completed (or callback_failed if delivery fails) |
| Payment arrives before expiry, amount > expected | overpaid → webhook → completed, full received amount swept |
No payment before expires_at | expired, address released |
| Payment arrives after expiry | Seen on-chain but order stays expired, manual resolution needed |
| Webhook delivery exhausts retries | callback_failed, sweep auto-queued, admin retry available |