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