Skip to main content

Webhooks

Stateway sends outbound HTTP notifications (webhooks) to your endpoints when process events occur. Webhooks enable real-time integration with external systems without polling.

Creating a Webhook Subscription

Request:

curl -X POST https://api.stateway.io/v1/webhooks \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/webhooks/stateway",
"event_types": [
"instance.started",
"instance.completed",
"task.created",
"task.completed"
],
"secret": "your-webhook-secret-for-hmac"
}'

Response:

{
"data": {
"id": "wh_01j...",
"url": "https://your-app.com/webhooks/stateway",
"event_types": [
"instance.started",
"instance.completed",
"task.created",
"task.completed"
],
"is_active": true,
"created_at": "2026-04-26T12:00:00.000Z"
}
}
secret and headers accept plain text only

The secret and headers fields do not support {{credentials.*}} interpolation — values are stored and used as plain text. To rotate a webhook secret, update the subscription via PUT /v1/webhooks/{id}.

Scoping to a Definition

To receive events only for instances of a specific process definition, include definition_key:

Request:

curl -X POST https://api.stateway.io/v1/webhooks \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/expense",
"event_types": ["instance.completed", "instance.error"],
"definition_key": "expense-approval",
"secret": "your-secret"
}'

Response:

{
"data": {
"id": "wh_02k...",
"url": "https://your-app.com/hooks/expense",
"event_types": ["instance.completed", "instance.error"],
"definition_key": "expense-approval",
"is_active": true,
"created_at": "2026-04-26T12:01:00.000Z"
}
}

Without definition_key, the subscription receives events from all process definitions.

Custom Delivery Headers

Pass headers that Stateway should include in every outbound request (e.g., an authorization header for your endpoint):

Request:

curl -X POST https://api.stateway.io/v1/webhooks \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://your-app.com/hooks/stateway",
"event_types": ["instance.completed"],
"headers": { "X-Internal-Token": "secret123" }
}'

Response:

{
"data": {
"id": "wh_03l...",
"url": "https://your-app.com/hooks/stateway",
"event_types": ["instance.completed"],
"is_active": true,
"created_at": "2026-04-26T12:02:00.000Z"
}
}

Supported Events

EventDescription
instance.startedA new process instance was started
instance.completedA process instance reached an end event
instance.suspendedA process instance was suspended
instance.terminatedA process instance was terminated
instance.errorA process instance encountered an unhandled error
task.createdA human task was created (process is waiting)
task.claimedA human task was claimed by a user
task.completedA human task was completed
task.overdueA human task passed its due date without completion
timer.firedA timer event was triggered

Webhook Payload

Each delivery sends a JSON payload:

{
"event": "instance.completed",
"timestamp": "2026-04-26T14:30:00.000Z",
"data": {
"instance_id": "inst_01j...",
"definition_key": "expense-approval",
"status": "completed",
"variables": {
"amount": 1500,
"approved": true
}
}
}

HMAC Signature Verification

Every webhook delivery includes an HMAC-SHA256 signature in the X-Stateway-Signature header:

X-Stateway-Signature: sha256=a1b2c3d4...

To verify the signature on your end:

import crypto from 'crypto';

function verifyWebhook(payload, signature, secret) {
const expected = 'sha256=' + crypto
.createHmac('sha256', secret)
.update(payload)
.digest('hex');

return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}

// In your webhook handler:
app.post('/webhooks/stateway', (req, res) => {
const signature = req.headers['x-stateway-signature'];
const isValid = verifyWebhook(
JSON.stringify(req.body),
signature,
process.env.WEBHOOK_SECRET
);

if (!isValid) {
return res.status(401).json({ error: 'Invalid signature' });
}

// Process the event
console.log('Event:', req.body.event);
console.log('Data:', req.body.data);

res.status(200).json({ received: true });
});
warning

Always verify the HMAC signature before processing a webhook. This prevents attackers from sending fake events to your endpoint.

Retry Policy

Failed deliveries (non-2xx response or timeout) are retried with exponential backoff:

AttemptDelayCumulative
1Immediate0s
210s10s
330s40s
42 min~2.5 min
55 min~7.5 min
615 min~22 min
71 hour~1.2 hours
84 hours~5.2 hours

After 8 failed attempts, the delivery is marked as failed and no further retries are attempted.

Managing Webhooks

List Subscriptions

Request:

curl https://api.stateway.io/v1/webhooks \
-H "X-API-Key: sw_live_your_key"

Response:

{
"data": [
{
"id": "wh_01j...",
"url": "https://your-app.com/webhooks/stateway",
"event_types": ["instance.completed", "task.created"],
"is_active": true,
"created_at": "2026-04-26T12:00:00.000Z"
}
]
}

Get a Subscription

Request:

curl https://api.stateway.io/v1/webhooks/{webhook_id} \
-H "X-API-Key: sw_live_your_key"

Response:

{
"data": {
"id": "wh_01j...",
"url": "https://your-app.com/webhooks/stateway",
"event_types": ["instance.completed", "task.created"],
"is_active": true,
"created_at": "2026-04-26T12:00:00.000Z"
}
}

Update a Subscription

Request:

curl -X PUT https://api.stateway.io/v1/webhooks/{webhook_id} \
-H "X-API-Key: sw_live_your_key" \
-H "Content-Type: application/json" \
-d '{
"url": "https://new-endpoint.com/webhooks",
"event_types": ["instance.completed"],
"is_active": false
}'

Response:

{
"data": {
"id": "wh_01j...",
"url": "https://new-endpoint.com/webhooks",
"event_types": ["instance.completed"],
"is_active": false,
"created_at": "2026-04-26T12:00:00.000Z"
}
}

Only the fields you include are updated. You can use is_active: false to pause a subscription without deleting it.

Delete a Subscription

Request:

curl -X DELETE https://api.stateway.io/v1/webhooks/{webhook_id} \
-H "X-API-Key: sw_live_your_key"

Response:

{
"data": { "id": "wh_01j...", "deleted": true }
}

Deletion also removes the associated delivery history.

Test a Subscription

Send a test delivery to verify your endpoint is reachable:

Request:

curl -X POST https://api.stateway.io/v1/webhooks/{webhook_id}/test \
-H "X-API-Key: sw_live_your_key"

Response:

{
"data": {
"id": "del_01j...",
"event_type": "test",
"status": "pending",
"created_at": "2026-04-26T12:00:00.000Z"
}
}

List Deliveries

View delivery history for a subscription:

Request:

curl https://api.stateway.io/v1/webhooks/{webhook_id}/deliveries \
-H "X-API-Key: sw_live_your_key"

Response:

{
"data": [
{
"id": "del_01j...",
"event_type": "instance.completed",
"status": "success",
"created_at": "2026-04-26T14:30:01.000Z"
},
{
"id": "del_02k...",
"event_type": "instance.started",
"status": "failed",
"created_at": "2026-04-26T14:35:00.000Z"
}
]
}

Delivery status values:

StatusMeaning
pendingQueued for delivery
successEndpoint returned 2xx
failedAll retry attempts exhausted

Best Practices

  • Subscribe only to events you need — reduces noise and load on your endpoint
  • Use HTTPS endpoints — webhook payloads may contain sensitive process data
  • Always verify the HMAC signature — reject requests with invalid or missing signatures
  • Respond 200 OK quickly — process events asynchronously to avoid timeouts that trigger retries
  • Be idempotent — the same event may be delivered more than once due to retries
  • Monitor webhookDeliveries.failed — use GET /monitoring/summary to track delivery failures; a growing count indicates a consistently unreachable endpoint