Overview
SureGifts webhooks allow you to receive real-time notifications about events in your business account. All webhooks are authenticated using HMAC-SHA-512 signatures to ensure security and data integrity.
Key Features:
- Multiple webhook URLs per business account
- Event-specific subscriptions (choose which events to receive)
- Automatic retries with exponential backoff
- Secure HMAC-SHA-512 signature verification
Getting Started
Step 1: Access Developer Tools
- Log in to your SureGifts Business Dashboard
- Navigate to Developer in the left sidebar menu
- Click on the Webhooks tab
Step 2: Configure Your Webhook
- Click the + Add button to create a new webhook
- Enter your webhook URL (must be HTTPS)
- Select the events you want to receive:
ORDER_UPDATED- Receive notifications when orders are completed, cancelled, or failedVOUCHER_UPDATED- Receive notifications when voucher balances change
- Click Save
Step 3: Retrieve Your Webhook Secret
After creating your webhook:
- Your webhook secret will be displayed in the webhook card
- Click the Refresh icon (🔄) next to the secret to copy it
- Store this secret securely - you'll need it to verify webhook signatures
Important: Each webhook has its own unique secret. If you have multiple webhooks, each will have a different secret for signature verification.
Step 4: Get Your API Credentials
In the Developer tools section, you'll also find:
- Public Key: Your API public key (labeled "Suregifts")
- Secret Key: Click Generate to create or refresh your API secret key
These credentials are used for making API calls, separate from webhook authentication.
Webhook Event Types
SureGifts sends webhooks for two main event types:
1. ORDER_UPDATED
Trigger: Sent when an order reaches a terminal status (completed, partially completed, cancelled, or failed).
Endpoint: Your configured webhook URL
Required Header: signature
Payload Structure:
{
"type": "ORDER_UPDATED",
"data": {
"orderNumber": "169761",
"reference": "598",
"status": "COMPLETED",
"vouchers": [
{
"value": 500.00,
"expiryDate": "2025-12-15T08:54:42.1194851",
"pin": "0000",
"code": "729936755277",
"serial": 100001,
"status": null,
"firstName": null,
"lastName": null,
"email": null
}
]
}
}
Order Status Values:
COMPLETED- Order fully deliveredPARTIALLY_COMPLETED- Order partially deliveredCANCELED- Order cancelledFAILED- Order failed
Order Types:
SureGifts supports two order types, which affect the voucher data structure:
-
Download Orders: Vouchers are immediately available for download
- Includes:
pin,code,expiryDate - Excludes:
status,firstName,lastName,email(all null)
- Includes:
-
Delivery Orders: Vouchers are delivered to recipients via email/SMS
- Includes:
status,firstName,lastName,email - Excludes:
pin,code(null for security),expiryDate(null)
- Includes:
Data Fields:
| Field | Type | Description | Always Present |
|---|---|---|---|
orderNumber | string | SureGifts order reference number | Yes |
reference | string | Your reference used when creating the order | Yes |
status | string | Order status: COMPLETED, PARTIALLY_COMPLETED, CANCELED, FAILED | Yes |
vouchers | array | Array of voucher details (only for COMPLETED and PARTIALLY_COMPLETED) | Conditional |
Voucher Object Fields:
| Field | Type | Description |
|---|---|---|
value | decimal | Initial voucher value |
expiryDate | datetime | Voucher expiry date (nullable, auto-extends by 1 year) |
pin | string | Voucher redemption PIN (only for download orders) |
code | string | Voucher code (only for download orders) |
serial | long | Voucher serial number |
status | string | Item delivery status: DELIVERED, FAILED, CANCELED, PROCESSING (null for download orders) |
firstName | string | Recipient first name (null for download orders) |
lastName | string | Recipient last name (null for download orders) |
email | string | Recipient email address (null for download orders) |
Example Payloads by Status:
Completed Download Order:
{
"type": "ORDER_UPDATED",
"data": {
"orderNumber": "169761",
"reference": "598",
"status": "COMPLETED",
"vouchers": [
{
"value": 500.00,
"expiryDate": "2025-12-15T08:54:42.1194851",
"pin": "0000",
"code": "729936755277",
"serial": 100001,
"status": null,
"firstName": null,
"lastName": null,
"email": null
}
]
}
}
Completed Delivery Order:
{
"type": "ORDER_UPDATED",
"data": {
"orderNumber": "169762",
"reference": "599",
"status": "COMPLETED",
"vouchers": [
{
"value": 500.00,
"expiryDate": null,
"pin": null,
"code": null,
"serial": 100002,
"status": "DELIVERED",
"firstName": "John",
"lastName": "Doe",
"email": "[email protected]"
}
]
}
}
Partially Completed Delivery Order (mixed statuses):
{
"type": "ORDER_UPDATED",
"data": {
"orderNumber": "169763",
"reference": "600",
"status": "PARTIALLY_COMPLETED",
"vouchers": [
{
"value": 500.00,
"expiryDate": null,
"pin": null,
"code": null,
"serial": 100003,
"status": "DELIVERED",
"firstName": "Jane",
"lastName": "Smith",
"email": "[email protected]"
},
{
"value": 500.00,
"expiryDate": null,
"pin": null,
"code": null,
"serial": 100004,
"status": "FAILED",
"firstName": "Bob",
"lastName": "Johnson",
"email": "[email protected]"
}
]
}
}
Cancelled Order (no vouchers):
{
"type": "ORDER_UPDATED",
"data": {
"orderNumber": "169764",
"reference": "601",
"status": "CANCELED"
}
}
Failed Order (no vouchers):
{
"type": "ORDER_UPDATED",
"data": {
"orderNumber": "169765",
"reference": "602",
"status": "FAILED"
}
}
2. VOUCHER_UPDATED
Trigger: Sent when a voucher's balance changes due to redemption, activation, reversal, merge, or split operations.
Endpoint: Your configured webhook URL
Required Header: signature
Payload Structure:
{
"type": "VOUCHER_UPDATED",
"data": {
"orderNumber": "169761",
"voucherCode": "SURE-****-****-ABCD",
"serialNumber": 100001,
"balance": 450.00,
"email": "[email protected]"
}
}
Data Fields:
| Field | Type | Description |
|---|---|---|
orderNumber | string | Order number associated with the voucher |
voucherCode | string | Masked voucher code |
serialNumber | long | Voucher serial number |
balance | decimal | Current voucher balance after the transaction |
email | string | Email address of the voucher owner |
Webhook Security & Verification
Authentication Method
All webhooks are signed with HMAC-SHA-512 using your webhook secret. You MUST verify this signature before processing any webhook event.
Signature Details:
- Algorithm:
HMAC-SHA512 - Key: Your webhook secret (from the Developer dashboard)
- Message: Raw request body bytes (exactly as received)
- Output: Lowercase hexadecimal string
- Header Name:
signature(same for all webhook events)
Verification Steps
- Read the raw request body exactly as received (do not parse or modify)
- Compute the signature:
signature = lowercase_hex(HMAC_SHA512(key=webhookSecret, message=rawBodyBytes)) - Extract the signature from the
signatureheader - Compare signatures using constant-time comparison
- Reject if mismatch - respond with
401 Unauthorizedand do not process the event
Critical: Do not parse and re-stringify JSON before hashing. Whitespace, key order, and float formatting changes will break the signature verification.
Implementation Guide
Step-by-Step: Consuming Webhooks
1. Create Your Webhook Endpoint
Your webhook endpoint must:
- Accept POST requests
- Use HTTPS (required for production)
- Return responses quickly (< 2 seconds recommended)
- Verify the signature before processing
Example Endpoint Structure:
POST https://your-domain.com/webhooks/suregifts
2. Extract the Signature Header
Based on the event type, extract the appropriate header:
// Extract signature header (same for all events)
string signature = Request.Headers["signature"];
3. Read the Raw Request Body
Critical: You must read the raw body bytes exactly as received, without any modifications.
ASP.NET Core Example:
[HttpPost("webhooks/suregifts")]
public async Task<IActionResult> HandleWebhook()
{
// Enable buffering to allow multiple reads
Request.EnableBuffering();
// Read raw body
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
string rawBody = await reader.ReadToEndAsync();
// Reset stream position for later use
Request.Body.Position = 0;
// Extract signature header
string receivedSignature = Request.Headers["signature"];
// Verify signature
if (!VerifySignature(rawBody, receivedSignature, webhookSecret))
{
return Unauthorized(new { error = "Invalid signature" });
}
// Parse and process the webhook
var webhook = JsonConvert.DeserializeObject<WebhookPayload>(rawBody);
await ProcessWebhook(webhook);
return Ok();
}
const express = require('express');
const bodyParser = require('body-parser');
// Store raw body for signature verification
app.use(bodyParser.json({
verify: (req, res, buf) => {
req.rawBody = buf.toString('utf8');
}
}));
app.post('/webhooks/suregifts', async (req, res) => {
const receivedSignature = req.headers['signature'];
const webhookSecret = process.env.WEBHOOK_SECRET;
// Verify signature
if (!verifySignature(req.rawBody, receivedSignature, webhookSecret)) {
return res.status(401).json({ error: 'Invalid signature' });
}
// Process the webhook
const webhook = req.body;
await processWebhook(webhook);
res.status(200).json({ message: 'OK' });
});
4. Compute and Verify the Signature
C# Implementation:
using System.Security.Cryptography;
using System.Text;
public class WebhookSecretHashUtil
{
private static string ToHexString(byte[] bytes)
{
StringBuilder hexStringBuilder = new StringBuilder();
foreach (byte b in bytes)
{
hexStringBuilder.Append(b.ToString("x2"));
}
return hexStringBuilder.ToString();
}
public static string ComputeHash(string requestBody, string secret)
{
byte[] secretKeyBytes = Encoding.UTF8.GetBytes(secret);
using (HMACSHA512 hmac = new HMACSHA512(secretKeyBytes))
{
byte[] dataBytes = Encoding.UTF8.GetBytes(requestBody);
byte[] hashBytes = hmac.ComputeHash(dataBytes);
string transactionHash = ToHexString(hashBytes);
return transactionHash;
}
}
public static bool VerifySignature(string requestBody, string receivedSignature, string secret)
{
if (string.IsNullOrEmpty(receivedSignature))
return false;
string computedSignature = ComputeHash(requestBody, secret);
// Use constant-time comparison to prevent timing attacks
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(computedSignature),
Encoding.UTF8.GetBytes(receivedSignature)
);
}
}
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.nio.charset.StandardCharsets;
import java.security.InvalidKeyException;
import java.security.NoSuchAlgorithmException;
import java.security.MessageDigest;
public class WebhookSecretHashUtil {
private static String toHexString(byte[] bytes) {
StringBuilder hexStringBuilder = new StringBuilder();
for (byte b : bytes) {
hexStringBuilder.append(String.format("%02x", b));
}
return hexStringBuilder.toString();
}
public static String computeHash(String requestBody, String secret)
throws NoSuchAlgorithmException, InvalidKeyException {
byte[] secretKeyBytes = secret.getBytes(StandardCharsets.UTF_8);
SecretKeySpec secretKeySpec = new SecretKeySpec(secretKeyBytes, "HmacSHA512");
Mac hmac = Mac.getInstance("HmacSHA512");
hmac.init(secretKeySpec);
byte[] dataBytes = requestBody.getBytes(StandardCharsets.UTF_8);
byte[] hashBytes = hmac.doFinal(dataBytes);
return toHexString(hashBytes);
}
public static boolean verifySignature(String requestBody, String receivedSignature, String secret)
throws NoSuchAlgorithmException, InvalidKeyException {
if (receivedSignature == null || receivedSignature.isEmpty())
return false;
String computedSignature = computeHash(requestBody, secret);
return MessageDigest.isEqual(
computedSignature.getBytes(StandardCharsets.UTF_8),
receivedSignature.getBytes(StandardCharsets.UTF_8)
);
}
}
const crypto = require('crypto');
class WebhookSecretHashUtil {
static toHexString(bytes) {
return bytes.reduce((str, byte) => str + byte.toString(16).padStart(2, '0'), '');
}
static computeHash(requestBody, secret) {
const secretKeyBytes = Buffer.from(secret, 'utf8');
const hmac = crypto.createHmac('sha512', secretKeyBytes);
const dataBytes = Buffer.from(requestBody, 'utf8');
const hashBytes = hmac.update(dataBytes).digest();
return WebhookSecretHashUtil.toHexString(hashBytes);
}
static verifySignature(requestBody, receivedSignature, secret) {
if (!receivedSignature) return false;
const computedSignature = this.computeHash(requestBody, secret);
// Use constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(computedSignature, 'utf8'),
Buffer.from(receivedSignature, 'utf8')
);
}
}
module.exports = WebhookSecretHashUtil;
5. Process the Webhook Event
After verifying the signature, parse and process the webhook based on the event type:
public async Task ProcessWebhook(WebhookPayload webhook)
{
switch (webhook.Type)
{
case "ORDER_UPDATED":
await HandleOrderUpdated(webhook.Data);
break;
case "VOUCHER_UPDATED":
await HandleVoucherUpdated(webhook.Data);
break;
default:
_logger.LogWarning("Unknown webhook type: {Type}", webhook.Type);
break;
}
}
private async Task HandleOrderUpdated(OrderData order)
{
// Check order status
switch (order.Status)
{
case "COMPLETED":
case "PARTIALLY_COMPLETED":
// Process successful order
await UpdateOrderStatus(order.Reference, order.Status);
// Determine order type and process accordingly
if (order.Vouchers != null && order.Vouchers.Any())
{
var firstVoucher = order.Vouchers.First();
if (firstVoucher.Pin != null && firstVoucher.Code != null)
{
// Download order - vouchers ready for immediate use
await StoreDownloadVouchers(order.Vouchers);
await NotifyCustomerWithVoucherDetails(order.Reference, order.Vouchers);
}
else
{
// Delivery order - check individual voucher statuses
// For PARTIALLY_COMPLETED orders, some vouchers may have failed
var deliveredVouchers = order.Vouchers.Where(v => v.Status == "DELIVERED").ToList();
var failedVouchers = order.Vouchers.Where(v => v.Status == "FAILED").ToList();
if (deliveredVouchers.Any())
{
await StoreDeliveryVouchers(deliveredVouchers);
await NotifyCustomerOfDelivery(order.Reference, deliveredVouchers);
}
if (failedVouchers.Any())
{
await LogFailedDeliveries(order.Reference, failedVouchers);
await NotifyCustomerOfFailures(order.Reference, failedVouchers);
}
}
}
break;
case "CANCELED":
case "FAILED":
// Handle failed order
await UpdateOrderStatus(order.Reference, order.Status);
await RefundCustomer(order.Reference);
break;
}
}
private async Task HandleVoucherUpdated(VoucherBalanceData data)
{
// Update voucher balance in your system
await UpdateVoucherBalance(data.SerialNumber, data.Balance);
// Notify customer of balance change
await NotifyBalanceChange(data.Email, data.VoucherCode, data.Balance);
}
6. Implement Idempotency
To handle duplicate webhook deliveries, implement idempotency using a unique key:
public async Task<IActionResult> HandleWebhook()
{
// ... signature verification ...
var webhook = JsonConvert.DeserializeObject<WebhookPayload>(rawBody);
// Create idempotency key
string idempotencyKey = webhook.Type switch
{
"ORDER_UPDATED" => $"order_{webhook.Data.OrderNumber}_{webhook.Data.Status}",
"VOUCHER_UPDATED" => $"voucher_{webhook.Data.SerialNumber}_{webhook.Data.Balance}",
_ => Guid.NewGuid().ToString()
};
// Check if already processed
if (await _cache.ExistsAsync(idempotencyKey))
{
_logger.LogInformation("Webhook already processed: {Key}", idempotencyKey);
return Ok(new { message = "Already processed" });
}
// Process webhook
await ProcessWebhook(webhook);
// Mark as processed (cache for 24 hours)
await _cache.SetAsync(idempotencyKey, "processed", TimeSpan.FromHours(24));
return Ok();
}
Best Practices
Security
- Always verify signatures - Never process webhooks without signature verification
- Use HTTPS only - Webhooks will only be sent to HTTPS endpoints in production
- Store secrets securely - Use environment variables or secret management services (Azure Key Vault, AWS Secrets Manager, etc.)
- Use constant-time comparison - Prevents timing attacks when comparing signatures
- Validate webhook origin - Only accept webhooks from known SureGifts IP ranges (if applicable)
Reliability
- Respond quickly - Return
200 OKwithin 2 seconds to avoid retries - Process asynchronously - Queue webhook processing for long-running operations
- Implement idempotency - Use unique keys to prevent duplicate processing
- Handle retries gracefully - SureGifts will retry failed webhooks with exponential backoff
- Log webhook events - Keep audit logs for debugging and compliance
Data Handling
- Redact sensitive data in logs - Never log voucher PINs, codes, or webhook secrets
- Validate data structure - Check for required fields before processing
- Handle missing fields - Some fields (like
vouchers) are conditional - Store webhook payloads - Keep raw payloads for audit and debugging purposes
Error Handling
[HttpPost("webhooks/suregifts")]
public async Task<IActionResult> HandleWebhook()
{
try
{
// Signature verification
Request.EnableBuffering();
using var reader = new StreamReader(Request.Body, Encoding.UTF8, leaveOpen: true);
string rawBody = await reader.ReadToEndAsync();
Request.Body.Position = 0;
string signature = Request.Headers["signature"];
if (!WebhookSecretHashUtil.VerifySignature(rawBody, signature, _webhookSecret))
{
_logger.LogWarning("Invalid webhook signature");
return Unauthorized(new { error = "Invalid signature" });
}
// Parse webhook
var webhook = JsonConvert.DeserializeObject<WebhookPayload>(rawBody);
// Queue for async processing
await _queue.EnqueueAsync(webhook);
return Ok(new { message = "Webhook received" });
}
catch (JsonException ex)
{
_logger.LogError(ex, "Invalid JSON in webhook payload");
return BadRequest(new { error = "Invalid JSON" });
}
catch (Exception ex)
{
_logger.LogError(ex, "Error processing webhook");
return StatusCode(500, new { error = "Internal server error" });
}
}
Webhook Retry Policy
SureGifts automatically retries failed webhook deliveries:
- Retry Schedule: Exponential backoff (3 minutes, 6 minutes, 12 minutes, etc.)
- Maximum Attempts: 5 attempts
- Retry Conditions: HTTP status codes 5xx, timeouts, connection errors
- Success Criteria: HTTP status code 200-299
Your endpoint should:
- Return
200 OKfor successfully processed webhooks - Return
401 Unauthorizedfor signature verification failures (no retry) - Return
400 Bad Requestfor invalid data (no retry) - Return
500 Internal Server Errorfor temporary failures (will retry)
Common Pitfalls
❌ Hash Mismatch Issues
Problem: Signature verification fails even with correct secret
Causes:
- Parsing JSON and re-stringifying before hashing
- Modifying whitespace, indentation, or key order
- Using wrong encoding (not UTF-8)
- Adding BOM (Byte Order Mark) to the body
Solution: Always hash the raw body bytes exactly as received
❌ Case Sensitivity
Problem: Signatures don't match due to case differences
Solution: Convert both computed and received signatures to lowercase before comparison
// ❌ WRONG
if (computedSignature == receivedSignature)
// ✅ CORRECT
if (computedSignature.ToLowerInvariant() == receivedSignature.ToLowerInvariant())
❌ Framework Interference
Problem: Middleware modifies the request body before you can read it
Solution:
- In ASP.NET Core: Call
Request.EnableBuffering()before reading - In Express: Use
body-parserwithverifycallback to capture raw body
❌ Missing Idempotency
Problem: Duplicate webhooks cause duplicate processing
Solution: Implement idempotency using unique keys based on event data
Troubleshooting Checklist
When webhook verification fails, check:
- ✅ Using raw body bytes (not parsed and re-stringified JSON)
- ✅ Signature header is present and extracted correctly
- ✅ Webhook secret matches the one from Developer dashboard
- ✅ Using correct header name (
signature) - ✅ Signatures converted to lowercase before comparison
- ✅ Using constant-time comparison function
- ✅ UTF-8 encoding for both body and secret
- ✅ No BOM or extra whitespace added to body
- ✅ Framework middleware not modifying request body
Support
If you encounter issues with webhooks:
- Check the webhook delivery logs in your Developer dashboard
- Verify your signature implementation using the test vector
- Review the troubleshooting checklist above
- Contact SureGifts support with:
- Webhook URL
- Event type
- Timestamp of failed delivery
- Error logs from your endpoint
Quick Reference
Event Types Summary
| Event Type | Header Name | When Triggered | Contains Vouchers |
|---|---|---|---|
ORDER_UPDATED | signature | Order completed, cancelled, or failed | Yes (if COMPLETED or PARTIALLY_COMPLETED) |
VOUCHER_UPDATED | signature | Voucher balance changes | No |
Response Codes
| Code | Meaning | SureGifts Action |
|---|---|---|
| 200-299 | Success | No retry |
| 401 | Unauthorized (bad signature) | No retry |
| 400 | Bad request (invalid data) | No retry |
| 500-599 | Server error | Retry with backoff |
| Timeout | No response | Retry with backoff |
Signature Verification Quick Check
// 1. Read raw body
string rawBody = await ReadRawBodyAsync();
// 2. Get header
string signature = Request.Headers["signature"];
// 3. Compute expected signature
string expected = WebhookSecretHashUtil.ComputeHash(rawBody, webhookSecret);
// 4. Compare (constant-time)
bool isValid = CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(expected),
Encoding.UTF8.GetBytes(signature)
);
// 5. Reject if invalid
if (!isValid) return Unauthorized();
Summary
SureGifts Business Webhooks provide real-time notifications for order and voucher events. To successfully integrate:
- Configure webhooks in your Developer dashboard
- Secure your endpoint with HTTPS and signature verification
- Implement idempotency to handle duplicate deliveries
- Respond quickly (< 2 seconds) to avoid retries
- Handle errors gracefully with appropriate HTTP status codes
For additional support or questions, contact the SureGifts support team.