Webhooks
Receive real-time notifications when events occur in your store. Build automated workflows, sync external systems, and create responsive integrations.
Overview
Spwig sends HTTP POST requests to your configured endpoint URLs when events occur. Each webhook contains a JSON payload describing the event and the affected resource. Your endpoint should return a 2xx status code within 30 seconds to acknowledge receipt.
Configuring Webhooks: Set up webhook endpoints in your Spwig admin panel at Settings → Integrations → Webhooks. You can subscribe to specific events or all events.
Webhook Headers
Each webhook request includes the following headers:
| Header | Description |
|---|---|
X-Spwig-Signature |
HMAC-SHA256 signature of the payload for verification |
X-Spwig-Event |
The event type (e.g., order.created) |
X-Spwig-Delivery-ID |
Unique identifier for this delivery attempt (use for idempotency) |
X-Spwig-Timestamp |
Unix timestamp when the webhook was sent |
Content-Type |
application/json |
User-Agent |
Spwig-Webhooks/1.0 |
Signature Verification
Always verify the webhook signature to ensure requests are genuinely from Spwig. The signature is created using HMAC-SHA256 with your endpoint's secret key.
import hmac
import hashlib
import time
def verify_webhook(payload: bytes, signature: str, timestamp: str, secret: str) -> bool:
"""Verify Spwig webhook signature with timestamp validation."""
# Reject old webhooks (prevent replay attacks)
if abs(time.time() - int(timestamp)) > 300: # 5 minutes
return False
expected = hmac.new(
secret.encode('utf-8'),
payload,
hashlib.sha256
).hexdigest()
return hmac.compare_digest(f"sha256={expected}", signature)
# Flask example
@app.route('/webhooks/spwig', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Spwig-Signature')
timestamp = request.headers.get('X-Spwig-Timestamp')
if not verify_webhook(request.data, signature, timestamp, WEBHOOK_SECRET):
return 'Invalid signature', 401
event = request.headers.get('X-Spwig-Event')
data = request.json
# Process the webhook...
return 'OK', 200
const crypto = require('crypto');
function verifyWebhook(payload, signature, timestamp, secret) {
// Reject old webhooks (prevent replay attacks)
const age = Math.abs(Date.now() / 1000 - parseInt(timestamp));
if (age > 300) return false; // 5 minutes
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
// Express example
app.post('/webhooks/spwig', express.raw({type: 'application/json'}), (req, res) => {
const signature = req.headers['x-spwig-signature'];
const timestamp = req.headers['x-spwig-timestamp'];
if (!verifyWebhook(req.body, signature, timestamp, WEBHOOK_SECRET)) {
return res.status(401).send('Invalid signature');
}
const event = req.headers['x-spwig-event'];
const data = JSON.parse(req.body);
// Process the webhook...
res.status(200).send('OK');
});
<?php
function verifyWebhook($payload, $signature, $timestamp, $secret) {
// Reject old webhooks (prevent replay attacks)
if (abs(time() - intval($timestamp)) > 300) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $payload, $secret);
return hash_equals($expected, $signature);
}
// Usage
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_SPWIG_SIGNATURE'] ?? '';
$timestamp = $_SERVER['HTTP_X_SPWIG_TIMESTAMP'] ?? '';
if (!verifyWebhook($payload, $signature, $timestamp, WEBHOOK_SECRET)) {
http_response_code(401);
exit('Invalid signature');
}
$event = $_SERVER['HTTP_X_SPWIG_EVENT'];
$data = json_decode($payload, true);
// Process the webhook...
http_response_code(200);
echo 'OK';
Example Payloads
Below are example payloads for common webhook events. All payloads include the full resource data at the time of the event.
order.created
order.created
— Triggered when a new order is placed
{
"event": "order.created",
"created_at": "2024-01-15T14:30:00Z",
"data": {
"id": 12345,
"order_number": "SPW-10045",
"status": "pending",
"currency": "USD",
"subtotal": "149.99",
"tax": "12.00",
"shipping": "9.99",
"total": "171.98",
"customer": {
"id": 789,
"email": "[email protected]",
"first_name": "Jane",
"last_name": "Doe"
},
"shipping_address": {
"line1": "123 Main St",
"city": "New York",
"state": "NY",
"postal_code": "10001",
"country": "US"
},
"line_items": [
{
"id": 1,
"product_id": 456,
"variant_id": 789,
"name": "Wireless Headphones",
"sku": "WH-100-BLK",
"quantity": 1,
"unit_price": "149.99",
"total": "149.99"
}
],
"created_at": "2024-01-15T14:30:00Z"
}
}
payment.received
payment.received
— Triggered when payment is successfully captured
{
"event": "payment.received",
"created_at": "2024-01-15T14:32:00Z",
"data": {
"id": 5678,
"order_id": 12345,
"order_number": "SPW-10045",
"amount": "171.98",
"currency": "USD",
"status": "captured",
"payment_method": {
"type": "card",
"brand": "visa",
"last4": "4242"
},
"transaction_id": "txn_abc123def456",
"created_at": "2024-01-15T14:32:00Z"
}
}
inventory.low_stock
inventory.low_stock
— Triggered when stock falls below threshold
{
"event": "inventory.low_stock",
"created_at": "2024-01-15T15:00:00Z",
"data": {
"variant_id": 789,
"product_id": 456,
"product_name": "Wireless Headphones",
"variant_name": "Black",
"sku": "WH-100-BLK",
"current_stock": 3,
"threshold": 5,
"location": {
"id": 1,
"name": "Main Warehouse"
}
}
}
cart.abandoned
cart.abandoned
— Triggered after cart inactivity period
{
"event": "cart.abandoned",
"created_at": "2024-01-15T16:00:00Z",
"data": {
"cart_id": "cart_abc123",
"customer_email": "[email protected]",
"currency": "USD",
"subtotal": "249.98",
"items": [
{
"product_id": 456,
"variant_id": 789,
"name": "Wireless Headphones",
"quantity": 1,
"price": "149.99"
},
{
"product_id": 457,
"variant_id": 790,
"name": "Headphone Stand",
"quantity": 1,
"price": "99.99"
}
],
"abandoned_at": "2024-01-15T16:00:00Z",
"recovery_url": "https://store.example.com/cart/recover/abc123"
}
}
shipment.created
shipment.created
— Triggered when a shipment is created
{
"event": "shipment.created",
"created_at": "2024-01-16T09:00:00Z",
"data": {
"id": 234,
"order_id": 12345,
"order_number": "SPW-10045",
"carrier": "ups",
"service": "ground",
"tracking_number": "1Z999AA10123456784",
"tracking_url": "https://ups.com/track?num=1Z999AA10123456784",
"status": "label_created",
"items": [
{
"line_item_id": 1,
"quantity": 1
}
],
"shipped_at": null,
"estimated_delivery": "2024-01-19"
}
}
Available Events
Order
Fired when a new order is placed
Fired when payment for an order is confirmed
Fired when an order is cancelled
Fired when an order is refunded
Fired when all items in an order are shipped
Fired when some items in an order are shipped
Fired when order status changes
Fired when a note is added to an order
Payment
Fired when a payment is received
Fired when a payment attempt fails
Fired when a payment is pending confirmation
Refund
Fired when a refund is initiated
Fired when a refund is completed
Fired when a refund fails
Shipment
Fired when a shipment is created
Fired when a shipment is dispatched
Fired when a shipment is in transit
Fired when a shipment is out for delivery
Fired when a shipment is delivered
Fired when a shipment delivery fails
Fired when a shipment is returned
Fired when shipment tracking information is updated
Inventory
Fired when product stock falls below threshold
Fired when a product goes out of stock
Fired when a product is restocked
Fired when inventory is manually adjusted
Product
Fired when a new product is created
Fired when product details are updated
Fired when a product is deleted
Fired when a product is published
Fired when a product is unpublished
Customer
Fired when a new customer registers
Fired when customer profile is updated
Fired when a customer account is deleted
Subscription
Fired when a new subscription is created
Fired when a subscription is activated
Fired when a subscription is renewed
Fired when a subscription is cancelled
Fired when a subscription expires
Fired when a subscription is paused
Fired when a paused subscription is resumed
Fired when a subscription payment fails
Cart
Fired when a cart is abandoned (after configurable delay)
Fired when an abandoned cart is recovered
Translation
Fired when a translation job completes successfully
Fired when a translation job fails
Retry Policy
If your endpoint returns a non-2xx status code or times out (30 seconds), Spwig will retry the delivery with exponential backoff:
- 1st retry: 1 minute after initial failure
- 2nd retry: 5 minutes after 1st retry
- 3rd retry: 30 minutes after 2nd retry
- 4th retry: 2 hours after 3rd retry
- 5th retry: 24 hours after 4th retry
After 5 failed attempts, the endpoint will be automatically disabled. You can re-enable it from the admin panel after fixing the issue.
Idempotency: Use the X-Spwig-Delivery-ID header to detect duplicate deliveries. Store processed delivery IDs and skip duplicates.
Security Best Practices
-
Always verify signatures — Never process a webhook without validating the
X-Spwig-Signatureheader against your secret. -
Check timestamps — Reject webhooks where
X-Spwig-Timestampis more than 5 minutes old to prevent replay attacks. - Use HTTPS endpoints — Only configure webhook URLs using HTTPS to ensure payload encryption in transit.
-
Implement idempotency — Store
X-Spwig-Delivery-IDvalues and skip processing if you've seen a delivery before. -
Process asynchronously — Return
200 OKimmediately and process the webhook in a background job to avoid timeouts. - Rotate secrets periodically — Generate a new webhook secret every 90 days. You can have multiple active secrets during rotation.
IP Allowlisting (Optional)
For additional security, you can restrict your webhook endpoint to only accept requests from Spwig's IP addresses. Contact support to get the current list of egress IPs.
Webhook Versioning: Payload schemas are stable within a major API version. Breaking changes to webhook payloads will only occur in new API versions, giving you time to migrate.