Webhooks
Priostack can send HTTP POST notifications to your endpoint when process events occur. Webhooks allow you to react to workflow events in real-time without polling the API.
Event Types
| Event Type | Description |
|---|---|
instance.created | A new process instance has been started. |
instance.completed | A process instance has reached all end events and completed successfully. |
instance.terminated | A process instance was manually terminated. |
incident.created | A new incident was created (job retries exhausted or FEEL error). |
incident.resolved | An incident was resolved (manually or via retry). |
job.created | A new job was created (service task reached). |
job.completed | A job was completed by a worker. |
job.failed | A worker reported a job failure. |
task.created | A user task was created and is awaiting assignment. |
task.completed | A user task was completed. |
message.correlated | An incoming message was successfully correlated to a waiting process instance. |
credits.low | Account credit balance dropped below 50 credits. |
credits.exhausted | Account credit balance reached zero. |
Payload Format
Webhook payloads are JSON objects with the following structure:
{
"event": "instance.completed",
"id": "evt_a1b2c3d4e5f6",
"timestamp": "2026-05-01T14:30:00.123Z",
"data": {
"instanceKey": 2251799813685281,
"processDefinitionKey": 2251799813685249,
"bpmnProcessId": "order-processing",
"version": 1,
"state": "COMPLETED",
"variables": {
"orderId": "ORD-001",
"amount": 149.99,
"approved": true
}
}
}
Incident Event
{
"event": "incident.created",
"id": "evt_x9y8z7",
"timestamp": "2026-05-01T15:00:00Z",
"data": {
"incidentKey": 2251799813685300,
"instanceKey": 2251799813685281,
"jobKey": 2251799813685290,
"errorType": "JOB_NO_RETRIES",
"errorMessage": "Payment gateway timeout after 3 retries",
"elementId": "processPayment",
"elementInstanceKey": 2251799813685295
}
}
Signature Verification
Every webhook request includes an X-Priostack-Signature header containing an HMAC-SHA256 signature of the raw request body. Always verify this signature before processing the event.
How Signing Works
- Priostack signs the raw JSON body using your webhook secret (set in Console → Settings → Webhooks).
- The signature is
HMAC-SHA256(webhookSecret, rawBody), hex-encoded. - Your server must verify this before trusting the payload.
Verify in Go
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
)
func verifyWebhook(r *http.Request, secret []byte) ([]byte, bool) {
body, err := io.ReadAll(r.Body)
if err != nil {
return nil, false
}
sig := r.Header.Get("X-Priostack-Signature")
mac := hmac.New(sha256.New, secret)
mac.Write(body)
expected := hex.EncodeToString(mac.Sum(nil))
return body, hmac.Equal([]byte(sig), []byte(expected))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
secret := []byte(os.Getenv("PRIOSTACK_WEBHOOK_SECRET"))
body, ok := verifyWebhook(r, secret)
if !ok {
http.Error(w, "invalid signature", http.StatusUnauthorized)
return
}
var event map[string]interface{}
json.Unmarshal(body, &event)
// Process event...
w.WriteHeader(http.StatusOK)
}
Verify in Python
import hashlib
import hmac
import os
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = os.environ["PRIOSTACK_WEBHOOK_SECRET"].encode()
@app.route("/webhook", methods=["POST"])
def webhook():
sig = request.headers.get("X-Priostack-Signature", "")
expected = hmac.new(WEBHOOK_SECRET, request.data, hashlib.sha256).hexdigest()
if not hmac.compare_digest(sig, expected):
abort(401, "Invalid signature")
event = request.get_json()
print(f"Received event: {event['event']} id={event['id']}")
return "", 200
Verify in JavaScript
import crypto from "crypto";
import express from "express";
const app = express();
const WEBHOOK_SECRET = process.env.PRIOSTACK_WEBHOOK_SECRET;
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const sig = req.headers["x-priostack-signature"];
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(req.body)
.digest("hex");
if (!crypto.timingSafeEqual(Buffer.from(sig), Buffer.from(expected))) {
return res.status(401).send("Invalid signature");
}
const event = JSON.parse(req.body);
console.log("Event:", event.event, "ID:", event.id);
res.status(200).send("OK");
});
Retry Behavior
If your endpoint returns a non-2xx status code or times out, Priostack will retry the delivery:
| Attempt | Delay |
|---|---|
| 1st retry | 30 seconds |
| 2nd retry | 5 minutes |
| 3rd retry | 30 minutes |
| After 3 failed retries | Event is marked as failed. Visible in Console → Webhooks. |
Idempotency
Because webhooks can be delivered more than once (retries, network issues), your handler must be idempotent. Use the event id field to deduplicate:
// Store processed event IDs (use Redis, DB, or in-memory set)
const processedEvents = new Set();
function handleEvent(event) {
if (processedEvents.has(event.id)) {
console.log("Duplicate event, skipping:", event.id);
return;
}
processedEvents.add(event.id);
// ... process event
}
Local Testing with ngrok
Use ngrok to expose your local server to the internet during development:
# 1. Start your local webhook server on port 3000
node webhook-server.js
# 2. In another terminal, start ngrok
ngrok http 3000
# 3. Copy the HTTPS URL from ngrok output, e.g.:
# https://abc123.ngrok.io
# 4. Set your webhook URL in Console → Settings → Webhooks:
# https://abc123.ngrok.io/webhook
# 5. Trigger a process instance to see the webhook fire
curl -X POST https://api.priostack.com/api/v1/process-instances \
-H "X-API-Key: your_key" \
-d '{"bpmnProcessId": "order-processing"}'
Timeout: Your webhook handler must respond within 10 seconds. Long-running work should be processed asynchronously — return 200 immediately and process in a background queue.