Transaction Lifecycle

Full state machine documentation for transaction statuses, webhook events, and recommended handling patterns.

Transaction Lifecycle

Every payment in UrbanPayX is represented by a Transaction. Understanding how transactions move through statuses is essential to building a reliable payment integration.


Transaction statuses

A transaction always has one of six statuses:

StatusTerminal?Meaning
pendingNoPayment link created. The user has not started checkout yet.
processingNoThe user is in the checkout flow — authenticating with their bank.
successYesPayment completed. Funds are being settled to the merchant account.
failedYesPayment failed — the bank rejected the transaction, or an error occurred.
expiredYesThe payment link expired before the user completed checkout.
canceledYesThe payment was canceled by the user or the system.

Terminal means the transaction will not change status again. Once a transaction reaches success, failed, expired, or canceled, it is final.


State transitions

Not every transition is possible. Here is the complete state machine:

                    +------------------------------------------+
                    |                                          |
                    v                                          |
              +----------+                                     |
              | PENDING  |---------------------+               |
              +----------+                     |               |
                    |                          |               |
                    | User starts checkout     | Time passes   | User/system cancels
                    v                          v               |
              +----------+              +----------+           |
              |PROCESSING|              | EXPIRED  | <---------+ (from PROCESSING too)
              +----------+              +----------+
                    |
          +---------+---------+
          |         |         |
          v         v         v
     +---------+ +------+ +----------+
     | SUCCESS | |FAILED| | CANCELED |
     +---------+ +------+ +----------+

Allowed transitions:

FromToTrigger
pendingprocessingUser begins bank authentication
pendingexpiredPayment link TTL elapsed
pendingcanceledUser or system cancels before checkout
pendingfailedUpstream error before checkout
processingsuccessBank confirms payment
processingfailedBank rejects payment
processingexpiredTimeout during bank authentication
processingcanceledUser cancels during checkout

Blocked transitions: Any terminal status to any other status. A success transaction stays success forever.


What triggers status changes

Transaction statuses are updated by two mechanisms:

1. Inbound provider webhooks. When the upstream payment provider notifies UrbanPayX of a status change, the transaction is updated automatically. This is the primary mechanism and requires no action from you.

2. Manual refresh. You can call the refresh endpoint to check the current status with the provider:

POST /api/v1/transactions/{transaction_id}/refresh-status
Authorization: Bearer YOUR_ACCESS_TOKEN

This is useful if you suspect a webhook was missed or want to verify the current state. It is rate-limited — you will get a 429 with a retry-after value if you call it too frequently.

Refresh restrictions: Only pending and processing transactions can be refreshed. Refreshing a terminal transaction returns 400.


How you will know: webhooks

When a transaction status changes, UrbanPayX sends a payment.status_changed webhook to your configured webhook_url. The payload looks like this:

{
  "id": "event-uuid",
  "type": "payment.status_changed",
  "version": "v1",
  "occurred_at": "2026-03-26T10:15:00Z",
  "client_id": "your-client-uuid",
  "data": {
    "transaction_id": "tx-uuid",
    "ref_id": "abc123def456",
    "transfer_id": "token-io-payment-id",
    "user_id": "user-uuid",
    "project_id": "project-uuid",
    "previous_status": "pending",
    "current_status": "processing",
    "source": "webhook",
    "token_status": "INITIATION_COMPLETED"
  }
}

Key fields in data:

FieldDescription
transaction_idYour UrbanPayX transaction ID
ref_idShort reference ID for the transaction
previous_statusStatus before this change
current_statusNew status after this change
sourceWhat triggered the change (webhook for automatic, client_manual_refresh for polling)
token_statusRaw status from the payment provider — useful for debugging

Webhook security: Every webhook includes an X-UPX-Signature header containing an HMAC-SHA256 signature and an X-UPX-Timestamp header. Verify the signature against the payload using your webhook secret to ensure the event came from UrbanPayX.


Recommended handling patterns

For each status

pending — The payment link has been created. Show the user a "Waiting for payment" state. If you are displaying the checkout URL in your UI, this is the starting point.

processing — The user is actively completing payment. Show a "Payment in progress" indicator. Do not let the user initiate a new payment for the same intent — they are already in the flow.

success — Payment is confirmed. Mark the order as paid, deliver the product/service, send a confirmation to the user. This is the happy path.

failed — The bank rejected the payment. Show an error to the user and offer to retry with a new transaction. Do not retry with the same transaction — create a new one.

expired — The payment link was not used in time. Inform the user and offer a new payment link if the intent is still valid. Create a new transaction.

canceled — The user or system canceled the payment. Handle similarly to expired — offer a new payment link if needed.

Idempotent transaction creation

Always include an X-Idempotency-Key header when creating transactions. This prevents duplicate payments if your request is retried (network timeout, user double-clicks, etc.).

The key should be unique per payment intent. If you retry the same key with the same payload, you will get the original transaction back. If you retry with a different payload, you will get a 409 Conflict.

# First request
X-Idempotency-Key: order-12345-attempt-1

# Safe retry (same payload) — returns the same transaction
X-Idempotency-Key: order-12345-attempt-1

# New payment intent — use a new key
X-Idempotency-Key: order-12345-attempt-2

Webhooks vs. polling

Use webhooks as your primary mechanism. They are real-time, reliable, and do not consume your rate limit.

Use polling as a fallback — for example, if you suspect a webhook was lost, or during initial testing when webhooks are not set up yet. Remember:

  • Only pending and processing transactions can be refreshed.
  • There is a cooldown between refresh calls per transaction.
  • The refresh endpoint returns the updated transaction object.

Handling the callback URL

After the user completes (or abandons) the checkout flow, they are redirected to your callback_url. This is a user-facing redirect, not a server-to-server call. Do not rely on the callback to determine payment success — the user might close their browser before the redirect completes.

Always use webhooks (or polling as fallback) to confirm the final transaction status server-side.


Provider status mapping

For debugging, here is how the payment provider internal statuses map to UrbanPayX statuses:

Provider statusUrbanPayX status
INITIATION_COMPLETED, SETTLEMENT_COMPLETED, SUCCESS, COMPLETEDsuccess
INITIATION_PENDING, PENDING, PROCESSING, SETTLEMENT_IN_PROGRESSprocessing
INITIATION_REJECTED, FAILED, REJECTEDfailed
INITIATION_EXPIRED, INITIATION_NO_FINAL_STATUS_AVAILABLE, EXPIREDexpired
INITIATION_DECLINED, CANCELED, CANCELLEDcanceled

The raw provider status is included as token_status in webhook payloads and refresh responses, which can be helpful when debugging edge cases.


Related guides