Theropay uses webhooks to notify partners whenever a payment status changes.
This allows your system to stay updated without needing to constantly poll the API.
- Receive payment status updates in real time
- Keep your system in sync with Theropay
- Automatically update your internal records
- Reduce the need for repeated API calls
Partners configure their webhook inside the Theropay Partner Portal.
When setting up a webhook, you receive:
- Webhook URL — where we send all events
- Webhook Secret — used to verify that the request really came from Theropay
Every webhook request contains two important headers:
| Header Name | Purpose |
|---|---|
| X-Theropay-Signature | HMAC SHA-256 signature used to verify the request |
| X-Theropay-Timestamp | Timestamp (milliseconds) when the event was sent |
You must verify both values before trusting the payload.
To verify the request:
Read the
X-Theropay-TimestampheaderCombine it with the raw request body:
{timestamp}.{requestBody}Hash this using HMAC-SHA256 with your webhook secret
Compare your hash with the value in X-Theropay-Signature
If they match → webhook is valid
If not → reject the request
[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;
}After you finish processing the webhook, your endpoint must return the correct HTTP status code.
| HTTP Status | Meaning |
|---|---|
| 200–299 | The webhook was received successfully. |
| 4xx | Your system could not process the webhook (we may retry in the future). |
| Other codes | Treated as a failed webhook delivery. |
Tip: Always return 200 OK after your system successfully handles the webhook.
Theropay currently does not have a fixed retry mechanism for failed webhook deliveries.
Make sure your webhook endpoint is reliable, stable, and responds quickly to avoid missing events.
Every webhook event includes the essential details you need to update your system:
- The event type
- Information about the payment or payee
- The updated status
- A timestamp for when the event was created