Documentation Index
Fetch the complete documentation index at: https://uqpay-ecc4f4d8.mintlify.app/llms.txt
Use this file to discover all available pages before exploring further.
Why use webhooks
When building UQPAY integrations, you might want your applications to receive events as they occur in your UQPAY accounts, so that your backend systems can execute actions accordingly.
To enable webhook events, you need to register webhook endpoints. After you register them, UQPAY can push real-time event data to your application’s webhook endpoint when events happen in your UQPAY account. UQPAY uses HTTPS to send webhook events to your app as a JSON payload that includes an Event object.
Receiving webhook events is particularly useful for listening to asynchronous events such as when a customer’s bank confirms a payment, a customer disputes a charge, a recurring payment succeeds, or when collecting subscription payments.
Prerequisites
Before you read this, set up your webhook endpoint in the dashboard.
HTTP POST payloads that are sent to your webhook’s endpoint URL will contain several special headers:
HEADER | DESCRIPTION |
|---|
| x-wk-timestamp | The Long type timestamp, such as 1711077773. |
| x-wk-signature | The HMAC512 hex digest of the response body. This header will be sent if the webhook is configured with a secret. The HMAC512 hex digest is generated using the sha512 hash function and the secret as the HMAC key. |
Example event payload
{
"version": "V1.6.0",
"event_name": "ISSUING",
"event_type": "issuing.transaction.authorization",
"event_id": "234fca01-1ace-4d34-baf5-29b10e9d11c0",
"source_id": "1a53aebf-900c-4e25-9852-b98f4338d94c",
"data": {
"authorization_code": "W6MJU9",
"billing_amount": "11.5",
"billing_currency": "USD",
"card_available_balance": "988.5",
"card_id": "50418faa-57a8-4ce2-9157-621b00b13a3b",
"card_number": "40963608****1764",
"cardholder_id": "25ea804d-7fd5-43d5-8792-0fc0214cdb2f",
"description": "",
"fee_pass_through": "Y",
"merchant_data": [
{
"category_code": "5734",
"city": "",
"country": "",
"name": "Test Merchant"
}
],
"original_transaction_id": "",
"posted_time": "2026-04-12T15:27:39.563+08:00",
"short_transaction_id": "T260412-2AHV51KO5U68",
"transaction_amount": "10",
"transaction_currency": "USD",
"transaction_fee": "1.5",
"transaction_fee_currency": "USD",
"transaction_id": "1a53aebf-900c-4e25-9852-b98f4338d94c",
"transaction_status": "APPROVED",
"transaction_time": "2026-04-12T15:27:39.48+08:00",
"transaction_type": "AUTHORIZATION",
"wallet_type": ""
}
}
IP whitelist
UQPAY emits webhook calls using one of the following IPs. To receive webhook calls successfully, these IPs must be whitelisted:
Sandbox environment
Production environment
18.143.59.64, 54.179.248.205, 13.250.234.88, 18.136.58.213
Signature verification
UQPAY sends webhook events from a set list of IP addresses. Only trust events coming from these IP addresses.
Additionally, verify webhook signatures to confirm that received events are sent from UQPAY. UQPAY signs webhook events it sends to your endpoints by including a signature in each event’s x-wk-signature header. This allows you to verify that the events were sent by UQPAY, not by a third party. You can verify signatures either using our official libraries, or verify manually using your own solution.
To verify a webhook signature:
- Retrieve your endpoint’s secret.
- Extract the
x-wk-timestamp and x-wk-signature from the request headers.
- Prepare the
value_to_digest string by concatenating: the raw JSON payload (the request body, as a string) and the x-wk-timestamp (as a string).
- Compute an HMAC with the SHA-512 hash function. Use the endpoint’s signing secret as the key, and use the
value_to_digest string as the message.
- Compare the
x-wk-signature header to the expected signature. If they match, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.
Common issue: signature doesn’t matchWhen creating the expected signature, make sure to use the raw JSON payload.Many libraries format the JSON while parsing the payload, so check the signature before any transformation occurs.
Code examples
Go
Java
PHP
Rust
TypeScript
...
payload = "" //webhook payload
secret = '<YOUR_WEBHOOK_SECRET>'
timestamp := c.GetHeader("x-wk-timestamp")
signature := c.GetHeader("x-wk-signature")
valueToDigest := payload + timestamp
signatureHex := calculateHMACSHA512([]byte(secret), []byte(valueToDigest))
//compare signatureHex with signature
...
...
StringBuilder valueToDigest = new StringBuilder();
String payload = ""; //webhook payload
// Get the timestamp, signature from header
String timestamp = request.getHeader("x-wk-timestamp");
String signature = request.getHeader("x-wk-signature");
valueToDigest.append(payload);
valueToDigest.append(timestamp);
// Get your secret
String secret = getSecret();
String signatureHex = HmacUtils.hmacSha512Hex(secret, valueToDigest.toString())
...
<?php
...
$timestamp = $request->getHeaderLine('x-wk-timestamp');
$payload = $request->getBody()->getContents();
$secret = <your signatuer secret>;
$signature = $request->getHeaderLine('x-wk-signature');
if (hash_hmac('sha512', $payload.$timestamp, $secret) != $signature) {
return new Response(400, array(), 'failed to verify the signature');
}
// Do something with event
return new Response(200, array(), $body);
}
use ring::hmac;
fn verify_signature(payload: &str, timestamp: &str, received_signature: &str, secret: &str) -> bool {
let key = hmac::Key::new(hmac::HMAC_SHA512, secret.as_bytes());
let value_to_digest = format!("{}{}", payload, timestamp);
let signature = hmac::sign(&key, value_to_digest.as_bytes());
let signature_hex = hex::encode(signature.as_ref());
signature_hex == received_signature
}
fn main() {
let payload = ""; // Your webhook payload here
let secret = "<YOUR_WEBHOOK_SECRET>";
let timestamp = "x-wk-timestamp header value here";
let received_signature = "x-wk-signature header value here";
if verify_signature(payload, timestamp, received_signature, secret) {
println!("Signature verified!");
} else {
println!("Signature verification failed!");
}
}
// express.js
const crypto = require('crypto')
const secret = '<YOUR_WEBHOOK_SECRET>'
async webhookController(ctx, next) {
// webhook is received
const { headers, body } = ctx.request
const { name, accountId } = body || {} // payload
const timestamp = headers['x-wk-timestamp']
const payload = `${body}${timestamp}`
const signatureHex = crypto.createHmac('sha512', secret).update(payload).digest('hex')
if (signatureHex === headers['x-wk-signature']) {
// do business logic after signature is verified
return next() // http response code = 200: ack the webhook
} else {
ctx.status = 500
ctx.body = 'failed to verify webhook signature'
return
}
}
Retry logic
If your webhook endpoint is unavailable or takes too long to respond, UQPAY will resend the notification with exponential backoff until a successful response is returned or the retry limit is reached.
Retries are scheduled using exponential backoff: each retry occurs after 2^(attempts+3) seconds, starting at 16s and increasing with each attempt, capped at 900s (15 minutes). The maximum number of retry attempts is 5.
Best practices
If your webhook script performs complex logic, the script could time out before UQPAY sees its complete execution. Keep your webhook handling code (acknowledging receipt of an event by returning a 200 status code) separate from any processing performed for that event.
Handle duplicate events
Important — Handle duplicate eventsWebhook endpoints might occasionally receive the same event more than once. We advise you to guard against receiving duplicate events by making your event processing idempotent.To deduplicate events, use the event_id field included in each event payload. For any retry attempts (e.g., when a successful HTTP 200 response is not returned to UQPAY), the same event_id will be reused, allowing you to safely ignore duplicates.
Event ordering
UQPAY does not guarantee delivery of events in the order in which they are generated. Your endpoint should not expect delivery of these events in this order and should handle this accordingly. You can also use x-wk-timestamp in the event for ordering.