Skip to content
Last updated

Webhook – Payment Event Notifications

Overview

  • Webhooks are used to notify partners about payment-related events in real time.

  • When a partner (for example, a UK-based partner) initiates a payment through Theropay, the payment is processed by the Banking Provider.

  • Once the Banking Provider updates the payment status, Theropay receives this update and forwards the relevant information to the partner via a webhook.

  • This allows partners to stay informed about payment progress without continuously polling APIs.


High-Level Flow

  1. Partner initiates a payment using Theropay APIs.
  2. Theropay sends the payment to the Banking Provider for processing.
  3. The Banking Provider sends a webhook event to Theropay when the payment status changes.
  4. Theropay validates and processes the webhook.
  5. Theropay sends a webhook notification to the partner’s configured webhook URL.

Security & Signature Verification

To ensure webhook authenticity, every incoming webhook request is validated using HMAC SHA-256 signature verification.

Required Headers

Header NameDescription
X-Security-DigestHMAC SHA-256 signature of the request
X-Original-Transmission-TimeOriginal timestamp of the webhook event

How Signature Verification Works

  1. Theropay reads the raw request body.
  2. The original transmission timestamp is prepended to the request body.
  3. An HMAC SHA-256 hash is generated using a shared secret key.
  4. The generated hash is compared with the value sent in X-Security-Digest.

If the signature does not match, the webhook request is rejected.


Example Verification Code (C#)

[HttpPost("ReceiveWebhook")]
public async Task<IActionResult> ReceiveWebhook()
{
     var requestBody = await new StreamReader(Request.Body).ReadToEndAsync();
 
     var theropaySignature = Request.Headers["X-Theropay-Signature"].FirstOrDefault();
     var theropayTimestamp = Request.Headers["X-Theropay-Timestamp"].FirstOrDefault();
 
     var partnerSecret = "OIviKHaZVXcHg9QB0j8Dzq/1bf4y7CgKmIu9iQktMJ4="; // Base64 from Theropay on webhook creation
 
     var isValid = VerifySignature(
         requestBody,
         theropaySignature,
         theropayTimestamp,
         partnerSecret);
 
     if (!isValid)
     {
         return Unauthorized("Invalid webhook signature.");
     }
 
     // Process webhook event
     var json = JsonSerializer.Deserialize<object>(requestBody);
 
     return Ok();
}
 
public static bool VerifySignature(string requestBody, string theropaySignatureHeader, string theropayTimestamp, string partnerSecretPlainText)
{
     // remove sha256=
     var expectedSignatureHex = theropaySignatureHeader.StartsWith("sha256=")
         ? theropaySignatureHeader.Substring(7)
         : theropaySignatureHeader;
 
     // DO NOT Base64 decode — match sender logic
     var secretBytes = Encoding.UTF8.GetBytes(partnerSecretPlainText);
 
     var signingString = $"{theropayTimestamp}.{requestBody}";
     var signingBytes = Encoding.UTF8.GetBytes(signingString);
 
     using var hmac = new HMACSHA256(secretBytes);
     var hash = hmac.ComputeHash(signingBytes);
 
     var computedHex = Convert.ToHexString(hash).ToLower();
 
     return SlowEquals(computedHex, expectedSignatureHex);
}

private static bool SlowEquals(string a, string b)
{
     // Constant-time comparison to avoid timing attacks
     var aBytes = Encoding.UTF8.GetBytes(a);
     var bBytes = Encoding.UTF8.GetBytes(b);
 
     if (aBytes.Length != bBytes.Length)
         return false;
 
     int diff = 0;
     for (int i = 0; i < aBytes.Length; i++)
         diff |= aBytes[i] ^ bBytes[i];
 
     return diff == 0;
}

Webhook Event Structure

Every webhook request contains the following top-level fields:

{
  "eventType": "TRANSACTION_STATUS_UPDATE",
  "payload": { }
}
FieldDescription
eventTypeIdentifies the type of event being sent
payloadContains event-specific data

Summary

Webhooks provide a reliable and secure way to receive real-time payment updates. By validating signatures and handling events correctly, partners can seamlessly track payment lifecycles without additional API calls.