Webhook Verification

Overview

The Suregifts Vouchers system sends webhook notifications to merchants for key voucher lifecycle events. Webhooks are sent via HTTP POST requests with HMAC signature verification for security.

Webhook Configuration

Merchants must configure the following in their merchant settings:

  • WebHook URL: The endpoint to receive webhook notifications
  • Secret: A secret key used to generate HMAC signatures for request verification

Security

Signature Verification

All webhook requests include a signature header containing an HMAC hash computed using:

  • Algorithm: HMAC with the merchant's secret key
  • Data: The JSON payload (serialized with camelCase property names)
  • Header: signature: <computed_hash>

Verification Process:

string computedHash = WebhookSecretHashUtil.ComputeHash(requestBody, merchantSecret);
if (computedHash != request.Headers["signature"]) {
    // Invalid signature - reject request
}

Webhook Event Types

1. VOUCHER_GENERATED

Trigger: Sent when bulk vouchers have been successfully generated.

Payload Structure:

{
  "type": "VOUCHER_GENERATED",
  "data": {
    "reference": "string",
    "transactionId": "string",
    "status": "string",
    "expiryDate": "2024-01-15T10:30:00Z",
    "value": 100.00,
    "vouchers": [
      {
        "serialNumber": 12345,
        "code": "ABCD-EFGH-IJKL",
        "pin": "1234"
      }
    ]
  }
}

Data Fields:

FieldTypeDescription
referencestringYour unique reference used when creating the vouchers
transactionIdstringSuregifts-generated unique request ID
statusstringCurrent status: PROCESSING, CANCELED, FAILED, COMPLETED
expiryDatedatetimeVoucher expiry date (nullable)
valuedecimalVoucher value (nullable if not set)
vouchersarrayList of generated vouchers (only present when status is COMPLETED)

Voucher Object:

FieldTypeDescription
serialNumberlongUnique voucher serial number
codestringVoucher code
pinstringVoucher PIN (decrypted)

Sample Payload:

{
  "type": "VOUCHER_GENERATED",
  "data": {
    "reference": "MERCHANT_REF_001",
    "transactionId": "789456",
    "status": "COMPLETED",
    "expiryDate": "2024-12-31T23:59:59Z",
    "value": 50.00,
    "vouchers": [
      {
        "serialNumber": 100001,
        "code": "SURE-GIFT-2024-ABCD",
        "pin": "9876"
      },
      {
        "serialNumber": 100002,
        "code": "SURE-GIFT-2024-EFGH",
        "pin": "5432"
      }
    ]
  }
}

2. VOUCHER_ACTIVATED

Trigger: Sent when a voucher has been successfully activated.

Payload Structure:

{
  "type": "VOUCHER_ACTIVATED",
  "data": {
    "transactionId": "string",
    "voucherCode": "string",
    "serialNumber": "string",
    "transactionType": "ACTIVATION",
    "amount": 100.00,
    "timestamp": "2024-01-15T10:30:00Z",
    "reference": "string",
    "refundStatus": null
  }
}

Data Fields:

FieldTypeDescription
transactionIdstringUnique transaction ID generated by Suregifts
voucherCodestringMasked voucher code used in the transaction
serialNumberstringVoucher serial number
transactionTypestringAlways ACTIVATION for this event
amountdecimalTransaction amount
timestampdatetimeTransaction date and time
referencestringYour unique reference used when activating the voucher
refundStatusstringRefund status (null for activation)

Sample Payload:

{
  "type": "VOUCHER_ACTIVATED",
  "data": {
    "transactionId": "TXN_ACT_123456",
    "voucherCode": "SURE-****-****-ABCD",
    "serialNumber": "100001",
    "transactionType": "ACTIVATION",
    "amount": 50.00,
    "timestamp": "2024-01-15T14:25:30Z",
    "reference": "MERCHANT_ACT_REF_001",
    "refundStatus": null
  }
}

3. VOUCHER_REDEEMED

Trigger: Sent when a voucher has been successfully redeemed.

Payload Structure:

{
  "type": "VOUCHER_REDEEMED",
  "data": {
    "transactionId": "string",
    "voucherCode": "string",
    "serialNumber": "string",
    "transactionType": "REDEMPTION",
    "amount": 100.00,
    "timestamp": "2024-01-15T10:30:00Z",
    "reference": "string",
    "refundStatus": null
  }
}

Data Fields:

FieldTypeDescription
transactionIdstringUnique transaction ID generated by Suregifts
voucherCodestringMasked voucher code used in the transaction
serialNumberstringVoucher serial number
transactionTypestringAlways REDEMPTION for this event
amountdecimalTransaction amount
timestampdatetimeTransaction date and time
referencestringYour unique reference used when redeeming the voucher
refundStatusstringRefund status (null for normal redemption)

Sample Payload:

{
  "type": "VOUCHER_REDEEMED",
  "data": {
    "transactionId": "TXN_RED_789012",
    "voucherCode": "SURE-****-****-EFGH",
    "serialNumber": "100002",
    "transactionType": "REDEMPTION",
    "amount": 25.50,
    "timestamp": "2024-01-16T09:15:45Z",
    "reference": "MERCHANT_RED_REF_002",
    "refundStatus": null
  }
}

4. TRANSACTION_REVERSED

Trigger: Sent when a transaction has been reversed (refunded).

Payload Structure:

{
  "type": "TRANSACTION_REVERSED",
  "data": {
    "transactionId": "string",
    "voucherCode": "string",
    "serialNumber": "string",
    "transactionType": "ACTIVATION | REDEMPTION",
    "amount": 0.00,
    "timestamp": "2024-01-15T10:30:00Z",
    "reference": "string",
    "refundStatus": "REVERSED"
  }
}

Data Fields:

FieldTypeDescription
transactionIdstringUnique transaction ID that was reversed
voucherCodestringMasked voucher code
serialNumberstringVoucher serial number
transactionTypestringOriginal transaction type: ACTIVATION or REDEMPTION
amountdecimalTransaction amount that was reversed
timestampdatetimeOriginal transaction date and time
referencestringOriginal transaction reference
refundStatusstringAlways REVERSED for this event

Sample Payload:

{
  "type": "TRANSACTION_REVERSED",
  "data": {
    "transactionId": "TXN_RED_789012",
    "voucherCode": "SURE-****-****-EFGH",
    "serialNumber": "100002",
    "transactionType": "REDEMPTION",
    "amount": 0.0,
    "timestamp": "2024-01-16T09:15:45Z",
    "reference": "MERCHANT_RED_REF_002",
    "refundStatus": "REVERSED"
  }
}

5. TRANSACTION_UPDATED

Trigger: Sent when a transaction has been updated (partial refund or modification).

Payload Structure:

{
  "type": "TRANSACTION_UPDATED",
  "data": {
    "transactionId": "string",
    "voucherCode": "string",
    "serialNumber": "string",
    "transactionType": "ACTIVATION | REDEMPTION",
    "amount": 100.00,
    "timestamp": "2024-01-15T10:30:00Z",
    "reference": "string",
    "refundStatus": "UPDATED"
  }
}

Data Fields:

FieldTypeDescription
transactionIdstringUnique transaction ID that was updated
voucherCodestringMasked voucher code
serialNumberstringVoucher serial number
transactionTypestringTransaction type: ACTIVATION or REDEMPTION
amountdecimalUpdated transaction amount
timestampdatetimeOriginal transaction date and time
referencestringOriginal transaction reference
refundStatusstringAlways UPDATED for this event

Sample Payload:

{
  "type": "TRANSACTION_UPDATED",
  "data": {
    "transactionId": "TXN_ACT_123456",
    "voucherCode": "SURE-****-****-ABCD",
    "serialNumber": "100001",
    "transactionType": "REDEMPTION",
    "amount": 45.00,
    "timestamp": "2024-01-15T14:25:30Z",
    "reference": "MERCHANT_ACT_REF_001",
    "refundStatus": "UPDATED"
  }
}

Implementation Details

Webhook Delivery

HTTP Method: POST
Content-Type: application/json
Headers:

  • signature: HMAC hash of the request body
  • Content-Type: application/json

Error Handling

  • If webhook URL or secret is not configured, the notification is silently skipped
  • Failed webhook deliveries are logged but do not retry automatically
  • HTTP status codes and errors are logged for debugging


Testing Webhooks

Live Example for Testing

Use this example to test your signature verification implementation:

Sample Payload (JSON):

{"type":"VOUCHER_REDEEMED","data":{"transactionId":"TXN_RED_789012","voucherCode":"SURE-****-****-EFGH","serialNumber":"100002","transactionType":"REDEMPTION","amount":25.50,"timestamp":"2024-01-16T09:15:45Z","reference":"MERCHANT_RED_REF_002","refundStatus":null}}

Secret Key:

my_super_secret_webhook_key_12345

Expected Signature (HMAC-SHA512):

ad80b356f612e38959c34249d724da812bae0404bff12377165e19687ee120461f3347bbc569bf41a47ac5cc6baa8e8ace099e904ef713f7d7c3d199f8c2fc2a

Note: The signature is computed using HMAC-SHA512 algorithm with the secret key and the exact JSON payload (no whitespace formatting).

Test Endpoint Setup

  1. Configure your webhook URL in merchant settings
  2. Set a secure secret key
  3. Implement signature verification on your endpoint
  4. Test with the example above to verify your implementation

Signature Verification Examples

using System.Security.Cryptography;
using System.Text;
using Microsoft.AspNetCore.Mvc;
using Newtonsoft.Json;

public class WebhookController : ControllerBase
{
    [HttpPost("webhook")]
    public async Task<IActionResult> ReceiveWebhook()
    {
        using var reader = new StreamReader(Request.Body);
        var body = await reader.ReadToEndAsync();
        
        var signature = Request.Headers["signature"].FirstOrDefault();
        var secret = "my_super_secret_webhook_key_12345"; // Retrieve from config
        
        var computedHash = ComputeHmacSha512(body, secret);
        
        if (signature != computedHash)
        {
            return Unauthorized("Invalid signature");
        }
        
        var webhook = JsonConvert.DeserializeObject<dynamic>(body);
        string eventType = webhook.type;
        
        // Process webhook based on type
        switch (eventType)
        {
            case "VOUCHER_GENERATED":
                // Handle voucher generation
                break;
            case "VOUCHER_ACTIVATED":
                // Handle activation
                break;
            case "VOUCHER_REDEEMED":
                // Handle redemption
                break;
            case "TRANSACTION_REVERSED":
                // Handle reversal
                break;
            case "TRANSACTION_UPDATED":
                // Handle update
                break;
        }
        
        return Ok();
    }
    
    private string ComputeHmacSha512(string data, string secret)
    {
        byte[] secretKeyBytes = Encoding.UTF8.GetBytes(secret);
        using (HMACSHA512 hmac = new HMACSHA512(secretKeyBytes))
        {
            byte[] dataBytes = Encoding.UTF8.GetBytes(data);
            byte[] hashBytes = hmac.ComputeHash(dataBytes);
            
            StringBuilder hexStringBuilder = new StringBuilder();
            foreach (byte b in hashBytes)
            {
                hexStringBuilder.Append(b.ToString("x2"));
            }
            return hexStringBuilder.ToString();
        }
    }
}
const crypto = require('crypto');
const express = require('express');
const app = express();

app.use(express.json());

app.post('/webhook', (req, res) => {
    const signature = req.headers['signature'];
    const secret = 'my_super_secret_webhook_key_12345'; // Retrieve from config
    const body = JSON.stringify(req.body);
    
    // Compute HMAC-SHA512
    const computedHash = crypto
        .createHmac('sha512', secret)
        .update(body)
        .digest('hex');
    
    // Verify signature
    if (signature !== computedHash) {
        return res.status(401).json({ error: 'Invalid signature' });
    }
    
    const webhook = req.body;
    const eventType = webhook.type;
    
    // Process webhook based on type
    switch (eventType) {
        case 'VOUCHER_GENERATED':
            // Handle voucher generation
            console.log('Vouchers generated:', webhook.data);
            break;
        case 'VOUCHER_ACTIVATED':
            // Handle activation
            console.log('Voucher activated:', webhook.data);
            break;
        case 'VOUCHER_REDEEMED':
            // Handle redemption
            console.log('Voucher redeemed:', webhook.data);
            break;
        case 'TRANSACTION_REVERSED':
            // Handle reversal
            console.log('Transaction reversed:', webhook.data);
            break;
        case 'TRANSACTION_UPDATED':
            // Handle update
            console.log('Transaction updated:', webhook.data);
            break;
    }
    
    res.status(200).json({ success: true });
});

app.listen(3000, () => {
    console.log('Webhook server listening on port 3000');
});
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;

import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;

@RestController
@RequestMapping("/webhook")
public class WebhookController {
    
    private static final String SECRET = "my_super_secret_webhook_key_12345";
    private final ObjectMapper objectMapper = new ObjectMapper();
    
    @PostMapping
    public ResponseEntity<?> receiveWebhook(
            @RequestBody String body,
            @RequestHeader("signature") String signature) {
        
        try {
            // Compute HMAC-SHA512
            String computedHash = computeHmacSha512(body, SECRET);
            
            // Verify signature
            if (!signature.equals(computedHash)) {
                return ResponseEntity.status(HttpStatus.UNAUTHORIZED)
                    .body("Invalid signature");
            }
            
            // Parse webhook payload
            JsonNode webhook = objectMapper.readTree(body);
            String eventType = webhook.get("type").asText();
            JsonNode data = webhook.get("data");
            
            // Process webhook based on type
            switch (eventType) {
                case "VOUCHER_GENERATED":
                    // Handle voucher generation
                    System.out.println("Vouchers generated: " + data);
                    break;
                case "VOUCHER_ACTIVATED":
                    // Handle activation
                    System.out.println("Voucher activated: " + data);
                    break;
                case "VOUCHER_REDEEMED":
                    // Handle redemption
                    System.out.println("Voucher redeemed: " + data);
                    break;
                case "TRANSACTION_REVERSED":
                    // Handle reversal
                    System.out.println("Transaction reversed: " + data);
                    break;
                case "TRANSACTION_UPDATED":
                    // Handle update
                    System.out.println("Transaction updated: " + data);
                    break;
            }
            
            return ResponseEntity.ok().body("{\"success\": true}");
            
        } catch (Exception e) {
            return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR)
                .body("Error processing webhook: " + e.getMessage());
        }
    }
    
    private String computeHmacSha512(String data, String secret) 
            throws NoSuchAlgorithmException, InvalidKeyException {
        
        Mac hmacSha512 = Mac.getInstance("HmacSHA512");
        SecretKeySpec secretKeySpec = new SecretKeySpec(
            secret.getBytes(StandardCharsets.UTF_8), 
            "HmacSHA512"
        );
        hmacSha512.init(secretKeySpec);
        
        byte[] hashBytes = hmacSha512.doFinal(
            data.getBytes(StandardCharsets.UTF_8)
        );
        
        // Convert to hex string
        StringBuilder hexString = new StringBuilder();
        for (byte b : hashBytes) {
            String hex = Integer.toHexString(0xff & b);
            if (hex.length() == 1) {
                hexString.append('0');
            }
            hexString.append(hex);
        }
        
        return hexString.toString();
    }
}
package main

import (
	"crypto/hmac"
	"crypto/sha512"
	"encoding/hex"
	"encoding/json"
	"fmt"
	"io/ioutil"
	"log"
	"net/http"
)

const SECRET = "my_super_secret_webhook_key_12345"

type WebhookPayload struct {
	Type string          `json:"type"`
	Data json.RawMessage `json:"data"`
}

func computeHmacSha512(data, secret string) string {
	h := hmac.New(sha512.New, []byte(secret))
	h.Write([]byte(data))
	return hex.EncodeToString(h.Sum(nil))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
	if r.Method != http.MethodPost {
		http.Error(w, "Method not allowed", http.StatusMethodNotAllowed)
		return
	}
	
	// Read request body
	body, err := ioutil.ReadAll(r.Body)
	if err != nil {
		http.Error(w, "Error reading request body", http.StatusBadRequest)
		return
	}
	defer r.Body.Close()
	
	// Get signature from header
	signature := r.Header.Get("signature")
	if signature == "" {
		http.Error(w, "Missing signature header", http.StatusUnauthorized)
		return
	}
	
	// Compute HMAC-SHA512
	computedHash := computeHmacSha512(string(body), SECRET)
	
	// Verify signature
	if signature != computedHash {
		http.Error(w, "Invalid signature", http.StatusUnauthorized)
		return
	}
	
	// Parse webhook payload
	var webhook WebhookPayload
	if err := json.Unmarshal(body, &webhook); err != nil {
		http.Error(w, "Error parsing JSON", http.StatusBadRequest)
		return
	}
	
	// Process webhook based on type
	switch webhook.Type {
	case "VOUCHER_GENERATED":
		// Handle voucher generation
		log.Printf("Vouchers generated: %s", webhook.Data)
	case "VOUCHER_ACTIVATED":
		// Handle activation
		log.Printf("Voucher activated: %s", webhook.Data)
	case "VOUCHER_REDEEMED":
		// Handle redemption
		log.Printf("Voucher redeemed: %s", webhook.Data)
	case "TRANSACTION_REVERSED":
		// Handle reversal
		log.Printf("Transaction reversed: %s", webhook.Data)
	case "TRANSACTION_UPDATED":
		// Handle update
		log.Printf("Transaction updated: %s", webhook.Data)
	default:
		log.Printf("Unknown webhook type: %s", webhook.Type)
	}
	
	// Send success response
	w.Header().Set("Content-Type", "application/json")
	w.WriteHeader(http.StatusOK)
	fmt.Fprintf(w, `{"success": true}`)
}

func main() {
	http.HandleFunc("/webhook", webhookHandler)
	
	log.Println("Webhook server listening on port 8080")
	if err := http.ListenAndServe(":8080", nil); err != nil {
		log.Fatal(err)
	}
}

Best Practices

  1. Idempotency: Use the transactionId or reference to prevent duplicate processing
  2. Signature Verification: Always verify the signature before processing
  3. Quick Response: Return HTTP 200 quickly and process asynchronously
  4. Error Handling: Log all webhook failures for manual review
  5. Retry Logic: Implement your own retry mechanism if needed
  6. Monitoring: Monitor webhook delivery success rates

Support

For webhook configuration issues or questions, contact Suregifts technical support.