When PayInGame sends a webhook to your endpoint, it includes a header called:
Payingame-Signature
This header lets you verify that the webhook was sent by PayInGame, not a malicious third party.
The header looks like this:
t=1762795211,v1=d4ec6316d9dcbbc06db0a18bdf8cdb73c8865d4d5134c9890ba39a15a8f17b26
- t: Unix timestamp (in seconds) when the webhook was generated
- v1: HMAC-SHA256 signature of the payload, computed using your secret key
How It Works
When your project’s webhook is triggered, we compute:
signedPayload = “timestamp.raw_body”
Then we sign that payload using your webhook secret key (a 256-bit hex string) with HMAC-SHA256:
hmacValue = hmac(signedPayload, secretKey, “HMACSHA256”, “UTF-8”)
Example
Example webhook payload:
{
"PaymentGuid": "9C4E0E58-ABF8-DFC3-D130-EF993228349F",
"ProjectGuid": "5E3E59A2-FC03-88DE-6135-C05FAE5BA7B2",
"Quantity": 1,
"Products": [
"7BC62A19-E33F-E99D-F582-B720FF46A8CA",
"7BC62A19-E33F-E99D-F582-B720FF46A8CA"
],
"UserID": "Cus123"
}
translates into string:
{"PaymentGuid":"9C4E0E58-ABF8-DFC3-D130-EF993228349F","ProjectGuid":"5E3E59A2-FC03-88DE-6135-C05FAE5BA7B2","Quantity":1,"Products":["7BC62A19-E33F-E99D-F582-B720FF46A8CA","7BC62A19-E33F-E99D-F582-B720FF46A8CA"],"UserID":"Cus123"}
This is the raw_body that is used later on.
Example signature header:
Payingame-Signature: t=1762795211,v1=36DCF83BDD5DD52F29A37091A78A0906285BCB7FBFA40DD829D26FEF81956F0B
Example secret key:
e3cf0f521274f2badab694b0b8c861823aae5b33a59eb4809332dc03bdb9297b
Computed HMAC:
hmac("1762795211.raw_body", secretKey, "HMACSHA256")
This results in:
36DCF83BDD5DD52F29A37091A78A0906285BCB7FBFA40DD829D26FEF81956F0B
Receiver side (example .net)
using System;
using System.Linq;
using System.Text;
using System.Security.Cryptography;
public class Program
{
public static void Main()
{
const string sigHeader = "t=1762795211,v1=36DCF83BDD5DD52F29A37091A78A0906285BCB7FBFA40DD829D26FEF81956F0B";
// This is the **exact minified JSON** that the signature was generated from.
// Any formatting (extra spaces, newlines, tabs) will change the computed HMAC.
string body = """
{"PaymentGuid":"9C4E0E58-ABF8-DFC3-D130-EF993228349F","ProjectGuid":"5E3E59A2-FC03-88DE-6135-C05FAE5BA7B2","Quantity":1,"Products":["7BC62A19-E33F-E99D-F582-B720FF46A8CA","7BC62A19-E33F-E99D-F582-B720FF46A8CA"],"UserID":"Cus123"}
""";
ReceiveWebhook(body, sigHeader);
}
// Your 256-bit hex secret from the PayInGame dashboard
private const string SecretKey = "e3cf0f521274f2badab694b0b8c861823aae5b33a59eb4809332dc03bdb9297b";
public static void ReceiveWebhook(string body, string sigHeader)
{
// Normalize potential CRLF issues (to ensure consistent body hashing)
body = body.Replace("\r\n", "\n");
// 1️⃣ Parse the t= and v1= values
var parts = sigHeader
.Split(',', StringSplitOptions.RemoveEmptyEntries)
.Select(p => p.Trim().Split('='))
.Where(p => p.Length == 2)
.ToDictionary(p => p[0], p => p[1]);
if (!parts.TryGetValue("t", out var timestamp))
{
Console.WriteLine("Missing timestamp");
return;
}
if (!parts.TryGetValue("v1", out var receivedSignature))
{
Console.WriteLine("Missing signature");
return;
}
// 2️⃣ Build message to verify: "timestamp.body"
var message = $"{timestamp}.{body}";
Console.WriteLine(message);
// 3️⃣ Compute HMAC-SHA256 using the shared secret
var secretBytes = Encoding.UTF8.GetBytes(SecretKey);
string computedSignature;
using (var hmac = new HMACSHA256(secretBytes))
{
var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(message));
computedSignature = Convert.ToHexString(hash);
}
// 4️⃣ Compare safely (constant time)
if (SecureEquals(computedSignature, receivedSignature))
{
Console.WriteLine("✅ Verified webhook!");
}
else
{
Console.WriteLine($"❌ Invalid signature:\nExpected: {receivedSignature}\nComputed: {computedSignature}");
}
}
private static bool SecureEquals(string a, string b)
{
if (a.Length != b.Length) return false;
int diff = 0;
for (int i = 0; i < a.Length; i++)
diff |= a[i] ^ b[i];
return diff == 0;
}
}