Overview

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:

  1. Poll the API: Send periodic requests to the API to get updated information.
  2. 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

EventSent whenDetails
transaction_successA payment succeedsTransactions events
transaction_failedA payment failsTransactions events
transaction_cancelledThe customer rejects a payment requestTransactions events
transaction_updatedA payment changes in another wayTransactions events
payout_receivedA payout or transfer is createdPayouts events
payout_completedThe bank confirms a payoutPayouts events
payout_failedVendy declines a payout and refunds the walletPayouts events
payout_cancelledA payout is cancelled and the wallet refundedPayouts events
virtual_account_fundedA dedicated account receives a transferWallet events
wallet_fundedA wallet's account number receives a transferWallet 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 body

Compute 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");
        }
    }
}