When making requests to an API, you typically expect a quick response. However, for some long-running operations, an immediate response is not always feasible, and waiting could lead to timeout errors. In such cases, the API might return a "pending" response while continuing to process your request asynchronously. To keep your records updated with the final status, you can:
- Poll the API: Send periodic requests to the API to get updated information.
- Use Webhooks: Provide a URL endpoint to which the API sends updates automatically as events occur.
Webhooks are an efficient solution for monitoring the progress of asynchronous operations such as transactions or payouts. For example, webhook events can be triggered to describe the current state of the operation. Register a URL with Create Webhook and choose which events it receives.
Events
| Event | Sent when | Details |
|---|---|---|
transaction_success | A payment succeeds | Transactions events |
transaction_failed | A payment fails | Transactions events |
transaction_cancelled | The customer rejects a payment request | Transactions events |
transaction_updated | A payment changes in another way | Transactions events |
payout_received | A payout or transfer is created | Payouts events |
payout_completed | The bank confirms a payout | Payouts events |
payout_failed | Vendy declines a payout and refunds the wallet | Payouts events |
payout_cancelled | A payout is cancelled and the wallet refunded | Payouts events |
virtual_account_funded | A dedicated account receives a transfer | Wallet events |
wallet_funded | A wallet's account number receives a transfer | Wallet events |
Every event has the same shape: {"event.type": "<event>", "data": {...}}.
Validating Webhook Signatures
To ensure the authenticity of incoming webhook events, Vendy includes an X-Signature header with each event. This header contains a base64-encoded HMAC SHA256 signature of the JSON request body, using the secretHash you provided when setting up the webhook as the key. Before processing any incoming event, you should validate this signature.
These code samples demonstrate how to validate the signature of a webhook payload to ensure its authenticity. Make sure to replace your-secret-hash with your actual secret key for proper validation.
Use the raw request bodyCompute the signature over the body exactly as you received it. If you parse the JSON and serialise it again, spacing or key order can change and the signature won't match.
Here’s how to validate the signature in Node.js, Java, and C#:
Node.js Implementation
const crypto = require('crypto');
const express = require('express');
const app = express();
app.use(express.raw({ type: 'application/json' })); // Keep the raw body for the signature
const secretHash = 'your-secret-hash';
app.post('/webhook', (req, res) => {
const signature = req.headers['x-signature'];
const payload = req.body.toString('utf8');
const computedSignature = crypto
.createHmac('sha256', secretHash)
.update(payload)
.digest('base64');
if (signature === computedSignature) {
console.log('Valid webhook event:', JSON.parse(payload));
res.sendStatus(200); // Respond with success
} else {
console.error('Invalid signature');
res.sendStatus(400); // Respond with error
}
});
app.listen(3000, () => {
console.log('Listening on port 3000');
});Java Implementation
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.util.Base64;
import javax.servlet.http.*;
import java.io.BufferedReader;
public class WebhookServlet extends HttpServlet {
private static final String SECRET_HASH = "your-secret-hash";
@Override
protected void doPost(HttpServletRequest request, HttpServletResponse response) {
try {
String signature = request.getHeader("X-Signature");
StringBuilder payload = new StringBuilder();
BufferedReader reader = request.getReader();
String line;
while ((line = reader.readLine()) != null) {
payload.append(line);
}
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(SECRET_HASH.getBytes(), "HmacSHA256"));
byte[] computedHash = mac.doFinal(payload.toString().getBytes());
String computedSignature = Base64.getEncoder().encodeToString(computedHash);
if (signature.equals(computedSignature)) {
System.out.println("Valid webhook event: " + payload);
response.setStatus(HttpServletResponse.SC_OK);
} else {
System.out.println("Invalid signature");
response.setStatus(HttpServletResponse.SC_BAD_REQUEST);
}
} catch (Exception e) {
e.printStackTrace();
response.setStatus(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
}
}C# Implementation
using System;
using System.IO;
using System.Security.Cryptography;
using System.Text;
using System.Threading.Tasks;
using Microsoft.AspNetCore.Mvc;
[Route("webhook")]
[ApiController]
public class WebhookController : ControllerBase
{
private const string SecretHash = "your-secret-hash";
[HttpPost]
public async Task<IActionResult> HandleWebhook()
{
try
{
string signature = Request.Headers["X-Signature"];
using var reader = new StreamReader(Request.Body);
string payloadString = await reader.ReadToEndAsync(); // raw body, not re-serialised
using (var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(SecretHash)))
{
var computedHash = hmac.ComputeHash(Encoding.UTF8.GetBytes(payloadString));
var computedSignature = Convert.ToBase64String(computedHash);
if (computedSignature == signature)
{
Console.WriteLine("Valid webhook event: " + payloadString);
return Ok();
}
else
{
Console.WriteLine("Invalid signature");
return BadRequest();
}
}
}
catch (Exception ex)
{
Console.WriteLine($"Error: {ex.Message}");
return StatusCode(500, "Internal server error");
}
}
}