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

order.created

Fired when a new order is placed

order.paid

Fired when payment for an order is confirmed

order.cancelled

Fired when an order is cancelled

order.refunded

Fired when an order is refunded

order.fulfilled

Fired when all items in an order are shipped

order.partially_fulfilled

Fired when some items in an order are shipped

order.status_changed

Fired when order status changes

order.note_added

Fired when a note is added to an order

Payment

payment.received

Fired when a payment is received

payment.failed

Fired when a payment attempt fails

payment.pending

Fired when a payment is pending confirmation

Refund

refund.created

Fired when a refund is initiated

refund.completed

Fired when a refund is completed

refund.failed

Fired when a refund fails

Shipment

shipment.created

Fired when a shipment is created

shipment.shipped

Fired when a shipment is dispatched

shipment.in_transit

Fired when a shipment is in transit

shipment.out_for_delivery

Fired when a shipment is out for delivery

shipment.delivered

Fired when a shipment is delivered

shipment.failed

Fired when a shipment delivery fails

shipment.returned

Fired when a shipment is returned

shipment.tracking_updated

Fired when shipment tracking information is updated

Inventory

inventory.low_stock

Fired when product stock falls below threshold

inventory.out_of_stock

Fired when a product goes out of stock

inventory.restocked

Fired when a product is restocked

inventory.adjusted

Fired when inventory is manually adjusted

Product

product.created

Fired when a new product is created

product.updated

Fired when product details are updated

product.deleted

Fired when a product is deleted

product.published

Fired when a product is published

product.unpublished

Fired when a product is unpublished

Customer

customer.created

Fired when a new customer registers

customer.updated

Fired when customer profile is updated

customer.deleted

Fired when a customer account is deleted

Subscription

subscription.created

Fired when a new subscription is created

subscription.activated

Fired when a subscription is activated

subscription.renewed

Fired when a subscription is renewed

subscription.cancelled

Fired when a subscription is cancelled

subscription.expired

Fired when a subscription expires

subscription.paused

Fired when a subscription is paused

subscription.resumed

Fired when a paused subscription is resumed

subscription.payment_failed

Fired when a subscription payment fails

Cart

cart.abandoned

Fired when a cart is abandoned (after configurable delay)

cart.recovered

Fired when an abandoned cart is recovered

Translation

translation.job_completed

Fired when a translation job completes successfully

translation.job_failed

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:

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

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.