API Reference

Overview

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-timestampThe Long type timestamp, such as 1711077773.
x-wk-signatureThe 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 and x-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.