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.
- Partner initiates a payment using Theropay APIs.
- Theropay sends the payment to the Banking Provider for processing.
- The Banking Provider sends a webhook event to Theropay when the payment status changes.
- Theropay validates and processes the webhook.
- Theropay sends a webhook notification to the partner’s configured webhook URL.
To ensure webhook authenticity, every incoming webhook request is validated using HMAC SHA-256 signature verification.
| Header Name | Description |
|---|---|
X-Security-Digest | HMAC SHA-256 signature of the request |
X-Original-Transmission-Time | Original timestamp of the webhook event |
- Theropay reads the raw request body.
- The original transmission timestamp is prepended to the request body.
- An HMAC SHA-256 hash is generated using a shared secret key.
- The generated hash is compared with the value sent in
X-Security-Digest.
If the signature does not match, the webhook request is rejected.
[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;
}Every webhook request contains the following top-level fields:
{
"eventType": "TRANSACTION_STATUS_UPDATE",
"payload": { }
}| Field | Description |
|---|---|
eventType | Identifies the type of event being sent |
payload | Contains event-specific data |
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.