Configure webhook endpoints to listen for events within your UQPAY account, enabling your integration to automatically respond and trigger appropriate actions.
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.
Webhook headers
HTTP POST payloads that are sent to your webhook's endpoint URL. Endpoint 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 sha256 hash function and the secret as the HMAC key. |
Example event payload
The following event shows a subscription update at the end of a trial.
curl --request POST \
--url https://<your-website>/<your-webhook-endpoint> \
--header 'content-type: application/json' \
--header 'x-wk-timestamp: 1711077773' \
--header 'x-wk-signature: <webhook-signatures generated by UQPAY>' \
--data '
{
"version": "V1.5.2",
"event_type": "ISSUING",
"event_name": "issuing.transaction.declined",
"event_id": "8a78af1e-de83-43a5-b177-ecbc6a8a9fc6",
"data": {
"card_id": "c0cef051-29c5-4796-b86a-cd5b684bfad7",
"card_number": "************5668",
"card_order_id": "c0cef051-29c5-4796-b86a-cd5ee34bfad7",
"transaction_id": "5135e6cc-28b6-4889-81dc-3b86a09e1395",
"transaction_type": "AUTHORIZATION",
"authorization_code": 856268,
"billing_amount": 70.25,
"billing_currency": "SGD",
"transaction_amount": 100.25,
"transaction_currency": "USD",
"transaction_time": "2024-01-11 18:40:10",
"posted_time": "2024-01-13 13:40:10",
"merchant_data": [
{
"category_code": 5734,
"city": "string",
"country": "string",
"name": "string"
}
],
"failure_reason": "1107 - Invalid CVV2",
"transaction_status": "DECLINED"
}
}
'
Whitelist IP addresses
We emits Webhook calls using one of the following IPs. To receive webhook calls successfully, these IPs must be whitelisted:
For Sandbox Environment
34.96.187.146
For Production Environment
18.143.59.64
,54.179.248.205
,34.142.170.52
,8.219.110.188
Tips for using webhooks
Receive events with an HTTPS server
Please use an HTTPS URL for your webhook endpoint for security considerations. Furthermore, your server must be correctly configured to support HTTPS.
Verify events are sent from UQPAY
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.
The following section describes how to verify webhook signatures:
- Retrieve your endpoint’s secret.
- Verify the signature.
How to check webhook signatures:
- Extract the
x-wk-timestamp
andx-wk-signature
from the header. - Prepare the value_to_digest string. You achieve this by concatenating: the
x-wk-timestamp
(as a string) and the actual JSON payload (the request's body, as a string). - Compute an HMAC with the SHA-256 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
in the header to the expected signature. If a signature matches, compute the difference between the current timestamp and the received timestamp, then decide if the difference is within your tolerance.
Common issues : the signature doesn't match
When creating the expected signature, please make sure to use the raw JSON payload.
Note: many libraries tend to format the JSON while parsing the payload, so it's recommended to check the signature before any transformation occurs.
Signatures code example
...
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, We will resend the notification message several times over the course of three days until a successful response is returned.
Acknowledge events immediately
If your webhook script performs complex logic, it's possible that the script would time out before UQPAY sees its complete execution. Ideally, your webhook handling code (acknowledging receipt of an event by returning a 200 status code) is separate from any processing performed for that event.
Handle duplicate events
Webhook 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, please use the event_id
field sent on the event. Across retry, the same event keeps the same event_id
.
Order of events
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.