Webhooks

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

  1. Log in to your SureGifts Business Dashboard
  2. Navigate to Developer in the left sidebar menu
  3. Click on the Webhooks tab

Step 2: Configure Your Webhook

  1. Click the + Add button to create a new webhook
  2. Enter your webhook URL (must be HTTPS)
  3. Select the events you want to receive:
    • ORDER_UPDATED - Receive notifications when orders are completed, cancelled, or failed
    • VOUCHER_UPDATED - Receive notifications when voucher balances change
  4. Click Save

Step 3: Retrieve Your Webhook Secret

After creating your webhook:

  1. Your webhook secret will be displayed in the webhook card
  2. Click the Refresh icon (🔄) next to the secret to copy it
  3. 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 delivered
  • PARTIALLY_COMPLETED - Order partially delivered
  • CANCELED - Order cancelled
  • FAILED - Order failed

Order Types:

SureGifts supports two order types, which affect the voucher data structure:

  1. Download Orders: Vouchers are immediately available for download

    • Includes: pin, code, expiryDate
    • Excludes: status, firstName, lastName, email (all null)
  2. Delivery Orders: Vouchers are delivered to recipients via email/SMS

    • Includes: status, firstName, lastName, email
    • Excludes: pin, code (null for security), expiryDate (null)

Data Fields:

FieldTypeDescriptionAlways Present
orderNumberstringSureGifts order reference numberYes
referencestringYour reference used when creating the orderYes
statusstringOrder status: COMPLETED, PARTIALLY_COMPLETED, CANCELED, FAILEDYes
vouchersarrayArray of voucher details (only for COMPLETED and PARTIALLY_COMPLETED)Conditional

Voucher Object Fields:

FieldTypeDescription
valuedecimalInitial voucher value
expiryDatedatetimeVoucher expiry date (nullable, auto-extends by 1 year)
pinstringVoucher redemption PIN (only for download orders)
codestringVoucher code (only for download orders)
seriallongVoucher serial number
statusstringItem delivery status: DELIVERED, FAILED, CANCELED, PROCESSING (null for download orders)
firstNamestringRecipient first name (null for download orders)
lastNamestringRecipient last name (null for download orders)
emailstringRecipient 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:

FieldTypeDescription
orderNumberstringOrder number associated with the voucher
voucherCodestringMasked voucher code
serialNumberlongVoucher serial number
balancedecimalCurrent voucher balance after the transaction
emailstringEmail 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

  1. Read the raw request body exactly as received (do not parse or modify)
  2. Compute the signature:
    signature = lowercase_hex(HMAC_SHA512(key=webhookSecret, message=rawBodyBytes))
    
  3. Extract the signature from the signature header
  4. Compare signatures using constant-time comparison
  5. Reject if mismatch - respond with 401 Unauthorized and 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

  1. Always verify signatures - Never process webhooks without signature verification
  2. Use HTTPS only - Webhooks will only be sent to HTTPS endpoints in production
  3. Store secrets securely - Use environment variables or secret management services (Azure Key Vault, AWS Secrets Manager, etc.)
  4. Use constant-time comparison - Prevents timing attacks when comparing signatures
  5. Validate webhook origin - Only accept webhooks from known SureGifts IP ranges (if applicable)

Reliability

  1. Respond quickly - Return 200 OK within 2 seconds to avoid retries
  2. Process asynchronously - Queue webhook processing for long-running operations
  3. Implement idempotency - Use unique keys to prevent duplicate processing
  4. Handle retries gracefully - SureGifts will retry failed webhooks with exponential backoff
  5. Log webhook events - Keep audit logs for debugging and compliance

Data Handling

  1. Redact sensitive data in logs - Never log voucher PINs, codes, or webhook secrets
  2. Validate data structure - Check for required fields before processing
  3. Handle missing fields - Some fields (like vouchers) are conditional
  4. 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 OK for successfully processed webhooks
  • Return 401 Unauthorized for signature verification failures (no retry)
  • Return 400 Bad Request for invalid data (no retry)
  • Return 500 Internal Server Error for 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-parser with verify callback 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:

  1. Check the webhook delivery logs in your Developer dashboard
  2. Verify your signature implementation using the test vector
  3. Review the troubleshooting checklist above
  4. Contact SureGifts support with:
    • Webhook URL
    • Event type
    • Timestamp of failed delivery
    • Error logs from your endpoint

Quick Reference

Event Types Summary

Event TypeHeader NameWhen TriggeredContains Vouchers
ORDER_UPDATEDsignatureOrder completed, cancelled, or failedYes (if COMPLETED or PARTIALLY_COMPLETED)
VOUCHER_UPDATEDsignatureVoucher balance changesNo

Response Codes

CodeMeaningSureGifts Action
200-299SuccessNo retry
401Unauthorized (bad signature)No retry
400Bad request (invalid data)No retry
500-599Server errorRetry with backoff
TimeoutNo responseRetry 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:

  1. Configure webhooks in your Developer dashboard
  2. Secure your endpoint with HTTPS and signature verification
  3. Implement idempotency to handle duplicate deliveries
  4. Respond quickly (< 2 seconds) to avoid retries
  5. Handle errors gracefully with appropriate HTTP status codes

For additional support or questions, contact the SureGifts support team.