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
Create Webhook Endpoint
Navigate to Dashboard → Settings → Webhooks and click Add Endpoint
Configure Endpoint
Enter your HTTPS endpoint URL
Select events to subscribe to
Copy the signing secret for verification
Implement Handler
Create an endpoint that:
Accepts POST requests
Verifies signatures
Processes events asynchronously
Returns 2xx status quickly
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
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
Triggered when: A new order is created{
"type" : "order.created" ,
"data" : {
"object" : {
"id" : "ord_123" ,
"object" : "order" ,
"status" : "pending" ,
"customer" : { ... },
"items" : [ ... ],
"totals" : { ... },
"created_at" : "2024-01-15T10:30:00Z"
}
}
}
Triggered when: Order details are modified{
"type" : "order.updated" ,
"data" : {
"object" : {
"id" : "ord_123" ,
"status" : "processing" ,
// Full updated order
},
"previous_attributes" : {
"status" : "pending" ,
"updated_at" : "2024-01-15T10:00:00Z"
}
}
}
Triggered when: Order is cancelled{
"type" : "order.cancelled" ,
"data" : {
"object" : {
"id" : "ord_123" ,
"status" : "cancelled" ,
"cancelled_at" : "2024-01-15T11:00:00Z" ,
"cancellation_reason" : "customer_request"
}
}
}
Triggered when: Order fulfillment is complete{
"type" : "order.fulfilled" ,
"data" : {
"object" : {
"id" : "ord_123" ,
"status" : "fulfilled" ,
"fulfillment" : {
"tracking_number" : "1Z999AA10123456784" ,
"carrier" : "ups" ,
"shipped_at" : "2024-01-15T14:00:00Z"
}
}
}
}
Return Events
Triggered when: Return is initiated{
"type" : "return.created" ,
"data" : {
"object" : {
"id" : "ret_456" ,
"order_id" : "ord_123" ,
"status" : "pending" ,
"items" : [ ... ],
"reason" : "defective" ,
"rma_number" : "RMA-2024-001"
}
}
}
Triggered when: Return is approved{
"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://..."
}
}
}
}
Triggered when: Returned items are received{
"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"
}
}
}
}
Customer Events
Triggered when: New customer registers{
"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"
}
}
}
Triggered when: Customer profile is updated{
"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"
}
}
}
Inventory Events
Triggered when: Stock falls below threshold{
"type" : "inventory.low_stock" ,
"data" : {
"object" : {
"sku" : "WIDGET-001" ,
"available" : 5 ,
"threshold" : 10 ,
"warehouse_id" : "wh_123"
}
}
}
Triggered when: Item goes out of stock{
"type" : "inventory.out_of_stock" ,
"data" : {
"object" : {
"sku" : "WIDGET-001" ,
"available" : 0 ,
"backorder_enabled" : true ,
"restock_date" : "2024-01-25T00:00:00Z"
}
}
}
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
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)
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
Order Created Test
Return Created Test
{
"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
How do I handle out-of-order events?
Events may arrive out of order. Use the created
timestamp and resource state to handle this: 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 ;
}
}
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
Can I filter events by metadata?
How do I test webhook signature verification?
Use our test signature generator: curl https://api.stateset.com/v1/webhooks/test-signature \
-H "Authorization: Bearer sk_test_..." \
-d payload='{"test": true}' \
-d secret='whsec_test_...'
Need help? Contact api-support@stateset.com or visit our Discord community .