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

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

1

Create Webhook Endpoint

Navigate to Dashboard → Settings → Webhooks and click Add Endpoint
2

Configure Endpoint

  • Enter your HTTPS endpoint URL
  • Select events to subscribe to
  • Copy the signing secret for verification
3

Implement Handler

Create an endpoint that:
  • Accepts POST requests
  • Verifies signatures
  • Processes events asynchronously
  • Returns 2xx status quickly
4

Test Integration

Use the webhook simulator to send test events and verify your implementation

Webhook Security

Signature Verification

All webhooks include a Stateset-Signature header for verification:
const crypto = require('crypto');

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) {
      console.error('Webhook verification failed:', error);
      res.status(401).json({ error: 'Verification failed' });
    }
  }
);

Security Best Practices

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

Webhook Payload Structure

All webhook events follow a consistent structure:
{
  "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

FieldTypeDescription
idstringUnique event identifier
objectstringAlways “event”
api_versionstringAPI version used for the event
createdintegerUnix timestamp of event creation
typestringEvent type (e.g., “order.created”)
data.objectobjectFull resource data
data.previous_attributesobjectChanged fields (update events only)
request.idstringOriginal request ID
request.idempotency_keystringIdempotency key if provided
metadataobjectAdditional context

Event Types

Order Events

Return Events

Customer Events

Inventory Events

Handling Webhooks

Best Practices Implementation

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)) {
      console.log(`Event ${event.id} already processed`);
      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) {
        console.warn(`No handler for event type: ${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
      console.error(`Failed to process webhook ${event.id}:`, error);
      
      // 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:
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

AttemptDelayTotal Time
1Immediate0 seconds
210 seconds10 seconds
31 minute1.2 minutes
45 minutes6.2 minutes
530 minutes36.2 minutes
62 hours2.6 hours
76 hours8.6 hours
824 hours32.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)
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:
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:
# 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

{
  "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
      }
    }
  }
}

Webhook Management API

Programmatically manage webhooks:
// 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:
// Get webhook event logs
const logs = await stateset.webhookEvents.list({
  endpoint_id: 'we_123',
  limit: 100
});

logs.data.forEach(log => {
  console.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:
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


Need help? Contact api-support@stateset.com or visit our Discord community.