Client Webhooks

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

EventDescription
payment.status_changedPayment status changed
kyc.status_changedKYC status changed
opco.status_changedOpco status changed
project.frozen_changedProject frozen state changed
contract.participant.status_changedContract participant status changed
contract.request.status_changedContract request status changed
contract.request.completedContract request completed
webhook.testWebhook test

Authentication

Security scheme: HTTP Bearer

Authorization: Bearer <access_token>

Role Permissions

OperationRequired Role
Read endpointsAuthenticated client users
Update / Rotate / Test endpointsOwner, 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

{
  "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

{
  "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_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_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_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_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_id": "string",
  "completed_at": "2026-02-26T12:45:00.123456+00:00|null",
  "source": "string"
}

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.

ParameterValue
AlgorithmHMAC SHA-256
Signing payload<X-UPX-Timestamp>.<raw_request_body>
Header formatX-UPX-Signature: sha256=<hex_digest>

Steps

  1. Read the raw request body bytes exactly as received.
  2. Read X-UPX-Timestamp and reject if too old (recommended: 5 minutes).
  3. Build the signing payload: <timestamp>.<raw_body>.
  4. Compute HMAC-SHA256 using your webhook secret.
  5. Compare against X-UPX-Signature using a constant-time comparison.
  6. 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-Id header.

Delivery & Retry Semantics

  • Success — any 2xx response from your server.
  • Retry — non-2xx responses or network failures trigger automatic retries with exponential backoff and jitter.
  • Rate limiting — for 429 or 503 responses, the Retry-After header (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.
  • localhost and internal hostnames are not allowed.
  • Host resolution cannot point to private, loopback, link-local, or reserved IP ranges.

Error Responses

Status CodeDescription
400Invalid request payload, webhook URL, or event type
401Unauthorized — missing or invalid access token
403Authenticated but insufficient role for update, rotate, or test operations
409Duplicate webhook event key (internal deduplication edge case)

Related guides