Skip to main content

Webhook Integration Guide

This guide covers how to set up, receive, and process webhook events from VesuvioPay to keep your application in sync with real-time events.

Overview​

VesuvioPay webhooks enable real-time notifications when events occur in your store:

  • Cart events (item added, updated, removed, cleared)
  • Order events (created, status updated, shipped, delivered)
  • Product events (created, updated, inventory changed)
  • Customer events (created, updated, deleted)

How Webhooks Work​

sequenceDiagram
participant VS as VesuvioPay
participant YS as Your Server

VS->>YS: POST /webhook (Event Payload)
Note over YS: Verify Signature
Note over YS: Process Event
YS-->>VS: 200 OK

alt Delivery Failed
VS->>YS: Retry (Attempt 2)
YS-->>VS: 200 OK
end
  1. An event occurs in VesuvioPay (e.g., order created)
  2. VesuvioPay sends an HTTP POST request to your webhook endpoint
  3. Your server verifies the webhook signature
  4. Your server processes the event
  5. Your server responds with HTTP 200
  6. If delivery fails, VesuvioPay retries with exponential backoff

Webhook Events​

Cart Events​

Event TypeDescription
cart.createdNew cart created
cart.item_addedItem added to cart
cart.item_updatedCart item quantity updated
cart.item_removedItem removed from cart
cart.abandonedCart abandoned (no activity)
cart.recoveredAbandoned cart recovered
cart.clearedAll items removed from cart
cart.checkout_startedCheckout process started

Order Events​

Event TypeDescription
order.createdNew order created (after checkout)
order.status_updatedOrder status changed
order.confirmedOrder payment confirmed
order.shippedOrder shipped to customer
order.deliveredOrder delivered
order.cancelledOrder cancelled
order.refundedOrder refunded
order.partially_shippedSome items shipped
order.partially_deliveredSome items delivered

Product Events​

Event TypeDescription
product.createdNew product created
product.updatedProduct details updated
product.deletedProduct deleted
product.inventory_updatedInventory quantity changed
product.variant_createdProduct variant created
product.variant_updatedVariant details updated
product.variant_deletedVariant deleted

Customer Events​

Event TypeDescription
customer.createdNew customer registered
customer.updatedCustomer profile updated
customer.deletedCustomer account deleted

System Events​

Event TypeDescription
webhook.testTest webhook event
webhook.endpoint_createdWebhook endpoint created
webhook.endpoint_updatedWebhook endpoint updated
webhook.endpoint_deletedWebhook endpoint deleted

Step 1: Create a Webhook Endpoint​

1.1 Register Your Endpoint​

Endpoint: POST /api/v1/sdk/stores/{storeId}/webhooks

Authentication: Private API Key

async function createWebhookEndpoint(storeId, url, events) {
const response = await fetch(
`https://api.vesuviopay.com/api/v1/sdk/stores/${storeId}/webhooks`,
{
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'your-private-api-key'
},
body: JSON.stringify({
url: url,
events: events,
description: 'Main webhook endpoint',
isEnabled: true,
apiVersion: '1.0'
})
}
);

return await response.json();
}

// Usage
const webhook = await createWebhookEndpoint(
'a3f2b1c0-1234-5678-90ab-cdef12345678',
'https://yourdomain.com/webhooks/vesuvio',
[
'cart.item_added',
'cart.item_updated',
'cart.item_removed',
'order.created',
'order.status_updated',
'order.shipped',
'order.delivered'
]
);

console.log('Webhook created:', webhook.data.id);
console.log('Webhook secret:', webhook.data.secret); // Save this securely!

Request Body:

{
"url": "https://yourdomain.com/webhooks/vesuvio",
"events": [
"cart.item_added",
"order.created",
"order.status_updated"
],
"description": "Main webhook endpoint for production",
"isEnabled": true,
"apiVersion": "1.0"
}

Response:

{
"success": true,
"data": {
"id": "b4e3c2d1-2345-6789-01bc-def123456789",
"storeId": "a3f2b1c0-1234-5678-90ab-cdef12345678",
"url": "https://yourdomain.com/webhooks/vesuvio",
"events": ["cart.item_added", "order.created"],
"secret": "whsec_abc123xyz789...", // SAVE THIS SECURELY!
"secretMask": "whsec_...xyz",
"description": "Main webhook endpoint",
"isEnabled": true,
"apiVersion": "1.0",
"createdAt": "2025-10-08T10:00:00Z"
}
}

IMPORTANT: Save the webhook secret securely. You'll need it to verify webhook signatures. The secret is only returned once during creation.

1.2 Manage Webhook Endpoints​

List Webhooks​

const webhooks = await fetch(
`https://api.vesuviopay.com/api/v1/sdk/stores/${storeId}/webhooks`,
{ headers: { 'X-Api-Key': 'your-private-api-key' } }
);

Update Webhook​

await fetch(
`https://api.vesuviopay.com/api/v1/sdk/stores/${storeId}/webhooks/${webhookId}`,
{
method: 'PUT',
headers: {
'Content-Type': 'application/json',
'X-Api-Key': 'your-private-api-key'
},
body: JSON.stringify({
events: ['order.created', 'order.shipped'],
isEnabled: true
})
}
);

Delete Webhook​

await fetch(
`https://api.vesuviopay.com/api/v1/sdk/stores/${storeId}/webhooks/${webhookId}`,
{
method: 'DELETE',
headers: { 'X-Api-Key': 'your-private-api-key' }
}
);

Test Webhook​

const testResult = await fetch(
`https://api.vesuviopay.com/api/v1/sdk/stores/${storeId}/webhooks/${webhookId}/test`,
{
method: 'POST',
headers: { 'X-Api-Key': 'your-private-api-key' }
}
);

Rotate Webhook Secret​

const newSecret = await fetch(
`https://api.vesuviopay.com/api/v1/sdk/stores/${storeId}/webhooks/${webhookId}/rotate-secret`,
{
method: 'POST',
headers: { 'X-Api-Key': 'your-private-api-key' }
}
);
// Save the new secret returned in the response

Step 2: Implement Your Webhook Receiver​

2.1 Webhook Payload Structure​

All webhook events follow this structure:

{
"id": "evt_abc123xyz789",
"type": "order.created",
"apiVersion": "1.0",
"createdAt": "2025-10-08T12:30:00Z",
"livemode": true,
"data": {
// Event-specific data
}
}

Common Fields:

  • id (string): Unique event identifier (use for idempotency)
  • type (string): Event type (e.g., "order.created")
  • apiVersion (string): API version (currently "1.0")
  • createdAt (datetime): When the event occurred
  • livemode (boolean): Whether this is a production event
  • data (object): Event-specific payload

2.2 Example Event Payloads​

Cart Item Added Event​

{
"id": "evt_cart_item_added_123",
"type": "cart.item_added",
"apiVersion": "1.0",
"createdAt": "2025-10-08T12:30:00Z",
"livemode": true,
"data": {
"storeId": "a3f2b1c0-1234-5678-90ab-cdef12345678",
"customerId": "b4e3c2d1-2345-6789-01bc-def123456789",
"occurredAt": "2025-10-08T12:30:00Z",
"cartId": "c5f4d3e2-3456-7890-12cd-ef1234567890",
"item": {
"id": "d6g5e4f3-4567-8901-23de-f12345678901",
"productVariantId": "e7h6f5g4-5678-9012-34ef-g123456789012",
"externalId": "shopify_12345",
"externalVariantId": "shopify_variant_001",
"productTitle": "Premium T-Shirt",
"variantTitle": "Small / Red",
"sku": "TSHIRT-S-RED",
"quantity": 2,
"unitPrice": 29.99,
"totalPrice": 59.98,
"addedAt": "2025-10-08T12:30:00Z"
},
"cartSummary": {
"cartId": "c5f4d3e2-3456-7890-12cd-ef1234567890",
"itemCount": 1,
"totalQuantity": 2,
"subtotal": 59.98,
"estimatedTax": 4.80,
"estimatedShipping": 5.00,
"estimatedTotal": 69.78,
"status": "Active"
}
}
}

Order Created Event​

{
"id": "evt_order_created_456",
"type": "order.created",
"apiVersion": "1.0",
"createdAt": "2025-10-08T13:00:00Z",
"livemode": true,
"data": {
"storeId": "a3f2b1c0-1234-5678-90ab-cdef12345678",
"customerId": "b4e3c2d1-2345-6789-01bc-def123456789",
"occurredAt": "2025-10-08T13:00:00Z",
"orderId": "f8i7g6h5-6789-0123-45fg-h12345678901",
"orderNumber": "VES-1001",
"checkoutId": "g9j8h7i6-7890-1234-56gh-i12345678901",
"customer": {
"customerId": "b4e3c2d1-2345-6789-01bc-def123456789",
"email": "customer@example.com",
"phone": "+1234567890",
"firstName": "John",
"lastName": "Doe"
},
"items": [
{
"id": "h0k9i8j7-8901-2345-67hi-j12345678901",
"productVariantId": "e7h6f5g4-5678-9012-34ef-g123456789012",
"externalProductId": "shopify_12345",
"externalVariantId": "shopify_variant_001",
"productTitle": "Premium T-Shirt",
"variantTitle": "Small / Red",
"sku": "TSHIRT-S-RED",
"quantity": 2,
"unitPrice": 29.99,
"totalPrice": 59.98
}
],
"shippingAddress": {
"firstName": "John",
"lastName": "Doe",
"address1": "123 Main St",
"city": "New York",
"province": "NY",
"country": "United States",
"countryIsoCode": "US",
"postalCode": "10001",
"phone": "+1234567890"
},
"totals": {
"subtotal": 59.98,
"shippingTotal": 5.00,
"taxTotal": 4.80,
"discountTotal": 0.00,
"totalAmount": 69.78
},
"paymentMethod": {
"method": "credit_card",
"display": "Visa ending in 4242",
"last4": "4242",
"brand": "Visa"
},
"orderStatus": "Confirmed",
"placedAt": "2025-10-08T13:00:00Z"
}
}

Step 3: Verify Webhook Signatures​

CRITICAL: Always verify webhook signatures to ensure requests are from VesuvioPay.

VesuvioPay signs all webhook requests using HMAC-SHA256. The signature is sent in the X-Vesuvio-Signature header.

3.1 Signature Format​

X-Vesuvio-Signature: sha256=<hex-encoded-hash>

3.2 Verification Implementation​

JavaScript/Node.js​

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
// Extract the signature hash
const signatureParts = signature.split('=');
if (signatureParts.length !== 2 || signatureParts[0] !== 'sha256') {
return false;
}

const receivedHash = signatureParts[1];

// Compute expected signature
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expectedHash = hmac.digest('hex');

// Constant-time comparison to prevent timing attacks
return crypto.timingSafeEqual(
Buffer.from(receivedHash, 'hex'),
Buffer.from(expectedHash, 'hex')
);
}

// Express.js middleware
const express = require('express');
const app = express();

app.post('/webhooks/vesuvio',
express.raw({ type: 'application/json' }), // Important: get raw body
(req, res) => {
const signature = req.headers['x-vesuvio-signature'];
const payload = req.body.toString('utf8'); // Raw body as string
const secret = process.env.VESUVIO_WEBHOOK_SECRET;

// Verify signature
if (!verifyWebhookSignature(payload, signature, secret)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}

// Parse the payload after verification
const event = JSON.parse(payload);

// Process the event
processWebhookEvent(event);

// Always respond with 200 quickly
res.status(200).send('OK');
}
);

C# / .NET​

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

public class WebhookController : ControllerBase
{
private readonly string _webhookSecret;

public WebhookController(IConfiguration configuration)
{
_webhookSecret = configuration["VesuvioWebhookSecret"];
}

[HttpPost("webhooks/vesuvio")]
public async Task<IActionResult> HandleWebhook()
{
// Read raw body
using var reader = new StreamReader(Request.Body);
var payload = await reader.ReadToEndAsync();

// Get signature from header
var signature = Request.Headers["X-Vesuvio-Signature"].ToString();

// Verify signature
if (!VerifyWebhookSignature(payload, signature, _webhookSecret))
{
return Unauthorized("Invalid signature");
}

// Parse and process event
var webhookEvent = JsonSerializer.Deserialize<WebhookEvent>(payload);
await ProcessWebhookEvent(webhookEvent);

return Ok();
}

private bool VerifyWebhookSignature(string payload, string signature, string secret)
{
// Extract hash from signature
var parts = signature.Split('=');
if (parts.Length != 2 || parts[0] != "sha256")
{
return false;
}

var receivedHash = parts[1];

// Compute expected signature
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
var hashBytes = hmac.ComputeHash(Encoding.UTF8.GetBytes(payload));
var expectedHash = Convert.ToHexString(hashBytes).ToLower();

// Constant-time comparison
return CryptographicOperations.FixedTimeEquals(
Encoding.UTF8.GetBytes(receivedHash),
Encoding.UTF8.GetBytes(expectedHash)
);
}

private async Task ProcessWebhookEvent(WebhookEvent webhookEvent)
{
switch (webhookEvent.Type)
{
case "order.created":
await HandleOrderCreated(webhookEvent);
break;
case "cart.item_added":
await HandleCartItemAdded(webhookEvent);
break;
// ... other event types
}
}
}

3.3 Additional Security Headers​

VesuvioPay includes additional headers for verification:

X-Vesuvio-Signature: sha256=abc123...
X-Vesuvio-Event-Id: evt_abc123xyz789
X-Vesuvio-Delivery-Id: delivery_123
X-Vesuvio-Webhook-Id: webhook_456
X-Vesuvio-Timestamp: 1633024800
X-Vesuvio-Attempt: 1
User-Agent: VesuvioPay-Webhooks/2.0

You can use these for additional verification:

  • Check X-Vesuvio-Timestamp to reject old requests
  • Use X-Vesuvio-Event-Id for idempotency
  • Log X-Vesuvio-Attempt to track retries

Step 4: Process Events​

4.1 Idempotency Handling​

CRITICAL: Always implement idempotency to handle duplicate webhook deliveries.

const processedEvents = new Set(); // In production, use Redis or database

async function processWebhookEvent(event) {
const eventId = event.id;

// Check if we've already processed this event
if (processedEvents.has(eventId)) {
console.log(`Event ${eventId} already processed, skipping`);
return;
}

try {
// Process the event
await handleEvent(event);

// Mark as processed
processedEvents.add(eventId);

// In production, store in database:
// await db.webhookEvents.create({ eventId, processedAt: new Date() });
} catch (error) {
console.error(`Error processing event ${eventId}:`, error);
throw error; // Let the webhook retry
}
}

4.2 Event Processing Patterns​

Async Processing with Queue​

const Bull = require('bull');
const webhookQueue = new Bull('webhooks', {
redis: { host: 'localhost', port: 6379 }
});

// Webhook receiver - respond quickly
app.post('/webhooks/vesuvio', async (req, res) => {
const event = req.body;

// Verify signature first
if (!verifyWebhookSignature(...)) {
return res.status(401).send('Invalid signature');
}

// Add to queue for async processing
await webhookQueue.add(event, {
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});

// Respond immediately
res.status(200).send('OK');
});

// Queue processor
webhookQueue.process(async (job) => {
const event = job.data;
await processWebhookEvent(event);
});

Event Router Pattern​

const eventHandlers = {
'cart.item_added': handleCartItemAdded,
'cart.item_updated': handleCartItemUpdated,
'cart.item_removed': handleCartItemRemoved,
'order.created': handleOrderCreated,
'order.status_updated': handleOrderStatusUpdated,
'order.shipped': handleOrderShipped,
'order.delivered': handleOrderDelivered,
'product.inventory_updated': handleInventoryUpdated
};

async function processWebhookEvent(event) {
const handler = eventHandlers[event.type];

if (!handler) {
console.warn(`No handler for event type: ${event.type}`);
return;
}

try {
await handler(event.data);
} catch (error) {
console.error(`Error handling ${event.type}:`, error);
throw error;
}
}

// Individual event handlers
async function handleOrderCreated(data) {
console.log('New order created:', data.orderNumber);

// Update inventory
await updateInventory(data.items);

// Notify fulfillment
await notifyFulfillment(data.orderId);

// Send confirmation email
await sendOrderConfirmation(data.customer.email, data);
}

async function handleCartItemAdded(data) {
console.log('Item added to cart:', data.item.productTitle);

// Track analytics
await analytics.track('cart_item_added', {
customerId: data.customerId,
productId: data.item.externalId,
price: data.item.unitPrice,
quantity: data.item.quantity
});
}

async function handleInventoryUpdated(data) {
console.log('Inventory updated:', data.productTitle);

// Sync to external platform
await syncInventoryToShopify(data.externalProductId, data.inventoryQuantity);
}

Step 5: Error Handling and Retry Logic​

5.1 Webhook Delivery Behavior​

VesuvioPay webhook delivery:

  • Timeout: 30 seconds
  • Retries: Up to 5 attempts
  • Retry Schedule: Exponential backoff (1min, 5min, 15min, 1hr, 24hr)
  • Success: HTTP 2xx response
  • Failure: HTTP 4xx/5xx or timeout

5.2 Best Practices for Reliability​

Respond Quickly​

// Good: Acknowledge immediately, process async
app.post('/webhooks/vesuvio', async (req, res) => {
const event = req.body;

// Verify signature
if (!verifyWebhookSignature(...)) {
return res.status(401).send('Invalid signature');
}

// Queue for processing
await queue.add(event);

// Respond immediately (< 1 second)
res.status(200).send('OK');

// Process asynchronously
// processWebhookEvent(event); // This happens in queue worker
});

// Bad: Process synchronously
app.post('/webhooks/vesuvio', async (req, res) => {
const event = req.body;

// This could take 10+ seconds
await processWebhookEvent(event);
await updateDatabase(event);
await sendEmail(event);

res.status(200).send('OK'); // Too slow!
});

Handle Errors Gracefully​

async function processWebhookEvent(event) {
try {
await handleEvent(event);
} catch (error) {
if (error.code === 'DUPLICATE_EVENT') {
// Already processed, don't retry
console.log('Event already processed');
return;
}

if (error.code === 'TEMPORARY_ERROR') {
// Temporary error, let it retry
throw error;
}

// Log permanent errors but don't retry
console.error('Permanent error processing event:', error);
await logErrorToMonitoring(error, event);
}
}

5.3 Monitoring Webhook Health​

const webhookMetrics = {
received: 0,
processed: 0,
failed: 0,
processingTimeMs: []
};

async function trackWebhookProcessing(event, processFn) {
const startTime = Date.now();
webhookMetrics.received++;

try {
await processFn(event);
webhookMetrics.processed++;
} catch (error) {
webhookMetrics.failed++;
throw error;
} finally {
const duration = Date.now() - startTime;
webhookMetrics.processingTimeMs.push(duration);

// Alert if processing is slow
if (duration > 5000) {
console.warn(`Slow webhook processing: ${duration}ms for ${event.type}`);
}
}
}

// Expose metrics endpoint
app.get('/metrics/webhooks', (req, res) => {
const avgProcessingTime = webhookMetrics.processingTimeMs.length > 0
? webhookMetrics.processingTimeMs.reduce((a, b) => a + b, 0) / webhookMetrics.processingTimeMs.length
: 0;

res.json({
received: webhookMetrics.received,
processed: webhookMetrics.processed,
failed: webhookMetrics.failed,
successRate: webhookMetrics.processed / webhookMetrics.received,
avgProcessingTimeMs: avgProcessingTime
});
});

Step 6: Testing Your Integration​

6.1 Use Webhook Testing Endpoint​

const testWebhook = await fetch(
`https://api.vesuviopay.com/api/v1/sdk/stores/${storeId}/webhooks/${webhookId}/test`,
{
method: 'POST',
headers: { 'X-Api-Key': 'your-private-api-key' },
body: JSON.stringify({ eventType: 'order.created' })
}
);

const result = await testWebhook.json();
console.log('Test result:', result);

6.2 Local Testing with ngrok​

# Install ngrok
npm install -g ngrok

# Start your local server
node server.js

# Create tunnel to localhost
ngrok http 3000

# Use the ngrok URL for your webhook endpoint
# https://abc123.ngrok.io/webhooks/vesuvio

6.3 Webhook Testing Tools​

Use tools like webhook.site for testing:

// Create test webhook pointing to webhook.site
await createWebhookEndpoint(
storeId,
'https://webhook.site/unique-url',
['order.created']
);

// Trigger events and view payloads at webhook.site

Complete Integration Example​

Here's a production-ready webhook receiver:

const express = require('express');
const crypto = require('crypto');
const Bull = require('bull');

const app = express();
const webhookQueue = new Bull('webhooks', {
redis: { host: 'localhost', port: 6379 }
});

// Store for idempotency (use Redis/Database in production)
const processedEvents = new Set();

// Webhook secret from environment
const WEBHOOK_SECRET = process.env.VESUVIO_WEBHOOK_SECRET;

// Signature verification
function verifyWebhookSignature(payload, signature, secret) {
const parts = signature.split('=');
if (parts.length !== 2 || parts[0] !== 'sha256') return false;

const receivedHash = parts[1];
const hmac = crypto.createHmac('sha256', secret);
hmac.update(payload);
const expectedHash = hmac.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(receivedHash, 'hex'),
Buffer.from(expectedHash, 'hex')
);
}

// Webhook receiver endpoint
app.post('/webhooks/vesuvio',
express.raw({ type: 'application/json' }),
async (req, res) => {
const signature = req.headers['x-vesuvio-signature'];
const payload = req.body.toString('utf8');

// Verify signature
if (!verifyWebhookSignature(payload, signature, WEBHOOK_SECRET)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}

// Parse event
const event = JSON.parse(payload);
const eventId = event.id;

// Check idempotency
if (processedEvents.has(eventId)) {
console.log(`Event ${eventId} already processed`);
return res.status(200).send('OK');
}

// Queue for processing
await webhookQueue.add(event, {
jobId: eventId,
attempts: 3,
backoff: { type: 'exponential', delay: 2000 }
});

// Mark as received
processedEvents.add(eventId);

// Respond quickly
res.status(200).send('OK');
}
);

// Event handlers
const eventHandlers = {
'order.created': async (data) => {
console.log('Order created:', data.orderNumber);

// Sync to your system
await db.orders.create({
vesuvioOrderId: data.orderId,
orderNumber: data.orderNumber,
customerId: data.customer.customerId,
totalAmount: data.totals.totalAmount,
status: data.orderStatus
});

// Notify fulfillment
await fulfillmentService.createShipment(data);

// Send confirmation
await emailService.sendOrderConfirmation(data.customer.email, data);
},

'order.shipped': async (data) => {
console.log('Order shipped:', data.orderNumber);

// Update your database
await db.orders.update(
{ vesuvioOrderId: data.orderId },
{
status: 'Shipped',
trackingNumber: data.trackingInfo[0]?.trackingNumber
}
);

// Notify customer
await smsService.sendShippingNotification(
data.customer.phone,
data.trackingInfo
);
},

'cart.item_added': async (data) => {
console.log('Cart item added:', data.item.productTitle);

// Track analytics
await analytics.track('cart_add', {
customerId: data.customerId,
productId: data.item.externalId,
variantId: data.item.externalVariantId,
price: data.item.unitPrice,
quantity: data.item.quantity
});
},

'product.inventory_updated': async (data) => {
console.log('Inventory updated:', data.productTitle);

// Sync back to Shopify
await shopify.updateInventory(
data.externalProductId,
data.externalVariantId,
data.inventoryQuantity
);
}
};

// Queue processor
webhookQueue.process(async (job) => {
const event = job.data;
const handler = eventHandlers[event.type];

if (!handler) {
console.warn(`No handler for event type: ${event.type}`);
return;
}

try {
await handler(event.data);
console.log(`Successfully processed event: ${event.type}`);
} catch (error) {
console.error(`Error processing ${event.type}:`, error);
throw error; // Will retry
}
});

// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', uptime: process.uptime() });
});

// Start server
app.listen(3000, () => {
console.log('Webhook server running on port 3000');
});

Troubleshooting​

Webhook Not Receiving Events​

  1. Verify endpoint URL is publicly accessible
  2. Check webhook is enabled
  3. Confirm events are subscribed
  4. Test with the test endpoint

Signature Verification Failing​

  1. Ensure you're using the raw request body
  2. Verify the webhook secret is correct
  3. Check for middleware that modifies the body
  4. Ensure charset encoding is UTF-8

Duplicate Events​

  1. Implement idempotency using event ID
  2. Store processed event IDs in database
  3. Return 200 for already-processed events

Slow Processing​

  1. Respond with 200 immediately
  2. Process events asynchronously using queues
  3. Monitor processing time
  4. Scale queue workers as needed

Best Practices Summary​

  1. Always verify signatures before processing
  2. Respond quickly (< 1 second) with HTTP 200
  3. Process asynchronously using queues
  4. Implement idempotency using event IDs
  5. Handle retries gracefully with exponential backoff
  6. Monitor webhook health and processing times
  7. Log all events for debugging and auditing
  8. Use HTTPS for webhook endpoints
  9. Rotate secrets periodically for security
  10. Test thoroughly before going live

Next Steps​