UrbanpayX supports outbound webhooks so each client can receive asynchronous business events. All webhook management endpoints require a client dashboard access token.
Supported Event Types
| Event | Description |
|---|---|
payment.status_changed | Payment status changed |
kyc.status_changed | KYC status changed |
opco.status_changed | Opco status changed |
project.frozen_changed | Project frozen state changed |
contract.participant.status_changed | Contract participant status changed |
contract.request.status_changed | Contract request status changed |
contract.request.completed | Contract request completed |
webhook.test | Webhook test |
Authentication
Security scheme: HTTP Bearer
Authorization: Bearer <access_token>Role Permissions
| Operation | Required Role |
|---|---|
| Read endpoints | Authenticated client users |
| Update / Rotate / Test endpoints | Owner, Developer |
Outbound Webhook Request Format
UrbanpayX sends an HTTP POST to your configured webhook_url.
Headers
Content-Type: application/json
X-UPX-Event-Id: <uuid>
X-UPX-Event-Type: <event_type>
X-UPX-Event-Version: v1
X-UPX-Timestamp: <unix_seconds>
X-UPX-Signature: sha256=<hex_hmac>Body Envelope
{
"id": "event_uuid",
"type": "payment.status_changed",
"version": "v1",
"occurred_at": "2026-02-26T12:45:00.123456+00:00",
"client_id": "client_uuid",
"data": {}
}Event Data Schemas
Each event type populates the data field with a specific schema.
payment.status_changed
payment.status_changed{
"transaction_id": "string",
"ref_id": "string",
"transfer_id": "string|null",
"user_id": "string",
"project_id": "string",
"previous_payment_status": "pending|processing|success|failed|canceled|expired",
"current_payment_status": "pending|processing|success|failed|canceled|expired",
"source": "string",
"token_status": "string|null",
"actor_type": "string|null",
"actor_account_id": "string|null"
}kyc.status_changed
kyc.status_changed{
"user_id": "string",
"email": "string",
"verification_mode": "urbanpayx_kyc|external_attestation",
"previous_kyc_status": "string",
"current_kyc_status": "string",
"previous_external_status": "string",
"current_external_status": "string",
"previous_verification_source": "string",
"current_verification_source": "string",
"source": "string"
}opco.status_changed
opco.status_changed{
"opco_id": "string",
"sub_tpp_id": "string|null",
"name": "string",
"previous_opco_status": "string",
"current_opco_status": "string",
"opco_status_reason": "string|null",
"source": "string"
}project.frozen_changed
project.frozen_changed{
"project_id": "string",
"project_name": "string",
"previous_is_frozen": false,
"current_is_frozen": true,
"source": "string",
"actor_type": "string|null",
"actor_id": "string|null"
}contract.participant.status_changed
contract.participant.status_changed{
"contract_request_id": "string",
"participant_id": "string",
"participant_key": "string",
"docuseal_submitter_id": "string|null",
"participant_status": "pending|completed|declined",
"source": "string"
}contract.request.status_changed
contract.request.status_changed{
"contract_request_id": "string",
"request_status": "pending|completed|declined|canceled|expired",
"current_stage_index": "integer|null",
"current_participant_key": "string|null",
"source": "string"
}contract.request.completed
contract.request.completed{
"contract_request_id": "string",
"completed_at": "2026-02-26T12:45:00.123456+00:00|null",
"source": "string"
}webhook.test
webhook.test{
"message": "UrbanpayX webhook test event",
"requested_by_account_id": "string|null"
}Signature Verification
All webhook requests are signed so you can verify they originate from UrbanpayX.
| Parameter | Value |
|---|---|
| Algorithm | HMAC SHA-256 |
| Signing payload | <X-UPX-Timestamp>.<raw_request_body> |
| Header format | X-UPX-Signature: sha256=<hex_digest> |
Steps
- Read the raw request body bytes exactly as received.
- Read
X-UPX-Timestampand reject if too old (recommended: 5 minutes). - Build the signing payload:
<timestamp>.<raw_body>. - Compute HMAC-SHA256 using your webhook secret.
- Compare against
X-UPX-Signatureusing a constant-time comparison. - Deduplicate events using
X-UPX-Event-Id.
Python Example
import hmac
import hashlib
def verify(sig_header: str, ts: str, raw_body: bytes, secret: str) -> bool:
payload = ts.encode() + b"." + raw_body
digest = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest()
expected = f"sha256={digest}"
return hmac.compare_digest(sig_header or "", expected)Recommended security practices:
- Reject requests with a missing or invalid signature.
- Reject stale timestamps (e.g., older than 5 minutes).
- Process events idempotently using the
X-UPX-Event-Idheader.
Delivery & Retry Semantics
- Success — any
2xxresponse from your server. - Retry — non-
2xxresponses or network failures trigger automatic retries with exponential backoff and jitter. - Rate limiting — for
429or503responses, theRetry-Afterheader (in seconds) is respected if present. - Dead letter — after max retry attempts, the event moves to a dead-letter state. Events that fail due to misconfiguration (disabled webhook, missing secret, invalid URL, or unsubscribed event type) are dead-lettered immediately without retry.
URL Validation Rules
When saving a webhook_url, UrbanpayX enforces the following rules:
- Scheme must be
https. localhostand internal hostnames are not allowed.- Host resolution cannot point to private, loopback, link-local, or reserved IP ranges.
Error Responses
| Status Code | Description |
|---|---|
400 | Invalid request payload, webhook URL, or event type |
401 | Unauthorized — missing or invalid access token |
403 | Authenticated but insufficient role for update, rotate, or test operations |
409 | Duplicate webhook event key (internal deduplication edge case) |
Related guides
- Transaction Lifecycle — Webhook payload examples for payment events
- Sandbox and Testing — Testing webhooks and verifying signatures
- Launch Checklist — Webhook verification checklist items
- Error Reference — Webhook configuration errors