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 TypeDescription
instance.createdA new process instance has been started.
instance.completedA process instance has reached all end events and completed successfully.
instance.terminatedA process instance was manually terminated.
incident.createdA new incident was created (job retries exhausted or FEEL error).
incident.resolvedAn incident was resolved (manually or via retry).
job.createdA new job was created (service task reached).
job.completedA job was completed by a worker.
job.failedA worker reported a job failure.
task.createdA user task was created and is awaiting assignment.
task.completedA user task was completed.
message.correlatedAn incoming message was successfully correlated to a waiting process instance.
credits.lowAccount credit balance dropped below 50 credits.
credits.exhaustedAccount 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

  1. Priostack signs the raw JSON body using your webhook secret (set in Console → Settings → Webhooks).
  2. The signature is HMAC-SHA256(webhookSecret, rawBody), hex-encoded.
  3. 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:

AttemptDelay
1st retry30 seconds
2nd retry5 minutes
3rd retry30 minutes
After 3 failed retriesEvent 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.