Webhook
Because your notification URL is public and can be called by anyone, you must validate each event notification to confirm that it came from the system.
A non-system post can potentially compromise your application
All webhook notifications from the system include an x-hmac-sha256-signature header.
The value of this header is an HMAC-SHA-256 signature generated using your webhook signature key and the raw body of the request.
You can validate the webhook notification by generating the HMAC-SHA-256 value in your own code and comparing it to the signature of the event notification you received.
The following functions generates an HMAC-SHA256 signature from your signature key and the event notification body.
You can then compare the result with the event notification’s x-hmac-sha256-signature.
Before you respond to a webhook, you need to verify that the webhook was sent from the system.
The system verifies SSL certificates when delivering payloads to HTTPS webhook addresses. Make sure your server is correctly configured to support HTTPS with a valid SSL certificate.
import org.junit.Test;import org.slf4j.Logger;import org.slf4j.LoggerFactory;
import javax.crypto.Mac;import javax.crypto.SecretKey;import javax.crypto.spec.SecretKeySpec;import java.security.InvalidKeyException;import java.security.NoSuchAlgorithmException;import java.util.Base64;
import static java.nio.charset.StandardCharsets.UTF_8;
public class Signature { public static void main(String[] args) throws Exception { String key = "kjdfkdfjdlfkjaoldasjdflidufidfuf"; byte[] bodyBytes = "{\"orderId\" : 123}".getBytes(UTF_8);
SecretKey secretKey = new SecretKeySpec(key.getBytes(UTF_8), "HmacSHA256"); Mac mac = Mac.getInstance("HmacSHA256"); mac.init(secretKey); byte[] signatureBytes = mac.doFinal(bodyBytes); String signature = Base64.getEncoder().encodeToString(signatureBytes);
LOG.debug("signature = {}", signature); }}using System.Security.Cryptography;using Microsoft.Extensions.Primitives;using System.Text;using System.Text.Json;using System.Text.Json.Serialization;
var builder = WebApplication.CreateBuilder(args);var app = builder.Build();
app.MapPost("/webhook/dotnet", async (HttpContext context) =>{
// check if header with signature is present if (!context.Request.Headers.TryGetValue("x-hmac-sha256-signature", out StringValues signature)) { return Results.BadRequest("Header 'x-hmac-sha256-signature' not found."); }
// Convert the body stream to a byte array using var ms = new MemoryStream(); await context.Request.Body.CopyToAsync(ms); byte[] byteArray = ms.ToArray();
// validate signature if (!ValidateSignature(signature!, byteArray)) { return Results.BadRequest("Bad signature"); }
Console.WriteLine($"Received byte array of length: {byteArray.Length}");
string json = Encoding.UTF8.GetString(byteArray);
Console.WriteLine($"Received json is {json}");
WebhookRequest? webhookRequest = JsonSerializer.Deserialize<WebhookRequest>(json);
Console.WriteLine($"Received webook {webhookRequest}");
return Results.Ok();});
app.Run();
bool ValidateSignature(string signature, byte[] body) { String secret = "WEBHOOK_SECRET"; using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret)); byte[] hashBytes = hmac.ComputeHash(body); String hashString = Convert.ToBase64String(hashBytes);
Console.WriteLine($"Expected hash is {hashString}, received {signature}"); return signature.Equals(hashString);}
public record WebhookRequest { public string? correlationId { get; init; } public long? orderSystemId { get; init; } public string? orderMerchantId { get; init; } public string? orderState { get; init; }}import express, { Request, Response } from 'express';import crypto from 'crypto';
type WebhookRequest = { correlationId: string; orderSystemId: number; orderMerchantId: number; orderState: string;};
const app = express();const port = 3000;
app.use(express.raw({ type: '*/*' }));
app.post('/webhook/typescript-express', (req: Request, res: Response) => { const secretKey = 'WEBHOOK_SECRET'; const bodyBuffer = req.body;
const hmac = crypto.createHmac('sha256', secretKey); hmac.update(bodyBuffer); const calculatedHmacDigest = hmac.digest();
const expectedHmacBase64 = req.header('x-hmac-sha256-signature') ?? ''; const expectedHmacDigest = Buffer.from(expectedHmacBase64, 'base64');
if (!calculatedHmacDigest.equals(expectedHmacDigest)) { console.log('HMACs do not match', 'expected', expectedHmacBase64, 'calculated', calculatedHmacDigest.toString('base64')); res.status(403).send('Wrong signature'); return; }
console.log('HMACs match');
const json: string = bodyBuffer.toString('utf8'); const webhookRequest: WebhookRequest = JSON.parse(json);
console.log('Webhook request', webhookRequest);
res.status(200).send('Signature OK');});
app.listen(port, () => { console.log(`Server running on port ${port}`);});Respond to the webhook
Your webhook acknowledges that it received data by sending a 200 OK response.
Any response outside of the 200 range, including 3XX HTTP redirection codes, indicates that you didn’t receive the webhook.
Ths system doesn’t follow redirects for webhook notifications and considers them to be an error response.
Retry frequency
The has implemented a five-second timeout period and a retry period for webhook subscriptions.
The system waits 10 seconds for a response to each request to a webhook. If there’s no response, or an error is returned, then the system retries the connection 30 times over the next 360 hours.
To avoid timeouts and errors, consider deferring app processing until after the webhook response has been successfully sent.