> ## Documentation Index
> Fetch the complete documentation index at: https://docs.stateset.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Webhooks

> Real-time event notifications for your StateSet integration

<Info>
  Webhooks allow your application to receive real-time notifications when events occur in StateSet, eliminating the need for polling.
</Info>

## Overview

StateSet webhooks provide real-time event notifications delivered via HTTPS POST requests to your configured endpoints. Each webhook payload includes comprehensive event data and is secured with HMAC signatures.

### Key Features

* 🔄 **Automatic retries** with exponential backoff
* 🔐 **Secure signatures** using HMAC-SHA256
* 📊 **Event versioning** for backward compatibility
* 🎯 **Granular event selection** - subscribe only to events you need
* 📝 **Detailed payloads** with full resource data
* 🔍 **Event replay** for missed or failed deliveries

## Setting Up Webhooks

<Steps>
  <Step title="Create Webhook Endpoint">
    Navigate to **Dashboard → Settings → Webhooks** and click **Add Endpoint**
  </Step>

  <Step title="Configure Endpoint">
    * Enter your HTTPS endpoint URL
    * Select events to subscribe to
    * Copy the signing secret for verification
  </Step>

  <Step title="Implement Handler">
    Create an endpoint that:

    * Accepts POST requests
    * Verifies signatures
    * Processes events asynchronously
    * Returns 2xx status quickly
  </Step>

  <Step title="Test Integration">
    Use the webhook simulator to send test events and verify your implementation
  </Step>
</Steps>

## Webhook Security

### Signature Verification

All webhooks include a `Stateset-Signature` header for verification:

<CodeGroup>
  ```javascript Node.js theme={null}
  const crypto = require('crypto');
  const winston = require('winston');

  const logger = winston.createLogger({
    level: process.env.LOG_LEVEL || 'info',
    transports: [new winston.transports.Console()]
  });

  class WebhookVerifier {
    constructor(secret) {
      this.secret = secret;
    }
    
    verify(payload, signatureHeader) {
      // Parse signature header
      // Format: "t=timestamp v1=signature1 v1=signature2"
      const elements = signatureHeader.split(' ');
      const timestamp = elements.find(e => e.startsWith('t=')).slice(2);
      const signatures = elements
        .filter(e => e.startsWith('v1='))
        .map(e => e.slice(3));
      
      // Check timestamp is recent (5 minute window)
      const currentTime = Math.floor(Date.now() / 1000);
      const timestampAge = currentTime - parseInt(timestamp);
      
      if (timestampAge > 300) {
        throw new Error('Webhook timestamp too old');
      }
      
      if (timestampAge < -300) {
        throw new Error('Webhook timestamp too far in future');
      }
      
      // Compute expected signature
      const signedPayload = `${timestamp}.${payload}`;
      const expectedSignature = crypto
        .createHmac('sha256', this.secret)
        .update(signedPayload)
        .digest('hex');
      
      // Timing-safe comparison
      const valid = signatures.some(sig => 
        crypto.timingSafeEqual(
          Buffer.from(sig),
          Buffer.from(expectedSignature)
        )
      );
      
      if (!valid) {
        throw new Error('Invalid webhook signature');
      }
      
      return {
        timestamp: parseInt(timestamp),
        verified: true
      };
    }
  }

  // Express.js implementation
  app.post('/webhooks/stateset',
    express.raw({ type: 'application/json' }),
    (req, res) => {
      const verifier = new WebhookVerifier(process.env.WEBHOOK_SECRET);
      
      try {
        verifier.verify(
          req.body.toString(),
          req.headers['stateset-signature']
        );
        
        const event = JSON.parse(req.body);
        processWebhookEvent(event);
        
        res.status(200).json({ received: true });
      } catch (error) {
        logger.error('Webhook verification failed', { message: error.message });
        res.status(401).json({ error: 'Verification failed' });
      }
    }
  );
  ```

  ```python Python theme={null}
  import hmac
  import hashlib
  import time
  import json

  class WebhookVerifier:
      def __init__(self, secret):
          self.secret = secret
      
      def verify(self, payload, signature_header):
          # Parse signature header
          elements = signature_header.split(' ')
          timestamp = next(e[2:] for e in elements if e.startswith('t='))
          signatures = [e[3:] for e in elements if e.startswith('v1=')]
          
          # Check timestamp is recent (5 minute window)
          current_time = int(time.time())
          timestamp_age = current_time - int(timestamp)
          
          if timestamp_age > 300:
              raise ValueError('Webhook timestamp too old')
          
          if timestamp_age < -300:
              raise ValueError('Webhook timestamp too far in future')
          
          # Compute expected signature
          signed_payload = f"{timestamp}.{payload}"
          expected_signature = hmac.new(
              self.secret.encode(),
              signed_payload.encode(),
              hashlib.sha256
          ).hexdigest()
          
          # Verify signature
          if expected_signature not in signatures:
              raise ValueError('Invalid webhook signature')
          
          return {
              'timestamp': int(timestamp),
              'verified': True
          }

  # Flask implementation
  from flask import Flask, request, jsonify

  app = Flask(__name__)
  verifier = WebhookVerifier(os.getenv('WEBHOOK_SECRET'))

  @app.route('/webhooks/stateset', methods=['POST'])
  def handle_webhook():
      try:
          payload = request.get_data(as_text=True)
          signature = request.headers.get('Stateset-Signature')
          
          verifier.verify(payload, signature)
          
          event = json.loads(payload)
          process_webhook_event(event)
          
          return jsonify({'received': True}), 200
      except Exception as e:
          print(f'Webhook verification failed: {e}')
          return jsonify({'error': 'Verification failed'}), 401
  ```
</CodeGroup>

### Security Best Practices

<Warning>
  **Critical Security Requirements:**

  * Always verify webhook signatures
  * Use HTTPS endpoints only
  * Store signing secrets securely
  * Implement idempotency to handle duplicate events
  * Process events asynchronously to avoid timeouts
</Warning>

## Webhook Payload Structure

All webhook events follow a consistent structure:

```json theme={null}
{
  "id": "evt_1NXWPnCo6bFb1KQto6C8OWvE",
  "object": "event",
  "api_version": "2024-01-01",
  "created": 1704067200,
  "type": "order.created",
  "data": {
    "object": {
      // Full resource object
    },
    "previous_attributes": {
      // For update events, previous values of changed fields
    }
  },
  "request": {
    "id": "req_abc123",
    "idempotency_key": "order-123"
  },
  "metadata": {
    "workspace_id": "ws_123",
    "user_id": "usr_456",
    "source": "api"
  }
}
```

### Payload Fields

| Field                      | Type    | Description                         |
| -------------------------- | ------- | ----------------------------------- |
| `id`                       | string  | Unique event identifier             |
| `object`                   | string  | Always "event"                      |
| `api_version`              | string  | API version used for the event      |
| `created`                  | integer | Unix timestamp of event creation    |
| `type`                     | string  | Event type (e.g., "order.created")  |
| `data.object`              | object  | Full resource data                  |
| `data.previous_attributes` | object  | Changed fields (update events only) |
| `request.id`               | string  | Original request ID                 |
| `request.idempotency_key`  | string  | Idempotency key if provided         |
| `metadata`                 | object  | Additional context                  |

## Event Types

### Order Events

<AccordionGroup>
  <Accordion title="order.created">
    **Triggered when:** A new order is created

    ```json theme={null}
    {
      "type": "order.created",
      "data": {
        "object": {
          "id": "ord_123",
          "object": "order",
          "status": "pending",
          "customer": {...},
          "items": [...],
          "totals": {...},
          "created_at": "2024-01-15T10:30:00Z"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="order.updated">
    **Triggered when:** Order details are modified

    ```json theme={null}
    {
      "type": "order.updated",
      "data": {
        "object": {
          "id": "ord_123",
          "status": "processing",
          // Full updated order
        },
        "previous_attributes": {
          "status": "pending",
          "updated_at": "2024-01-15T10:00:00Z"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="order.cancelled">
    **Triggered when:** Order is cancelled

    ```json theme={null}
    {
      "type": "order.cancelled",
      "data": {
        "object": {
          "id": "ord_123",
          "status": "cancelled",
          "cancelled_at": "2024-01-15T11:00:00Z",
          "cancellation_reason": "customer_request"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="order.fulfilled">
    **Triggered when:** Order fulfillment is complete

    ```json theme={null}
    {
      "type": "order.fulfilled",
      "data": {
        "object": {
          "id": "ord_123",
          "status": "fulfilled",
          "fulfillment": {
            "tracking_number": "1Z999AA10123456784",
            "carrier": "ups",
            "shipped_at": "2024-01-15T14:00:00Z"
          }
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

### Return Events

<AccordionGroup>
  <Accordion title="return.created">
    **Triggered when:** Return is initiated

    ```json theme={null}
    {
      "type": "return.created",
      "data": {
        "object": {
          "id": "ret_456",
          "order_id": "ord_123",
          "status": "pending",
          "items": [...],
          "reason": "defective",
          "rma_number": "RMA-2024-001"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="return.approved">
    **Triggered when:** Return is approved

    ```json theme={null}
    {
      "type": "return.approved",
      "data": {
        "object": {
          "id": "ret_456",
          "status": "approved",
          "approved_at": "2024-01-16T09:00:00Z",
          "shipping_label": {
            "carrier": "fedex",
            "tracking_number": "123456789",
            "label_url": "https://..."
          }
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="return.received">
    **Triggered when:** Returned items are received

    ```json theme={null}
    {
      "type": "return.received",
      "data": {
        "object": {
          "id": "ret_456",
          "status": "received",
          "received_at": "2024-01-20T10:00:00Z",
          "inspection": {
            "condition": "good",
            "notes": "Minor wear, acceptable for resale"
          }
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

### Customer Events

<AccordionGroup>
  <Accordion title="customer.created">
    **Triggered when:** New customer registers

    ```json theme={null}
    {
      "type": "customer.created",
      "data": {
        "object": {
          "id": "cus_789",
          "email": "customer@example.com",
          "first_name": "John",
          "last_name": "Doe",
          "created_at": "2024-01-15T08:00:00Z"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="customer.updated">
    **Triggered when:** Customer profile is updated

    ```json theme={null}
    {
      "type": "customer.updated",
      "data": {
        "object": {
          "id": "cus_789",
          "email": "newemail@example.com",
          // Full updated customer
        },
        "previous_attributes": {
          "email": "oldemail@example.com",
          "updated_at": "2024-01-14T10:00:00Z"
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

### Inventory Events

<AccordionGroup>
  <Accordion title="inventory.low_stock">
    **Triggered when:** Stock falls below threshold

    ```json theme={null}
    {
      "type": "inventory.low_stock",
      "data": {
        "object": {
          "sku": "WIDGET-001",
          "available": 5,
          "threshold": 10,
          "warehouse_id": "wh_123"
        }
      }
    }
    ```
  </Accordion>

  <Accordion title="inventory.out_of_stock">
    **Triggered when:** Item goes out of stock

    ```json theme={null}
    {
      "type": "inventory.out_of_stock",
      "data": {
        "object": {
          "sku": "WIDGET-001",
          "available": 0,
          "backorder_enabled": true,
          "restock_date": "2024-01-25T00:00:00Z"
        }
      }
    }
    ```
  </Accordion>
</AccordionGroup>

## Handling Webhooks

### Best Practices Implementation

```javascript theme={null}
const winston = require('winston');

const logger = winston.createLogger({
  level: process.env.LOG_LEVEL || 'info',
  transports: [new winston.transports.Console()]
});

class WebhookProcessor {
  constructor() {
    this.queue = new Queue('webhooks');
    this.processed = new Set();
  }
  
  async handle(event) {
    // 1. Check for duplicate processing
    if (this.processed.has(event.id)) {
      logger.info('Event already processed', { eventId: event.id });
      return { status: 'duplicate' };
    }
    
    // 2. Add to processing set
    this.processed.add(event.id);
    
    // 3. Queue for async processing
    await this.queue.add('process-webhook', {
      event,
      receivedAt: new Date().toISOString()
    });
    
    // 4. Return quickly
    return { status: 'queued' };
  }
  
  async process(job) {
    const { event } = job.data;
    
    try {
      // 5. Route to appropriate handler
      const handler = this.getHandler(event.type);
      if (!handler) {
        logger.warn('No handler for event type', { eventType: event.type });
        return;
      }
      
      // 6. Process with retry logic
      await this.withRetry(() => handler(event.data.object));
      
      // 7. Mark as completed
      await this.markProcessed(event.id);
      
    } catch (error) {
      // 8. Handle errors
      logger.error('Failed to process webhook', {
        eventId: event.id,
        message: error.message
      });
      
      // 9. Retry or dead letter
      if (job.attemptsMade < 3) {
        throw error; // Retry
      } else {
        await this.sendToDeadLetter(event, error);
      }
    }
  }
  
  getHandler(eventType) {
    const handlers = {
      'order.created': this.handleOrderCreated,
      'order.updated': this.handleOrderUpdated,
      'order.cancelled': this.handleOrderCancelled,
      'return.created': this.handleReturnCreated,
      'customer.created': this.handleCustomerCreated,
      'inventory.low_stock': this.handleLowStock,
      // Add more handlers
    };
    
    return handlers[eventType];
  }
  
  async handleOrderCreated(order) {
    // Send confirmation email
    await emailService.sendOrderConfirmation(order);
    
    // Update inventory
    await inventoryService.allocate(order.items);
    
    // Sync with ERP
    await erpService.createOrder(order);
    
    // Analytics
    await analytics.track('order_created', {
      order_id: order.id,
      value: order.totals.total,
      customer_id: order.customer.id
    });
  }
  
  async handleReturnCreated(return) {
    // Generate return label
    const label = await shippingService.createReturnLabel(return);
    
    // Email label to customer
    await emailService.sendReturnLabel(return, label);
    
    // Create support ticket
    await supportService.createTicket({
      type: 'return',
      return_id: return.id,
      customer_email: return.customer_email
    });
  }
  
  async withRetry(fn, maxRetries = 3) {
    for (let i = 0; i < maxRetries; i++) {
      try {
        return await fn();
      } catch (error) {
        if (i === maxRetries - 1) throw error;
        await new Promise(r => setTimeout(r, 1000 * Math.pow(2, i)));
      }
    }
  }
}
```

### Idempotency

Ensure your webhook handler is idempotent to safely handle duplicate deliveries:

```javascript theme={null}
class IdempotentWebhookHandler {
  constructor(redis) {
    this.redis = redis;
  }
  
  async handle(event) {
    const key = `webhook:${event.id}`;
    const lockKey = `${key}:lock`;
    
    // Try to acquire lock
    const acquired = await this.redis.set(
      lockKey,
      '1',
      'NX',
      'EX',
      30 // 30 second lock
    );
    
    if (!acquired) {
      // Another process is handling this event
      return { status: 'processing' };
    }
    
    try {
      // Check if already processed
      const processed = await this.redis.get(key);
      if (processed) {
        return JSON.parse(processed);
      }
      
      // Process event
      const result = await this.processEvent(event);
      
      // Cache result
      await this.redis.setex(
        key,
        86400, // 24 hour TTL
        JSON.stringify(result)
      );
      
      return result;
      
    } finally {
      // Release lock
      await this.redis.del(lockKey);
    }
  }
}
```

## Retry Logic

StateSet automatically retries failed webhook deliveries with exponential backoff:

### Retry Schedule

| Attempt | Delay      | Total Time   |
| ------- | ---------- | ------------ |
| 1       | Immediate  | 0 seconds    |
| 2       | 10 seconds | 10 seconds   |
| 3       | 1 minute   | 1.2 minutes  |
| 4       | 5 minutes  | 6.2 minutes  |
| 5       | 30 minutes | 36.2 minutes |
| 6       | 2 hours    | 2.6 hours    |
| 7       | 6 hours    | 8.6 hours    |
| 8       | 24 hours   | 32.6 hours   |

After 8 failed attempts, the webhook is marked as failed and won't be retried automatically.

### Handling Failures

Your endpoint should:

* Return `2xx` status for successful processing
* Return `4xx` for permanent failures (won't retry)
* Return `5xx` for temporary failures (will retry)

```javascript theme={null}
app.post('/webhook', async (req, res) => {
  try {
    await processWebhook(req.body);
    res.status(200).json({ success: true });
  } catch (error) {
    if (error.type === 'VALIDATION_ERROR') {
      // Permanent failure - don't retry
      res.status(400).json({ error: error.message });
    } else {
      // Temporary failure - retry
      res.status(500).json({ error: 'Processing failed' });
    }
  }
});
```

## Testing Webhooks

### Webhook Simulator

Test your webhook endpoint using our simulator:

```bash theme={null}
curl -X POST https://api.stateset.com/v1/webhooks/simulate \
  -H "Authorization: Bearer sk_test_..." \
  -H "Content-Type: application/json" \
  -d '{
    "event_type": "order.created",
    "endpoint_url": "https://your-app.com/webhooks",
    "custom_data": {
      "order_id": "test_123"
    }
  }'
```

### Local Development

Use ngrok or similar tools to test webhooks locally:

```bash theme={null}
# Start your local server
npm run dev

# In another terminal, create tunnel
ngrok http 3000

# Configure webhook endpoint in StateSet dashboard
# https://abc123.ngrok.io/webhooks
```

### Test Event Payloads

<CodeGroup>
  ```json Order Created Test theme={null}
  {
    "id": "evt_test_123",
    "type": "order.created",
    "data": {
      "object": {
        "id": "ord_test_123",
        "status": "pending",
        "customer": {
          "email": "test@example.com"
        },
        "items": [
          {
            "sku": "TEST-001",
            "quantity": 1,
            "price": 1000
          }
        ],
        "totals": {
          "subtotal": 1000,
          "tax": 80,
          "total": 1080
        }
      }
    }
  }
  ```

  ```json Return Created Test theme={null}
  {
    "id": "evt_test_456",
    "type": "return.created",
    "data": {
      "object": {
        "id": "ret_test_456",
        "order_id": "ord_test_123",
        "status": "pending",
        "items": [
          {
            "sku": "TEST-001",
            "quantity": 1,
            "reason": "defective"
          }
        ],
        "rma_number": "RMA-TEST-001"
      }
    }
  }
  ```
</CodeGroup>

## Webhook Management API

Programmatically manage webhooks:

```javascript theme={null}
// List webhook endpoints
const endpoints = await stateset.webhookEndpoints.list();

// Create webhook endpoint
const endpoint = await stateset.webhookEndpoints.create({
  url: 'https://your-app.com/webhooks',
  events: [
    'order.created',
    'order.updated',
    'return.created'
  ],
  description: 'Production webhook endpoint',
  metadata: {
    environment: 'production'
  }
});

// Update webhook endpoint
await stateset.webhookEndpoints.update(endpoint.id, {
  events: [...endpoint.events, 'customer.created']
});

// Delete webhook endpoint
await stateset.webhookEndpoints.delete(endpoint.id);

// Retrieve endpoint secret
const secret = await stateset.webhookEndpoints.getSecret(endpoint.id);
```

## Monitoring and Debugging

### Webhook Logs

View webhook delivery attempts in the dashboard:

```javascript theme={null}
// Assume a structured logger is available as `logger`
// Get webhook event logs
const logs = await stateset.webhookEvents.list({
  endpoint_id: 'we_123',
  limit: 100
});

logs.data.forEach(log => {
  logger.info('Webhook event log', {
    event_id: log.event_id,
    status: log.status,
    attempts: log.attempts,
    last_error: log.last_error,
    next_retry: log.next_retry_at
  });
});

// Retry failed webhook
await stateset.webhookEvents.retry('evt_failed_123');
```

### Metrics and Alerts

Monitor webhook health:

```javascript theme={null}
class WebhookMonitor {
  trackDelivery(event, success, duration) {
    metrics.increment('webhooks.delivered', {
      event_type: event.type,
      success: success
    });
    
    metrics.histogram('webhooks.duration', duration, {
      event_type: event.type
    });
    
    if (!success) {
      this.alertOnFailure(event);
    }
  }
  
  alertOnFailure(event) {
    if (this.getFailureRate() > 0.05) { // 5% failure rate
      alerts.send({
        severity: 'high',
        message: 'High webhook failure rate detected',
        details: {
          rate: this.getFailureRate(),
          event_type: event.type
        }
      });
    }
  }
}
```

## FAQ

<AccordionGroup>
  <Accordion title="How do I handle out-of-order events?">
    Events may arrive out of order. Use the `created` timestamp and resource state to handle this:

    ```javascript theme={null}
    if (event.created < lastProcessedTimestamp) {
      // This is an old event, check if it should be processed
      const currentResource = await stateset.orders.get(event.data.object.id);
      if (currentResource.updated_at > event.created) {
        // Skip this event, we have newer data
        return;
      }
    }
    ```
  </Accordion>

  <Accordion title="What happens if my endpoint is down?">
    StateSet will retry failed deliveries for up to 3 days with exponential backoff. You can also:

    * Manually retry failed events from the dashboard
    * Use the Event API to fetch missed events
    * Implement webhook replay for recovery
  </Accordion>

  <Accordion title="Can I filter events by metadata?">
    Yes, you can configure webhook endpoints to filter events based on metadata:

    ```javascript theme={null}
    const endpoint = await stateset.webhookEndpoints.create({
      url: 'https://your-app.com/webhooks',
      events: ['order.created'],
      filters: {
        'metadata.channel': 'online',
        'metadata.region': 'us-west'
      }
    });
    ```
  </Accordion>

  <Accordion title="How do I test webhook signature verification?">
    Use our test signature generator:

    ```bash theme={null}
    curl https://api.stateset.com/v1/webhooks/test-signature \
      -H "Authorization: Bearer sk_test_..." \
      -d payload='{"test": true}' \
      -d secret='whsec_test_...'
    ```
  </Accordion>
</AccordionGroup>

## Related Resources

* [Webhook Events Reference](/api-reference/events) - Complete list of events
* [Security Best Practices](/api-reference/security) - Security guidelines
* [API Error Handling](/api-reference/errors) - Error codes and handling
* [Event Replay API](/api-reference/event-replay) - Replay missed events

***

**Need help?** Contact [api-support@stateset.com](mailto:api-support@stateset.com) or visit our [Discord community](https://discord.gg/VfcaqgZywq).
