Signature
Every webhook notification we send you (deposits and withdrawals) is signed. The signature lets your endpoint confirm two things before it acts on a notification:
- Authenticity — the request genuinely came from us.
- Integrity — the body was not altered in transit.
Validating the signature on every webhook is how you keep your endpoints safe from spoofed or tampered requests.
Where the signature is
The signature travels in the request header of every webhook call we make to your endpoints:
Authorization: Bearer <signature>
<signature> is the lowercase hex-encoded SHA256 described below.
The formula
signature = SHA256( affiliate_username + raw_json_body + affiliate_username )
affiliate_usernameis the Affiliate Username we provide with your credentials.raw_json_bodyis the exact raw body of the request, byte for byte.- The username is concatenated immediately before and immediately after the raw body, and the whole string is SHA256-hashed and hex-encoded.
To validate a notification, you rebuild this value from the request you received and compare it to the Bearer value in the header.
Always compute the hash from the raw request body exactly as received. Do not parse the JSON into an object and serialize it again before hashing.
Re-serializing changes the bytes — whitespace, key order, and number formatting (for example 100.00 becomes 100) — even though the data "looks" the same. Any of those differences produces a different hash, so validation fails. This is the most common cause of "signature doesn't match" problems. Read the raw body first, validate the signature, and only then parse the JSON.
Worked example
You can reproduce this end to end.
Affiliate Username (provided with your credentials):
AFFILIATE_TESTING
Raw request body (the exact bytes we send — a deposit notification):
{"status":"success","code":"0000","point_of_sale":{"id":124,"name":"Sucursal de Pruebas","currency_code":"MXN"},"deposit":{"username":"5555555555","description":"Deposito en Cuenta - 4345FF2XB7F323CD","transaction_number":"4345FF2XB7F323CD","amount":100.00,"currency_code":"MXN"},"created_at":"2019-05-18 13:18:37"}
String that gets hashed — affiliate_username + raw body + affiliate_username:
AFFILIATE_TESTING{"status":"success","code":"0000","point_of_sale":{"id":124,"name":"Sucursal de Pruebas","currency_code":"MXN"},"deposit":{"username":"5555555555","description":"Deposito en Cuenta - 4345FF2XB7F323CD","transaction_number":"4345FF2XB7F323CD","amount":100.00,"currency_code":"MXN"},"created_at":"2019-05-18 13:18:37"}AFFILIATE_TESTING
Resulting signature — SHA256(...), hex-encoded:
5ef11c6d71fa9b2c76b55cdf9eb599c449830bdbe79cf16a4830e7204921accf
This is the value you would receive as Authorization: Bearer 5ef11c6d71fa9b2c76b55cdf9eb599c449830bdbe79cf16a4830e7204921accf.
Validating a notification
On your side, for each webhook you receive:
- Read the raw body exactly as received, before any JSON parsing.
- Recompute
SHA256(affiliate_username + raw_body + affiliate_username), hex-encoded, using the Affiliate Username we provided. - Read the header value and strip the
Bearerprefix to get the received signature. - Compare the recomputed signature to the received one using a constant-time comparison (not
==), to avoid leaking timing information. - If they don't match, reject the request with an error response (for example
401 Unauthorized) and do not process it. If they match, parse the body and continue.
Receiver-side validation
Each snippet rebuilds the signature from the raw body and compares it in constant time. In every language, the critical line is the one that reads the raw body — if you hash a parsed-and-re-serialized object instead, validation will fail.
PHP
<?php
// Critical: read the RAW body — do not json_decode then re-encode.
$rawBody = file_get_contents('php://input');
$affiliateUsername = 'AFFILIATE_TESTING'; // provided with your credentials
$expected = hash('sha256', $affiliateUsername . $rawBody . $affiliateUsername);
$header = $_SERVER['HTTP_AUTHORIZATION'] ?? '';
$received = preg_replace('/^Bearer\s+/i', '', $header);
if (!hash_equals($expected, $received)) {
http_response_code(401);
exit;
}
// Signature is valid — safe to decode and process.
$payload = json_decode($rawBody, true);
Node.js (Express)
const crypto = require('crypto');
// Critical: capture the RAW body buffer, not JSON.stringify(req.body).
// Mount this route with: app.use(express.raw({ type: '*/*' }));
app.post('/webhooks/deposits', (req, res) => {
const rawBody = req.body; // Buffer of the raw request body
const affiliateUsername = 'AFFILIATE_TESTING'; // provided with your credentials
const expected = crypto
.createHash('sha256')
.update(affiliateUsername + rawBody.toString('utf8') + affiliateUsername)
.digest('hex');
const received = (req.get('authorization') || '').replace(/^Bearer\s+/i, '');
const a = Buffer.from(expected);
const b = Buffer.from(received);
if (a.length !== b.length || !crypto.timingSafeEqual(a, b)) {
return res.sendStatus(401);
}
const payload = JSON.parse(rawBody.toString('utf8'));
// ... process the notification
res.sendStatus(201);
});
Python (Flask)
import hashlib
import hmac
from flask import request, abort
AFFILIATE_USERNAME = "AFFILIATE_TESTING" # provided with your credentials
@app.post("/webhooks/deposits")
def deposits():
# Critical: read the RAW body bytes, not request.json re-serialized.
raw_body = request.get_data()
expected = hashlib.sha256(
AFFILIATE_USERNAME.encode() + raw_body + AFFILIATE_USERNAME.encode()
).hexdigest()
received = request.headers.get("Authorization", "")
received = received.removeprefix("Bearer ").strip()
if not hmac.compare_digest(expected, received):
abort(401)
payload = request.get_json()
# ... process the notification
return "", 201
Java (Spring)
import java.nio.charset.StandardCharsets;
import java.security.MessageDigest;
import java.util.HexFormat;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
public class WebhookController {
private static final String AFFILIATE_USERNAME = "AFFILIATE_TESTING"; // provided with your credentials
// Critical: receive the RAW bytes (byte[]), not a parsed/re-serialized object.
@PostMapping("/webhooks/deposits")
public ResponseEntity<Void> deposits(
@RequestBody byte[] rawBody,
@RequestHeader("Authorization") String authHeader) throws Exception {
byte[] user = AFFILIATE_USERNAME.getBytes(StandardCharsets.UTF_8);
MessageDigest md = MessageDigest.getInstance("SHA-256");
md.update(user);
md.update(rawBody);
md.update(user);
String expected = HexFormat.of().formatHex(md.digest());
String received = authHeader.replaceFirst("(?i)^Bearer\\s+", "");
boolean ok = MessageDigest.isEqual(
expected.getBytes(StandardCharsets.UTF_8),
received.getBytes(StandardCharsets.UTF_8));
if (!ok) {
return ResponseEntity.status(401).build();
}
// Signature is valid — safe to parse rawBody and process.
return ResponseEntity.status(201).build();
}
}
C# (ASP.NET Core)
using System.Security.Cryptography;
using System.Text;
const string affiliateUsername = "AFFILIATE_TESTING"; // provided with your credentials
app.MapPost("/webhooks/deposits", async (HttpRequest request) =>
{
// Critical: read the RAW body stream, not a re-serialized model.
request.EnableBuffering();
using var reader = new StreamReader(request.Body, Encoding.UTF8, leaveOpen: true);
string rawBody = await reader.ReadToEndAsync();
request.Body.Position = 0;
byte[] expected = SHA256.HashData(
Encoding.UTF8.GetBytes(affiliateUsername + rawBody + affiliateUsername));
string header = request.Headers.Authorization.ToString();
string receivedHex = header.Replace("Bearer ", "", StringComparison.OrdinalIgnoreCase).Trim();
byte[] received = Convert.FromHexString(receivedHex);
if (!CryptographicOperations.FixedTimeEquals(expected, received))
{
return Results.Unauthorized();
}
// Signature is valid — safe to parse rawBody and process.
return Results.StatusCode(201);
});