Payment Status Update Webhook

Configure this webhook against a specific Integration in your Business Portal.

HMAC signature verification

Every webhook request includes a super-signature header so you can verify it came from Super and hasn't been tampered with.

Header format

The header contains a timestamp and a signature, separated by a comma:

super-signature: t:1669219987926,v1:vCcZMqom...base64...

  • t: — Unix timestamp in milliseconds, generated when the request was signed
  • v1: — HMAC-SHA256 signature, base64-encoded

Verifying a request

  1. Parse the header. Split on "," to get the timestamp and signature parts, then split each on ":" to extract the values.
  2. Build the signed message. Concatenate the timestamp and the raw request body, with no separator: "message = timestamp + raw_body"
  3. Use the raw bytes of the request body — do not re-serialize parsed JSON, as whitespace or key ordering changes will invalidate the signature.
  4. Compute the expected signature. Generate an HMAC-SHA256 of message using your webhook secret as the key, and base64-encode the result.
  5. Compare signatures. Use a constant-time comparison (e.g. crypto.timingSafeEqual in Node, hmac.compare_digest in Python) to avoid timing attacks. Reject the request if they don't match.
  6. Check the timestamp. Reject requests whose timestamp is more than 5 minutes old to prevent replay attacks.

Example (Node.js)

const crypto = require('crypto');

function verifySuperSignature(rawBody, header, secret) {
  const parts = Object.fromEntries(
    header.split(',').map((p) => { return p.split(':'); })
  );
  const timestamp = parts.t;
  const signature = parts.v1;

  if (Math.abs(Date.now() - Number(timestamp)) > 5 * 60 * 1000) {
    return false;
  }

  const expected = crypto
    .createHmac('sha256', secret)
    .update(`${timestamp}${rawBody}`)
    .digest('base64');

  const expectedBuf = Buffer.from(expected, 'base64');
  const actualBuf = Buffer.from(signature, 'base64');

  if (expectedBuf.length !== actualBuf.length) {
    return false;
  }

  return crypto.timingSafeEqual(expectedBuf, actualBuf);
}
Payload

This webhook is called by our payment gateway to inform you of a payment status changes, prior to our payments frontend redirecting the customer back to the given return URL, as specified in the initiate-payment-transaction request body.

const
enum
required

This will be set to: PaymentStatus

Allowed:
string
required

A unique ID for the payment transaction.

string
required

An 18 character payment transaction reference allocated by us, used/visible on the associated bank transaction.

string
enum
required

The payment status of the given payment transaction, specified by the super-transaction_id field above. The payment status can be one of:

  • PaymentSuccess: The payment transaction was successful, money has moved from the customer to your holding account.
  • PaymentCancelled: The payment transaction was cancelled by the customer, in their banking app.
  • PaymentFailed: The payment transaction failed due to a technical error, either on our side or with the customer's bank
  • PaymentDelayed: The payment transaction was not resolved by the bank within 15 seconds (<2% of transactions). Open banking can take up to 4 hours to resolve. The customer has the funds, but this is usually due to additional fraud checks within the banking system.
  • PaymentAbandoned: The payment transaction was abandoned by the customer. They closed the browser or just left the page open without explicitly clicking cancel.
Allowed:
integer
required

The payment transaction amount in minor units.

string

Your unique reference for the payment transaction e.g. your orderId

Response
200

Return a 200 status to indicate that the data was received successfully, any other response will be deemed a failure and retried

LoadingLoading…