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:
| Field | Type | Description |
|---|---|---|
reference | string | Your unique reference used when creating the vouchers |
transactionId | string | Suregifts-generated unique request ID |
status | string | Current status: PROCESSING, CANCELED, FAILED, COMPLETED |
expiryDate | datetime | Voucher expiry date (nullable) |
value | decimal | Voucher value (nullable if not set) |
vouchers | array | List of generated vouchers (only present when status is COMPLETED) |
Voucher Object:
| Field | Type | Description |
|---|---|---|
serialNumber | long | Unique voucher serial number |
code | string | Voucher code |
pin | string | Voucher 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:
| Field | Type | Description |
|---|---|---|
transactionId | string | Unique transaction ID generated by Suregifts |
voucherCode | string | Masked voucher code used in the transaction |
serialNumber | string | Voucher serial number |
transactionType | string | Always ACTIVATION for this event |
amount | decimal | Transaction amount |
timestamp | datetime | Transaction date and time |
reference | string | Your unique reference used when activating the voucher |
refundStatus | string | Refund 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:
| Field | Type | Description |
|---|---|---|
transactionId | string | Unique transaction ID generated by Suregifts |
voucherCode | string | Masked voucher code used in the transaction |
serialNumber | string | Voucher serial number |
transactionType | string | Always REDEMPTION for this event |
amount | decimal | Transaction amount |
timestamp | datetime | Transaction date and time |
reference | string | Your unique reference used when redeeming the voucher |
refundStatus | string | Refund 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:
| Field | Type | Description |
|---|---|---|
transactionId | string | Unique transaction ID that was reversed |
voucherCode | string | Masked voucher code |
serialNumber | string | Voucher serial number |
transactionType | string | Original transaction type: ACTIVATION or REDEMPTION |
amount | decimal | Transaction amount that was reversed |
timestamp | datetime | Original transaction date and time |
reference | string | Original transaction reference |
refundStatus | string | Always 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:
| Field | Type | Description |
|---|---|---|
transactionId | string | Unique transaction ID that was updated |
voucherCode | string | Masked voucher code |
serialNumber | string | Voucher serial number |
transactionType | string | Transaction type: ACTIVATION or REDEMPTION |
amount | decimal | Updated transaction amount |
timestamp | datetime | Original transaction date and time |
reference | string | Original transaction reference |
refundStatus | string | Always 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 bodyContent-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
- Configure your webhook URL in merchant settings
- Set a secure secret key
- Implement signature verification on your endpoint
- 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
- Idempotency: Use the
transactionIdorreferenceto prevent duplicate processing - Signature Verification: Always verify the signature before processing
- Quick Response: Return HTTP 200 quickly and process asynchronously
- Error Handling: Log all webhook failures for manual review
- Retry Logic: Implement your own retry mechanism if needed
- Monitoring: Monitor webhook delivery success rates
Support
For webhook configuration issues or questions, contact Suregifts technical support.