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"
}
}
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
| Event | Description |
|---|---|
instance.started | A new process instance was started |
instance.completed | A process instance reached an end event |
instance.suspended | A process instance was suspended |
instance.terminated | A process instance was terminated |
instance.error | A process instance encountered an unhandled error |
task.created | A human task was created (process is waiting) |
task.claimed | A human task was claimed by a user |
task.completed | A human task was completed |
task.overdue | A human task passed its due date without completion |
timer.fired | A 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 });
});
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:
| Attempt | Delay | Cumulative |
|---|---|---|
| 1 | Immediate | 0s |
| 2 | 10s | 10s |
| 3 | 30s | 40s |
| 4 | 2 min | ~2.5 min |
| 5 | 5 min | ~7.5 min |
| 6 | 15 min | ~22 min |
| 7 | 1 hour | ~1.2 hours |
| 8 | 4 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:
| Status | Meaning |
|---|---|
pending | Queued for delivery |
success | Endpoint returned 2xx |
failed | All 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 OKquickly — 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— useGET /monitoring/summaryto track delivery failures; a growing count indicates a consistently unreachable endpoint