Full state machine documentation for transaction statuses, webhook events, and recommended handling patterns.
Transaction Lifecycle
Every payment in UrbanPay 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:
| Status | Terminal? | Meaning |
|---|---|---|
pending | No | Payment link created. The user has not started checkout yet. |
processing | No | The user is in the checkout flow — authenticating with their bank. |
success | Yes | Payment completed. Funds are being settled to the merchant account. |
failed | Yes | Payment failed — the bank rejected the transaction, or an error occurred. |
expired | Yes | The payment link expired before the user completed checkout. |
canceled | Yes | The 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:
| From | To | Trigger |
|---|---|---|
pending | processing | User begins bank authentication |
pending | expired | Payment link TTL elapsed |
pending | canceled | User or system cancels before checkout |
pending | failed | Upstream error before checkout |
processing | success | Bank confirms payment |
processing | failed | Bank rejects payment |
processing | expired | Timeout during bank authentication |
processing | canceled | User 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 bank webhooks. When the bank notifies us 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 bank:
POST /api/v1/transactions/{transaction_id}/refresh-status
Authorization: Bearer YOUR_ACCESS_TOKENThis 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
pendingandprocessingtransactions can be refreshed. Refreshing a terminal transaction returns400.
How you will know: webhooks
When a transaction status changes, we send 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": "payment-network-transfer-id",
"amount": "125.00",
"currency": "EUR",
"user_id": "user-uuid",
"business_customer_id": null,
"project_id": "project-uuid",
"contract_request_id": null,
"previous_payment_status": "pending",
"current_payment_status": "processing",
"reconciliation_state": "none",
"status_detail": "awaiting_bank_authorization",
"status_reason": null,
"actor_type": null,
"actor_account_id": null
}
}Key fields in data:
| Field | Description |
|---|---|
transaction_id | Your UrbanPay transaction ID, the stable key for the payment |
ref_id | Reference of the current payment session on the payment network; it changes when the payer restarts an expired session, so do not key on it |
amount, currency | The amount as a decimal string (for example "125.00") and its ISO 4217 currency. Check them against your own order before fulfilling on success |
previous_payment_status | Status before this change |
current_payment_status | New status after this change |
reconciliation_state | none normally; manual_review_required when the outcome could not be confirmed with the bank and UrbanPay operations are reviewing it. That change arrives with equal previous and current status: do not treat the payment as failed or paid until a later event says so |
status_detail | Fine-grained status in UrbanPay's own vocabulary (for example awaiting_bank_authorization, rejected_insufficient_funds); informational, branch on current_payment_status |
status_reason | Human-readable explanation when one is available, most useful on failed and expired |
actor_type | client_user / client_api when your team triggered it, urbanpayx_internal for UrbanPay staff, null when automatic |
actor_account_id | The account that triggered it; null for automatic and internal changes |
The payment network's raw status codes are never included in the client webhook; status_detail carries the fine-grained status in UrbanPay's own vocabulary, and GET /api/v1/transactions/{transaction_id}/status-events returns the full timeline of a transaction. The envelope occurred_at is the time the status actually changed, not the time the event was delivered, so order events by it rather than by arrival.
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 UrbanPay.
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-2Webhooks 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
pendingandprocessingtransactions 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.
What arrives on the callback URL
UrbanPay appends transaction_id to your callback URL, keeping any query string already on it:
https://your-app.com/payment/complete?order=42&transaction_id=8f14e45f-ceea-467a-9f9c-2ba1d9b8f73e
That is the key your landing page needs: read it, then call GET /api/v1/transactions/{transaction_id} to render the true state. If your URL already carries a transaction_id of your own, UrbanPay leaves it alone and appends nothing.
Other parameters may ride along, added by UrbanPay or by the payer's bank. A status among them is a hint and nothing more. It is written before settlement is confirmed, so it is frequently stale, and a query string can be forged by anyone able to edit a URL. Never show a "payment received" screen, and never release goods, on the strength of it.
Detail status vs payment status
Alongside the six coarse statuses above, UrbanPay exposes a finer-grained detail status for
diagnosing where in the bank flow a payment sits. It appears as provider_status on
POST /api/v1/transactions/{transaction_id}/refresh-status and on each entry of
GET /api/v1/transactions/{transaction_id}/status-events. It is deliberately not part of the
webhook payload.
These are UrbanPay's own values, not the upstream network's raw codes: unrecognized upstream codes
collapse to the coarse status (or unknown), so a code the network adds later can never leak
through this boundary.
provider_status (detail) | Coarse payment status |
|---|---|
completed, settled | success |
initiated, awaiting_bank_authorization, awaiting_redemption, processing, settling | processing |
rejected, rejected_insufficient_funds, failed, settlement_incomplete | failed |
expired, no_final_status | expired |
declined, canceled | canceled |
authorized, transfer_created, transfer_updated, refunded | no coarse change |
The last row matters: those detail statuses are recorded on the transaction timeline but do not
move the coarse status, so they never produce a payment.status_changed webhook on their own.
Related guides
- Getting Started — Create your first transaction end to end
- Error Reference — What each error code means and how to fix it
- Webhook Setup — Configure webhooks, verify signatures, handle retries