How It Works 1 Create a webhook endpoint Set up a POST endpoint on your server that accepts JSON.
2 Register in Flow80 Add your URL and select events in Settings → Webhooks.
3 We send events Flow80 POSTs to your URL whenever matching events occur.
4 Verify & respond Validate the HMAC-SHA256 signature, process the event, return 2xx.
Available Events flow.started Fired when a workflow execution begins.
flow.completed Fired when a workflow finishes successfully.
flow.failed Fired when a workflow errors or times out.
flow.step_completed Fired after each individual step finishes.
flow.step_failed Fired when a specific step errors.
webhook.delivered Fired when Flow80 receives an inbound webhook.
webhook.received Fired when an inbound webhook is received and accepted.
Payload Format Every webhook POST includes this structure:
1 { 2 "event": "flow.completed", 3 "flow_id": "wf_xxxxxxxxxxxxxxxx", 4 "run_id": "run_yyyyyyyyyyyyyyyy", 5 "timestamp": "2026-04-08T10:23:45Z", 6 "payload": { 7 "order_id": "ORD-12345", 8 "status": "completed", 9 "steps_executed": 4, 10 "duration_ms": 1243 11 } 12 } Signature Verification Every outbound webhook includes an X-Flow80-Signature header — the HMAC-SHA256 hex digest of the raw request body, signed with your webhook secret. Always verify server-side.
Never skip verification. Without HMAC verification, any actor who discovers your webhook URL could send fake events to your system.
Verify webhook signature
PHP Python Node.js Ruby Go cURL
1 <?php 2 // Flow80 — verify inbound webhook signature (HMAC-SHA256) 3 function verify_flow80_signature( 4 string $payload, 5 string $signature, 6 string $secret 7 ): bool { 8 $expected = hash_hmac('sha256', $payload, $secret); 9 return hash_equals($expected, $signature); 10 } 11 12 // In your webhook handler: 13 $raw_body = file_get_contents('php://input'); 14 $signature = $headers['X-Flow80-Signature'] ?? ''; 15 $webhook_secret = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx'; 16 17 if (!verify_flow80_signature($raw_body, $signature, $webhook_secret)) { 18 http_response_code(401); 19 exit('Invalid signature'); 20 } 21 22 $data = json_decode($raw_body, true); 23 // → $data['event'], $data['flow_id'], $data['run_id'], $data['timestamp'] 1 import hmac 2 import hashlib 3 import json 4 from flask import request, abort 5 6 def verify_signature(payload: bytes, signature: str, secret: str) -> bool: 7 expected = hmac.new(secret.encode(), payload, hashlib.sha256).hexdigest() 8 return hmac.compare_digest(expected, signature) 9 10 @app.route('/webhooks/flow80', methods=['POST']) 11 def inbound_webhook(): 12 sig = request.headers.get('X-Flow80-Signature', '') 13 secret = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 14 15 if not verify_signature(request.data, sig, secret): 16 abort(401) 17 18 data = request.get_json() 19 event = data.get('event') 20 flow_id = data.get('flow_id') 21 run_id = data.get('run_id') 22 # handle the event ... 23 return '', 200 1 import crypto from 'crypto'; 2 3 function verifySignature(payload: Buffer, signature: string, secret: string): boolean { 4 const expected = crypto 5 .createHmac('sha256', secret) 6 .update(payload) 7 .digest('hex'); 8 return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(signature)); 9 } 10 11 app.post('/webhooks/flow80', (req, res) => { 12 const sig = req.headers['x-flow80-signature'] ?? ''; 13 const secret = process.env.FLOW80_WEBHOOK_SECRET!; 14 15 // req.rawBody must be a Buffer — ensure your Express parser preserves it 16 if (!verifySignature(req.rawBody, sig, secret)) { 17 return res.status(401).send('Invalid signature'); 18 } 19 20 const { event, flow_id, run_id, payload } = req.body; 21 // handle event ... 22 res.sendStatus(200); 23 }); 1 # Ruby — verify webhook signature 2 require 'openssl' 3 require 'json' 4 5 def verify_signature(payload, signature, secret) 6 expected = OpenSSL::HMAC.hexdigest('SHA256', secret, payload) 7 ActiveSupport::SecurityUtils.secure_compare(expected, signature) 8 end 9 10 # In your webhook controller: 11 raw_body = request.body.read 12 signature = request.headers['X-Flow80-Signature'] 13 webhook_secret = 'whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx' 14 15 unless verify_signature(raw_body, signature, webhook_secret) 16 head :unauthorized 17 end 18 19 data = JSON.parse(raw_body) 1 package main 2 3 import ( 4 "crypto/hmac" 5 "crypto/sha256" 6 "encoding/hex" 7 "io" 8 "net/http" 9 ) 10 11 func verifySignature(payload []byte, signature, secret string) bool { 12 mac := hmac.New(sha256.New, []byte(secret)) 13 mac.Write(payload) 14 expected := hex.EncodeToString(mac.Sum(nil)) 15 return hmac.Equal([]byte(expected), []byte(signature)) 16 } 17 18 func webhookHandler(w http.ResponseWriter, r *http.Request) { 19 body, _ := io.ReadAll(r.Body) 20 sig := r.Header.Get("X-Flow80-Signature") 21 if !verifySignature(body, sig, "whsec_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx") { 22 http.Error(w, "invalid signature", 401) 23 return 24 } 25 // parse JSON from body ... 26 } 1 # Signature verification is server-side only. 2 # To inspect a test webhook locally: 3 curl -X POST "https://your-app.com/webhooks/flow80" \ 4 -H "Content-Type: application/json" \ 5 -H "X-Flow80-Signature: <your_webhook_secret>" \ 6 -d '{"event":"flow.completed","flow_id":"wf_xxx","run_id":"run_yyy","timestamp":"2026-04-08T10:00:00Z","payload":{}}' Retry Behavior If your endpoint doesn't return a 2xx within 10 seconds, Flow80 retries with exponential backoff:
2xxAcknowledged within 10 seconds
4xxNot retried. Fix your request payload.
5xx / timeoutExponential backoff: 30s → 2m → 10m → 1h
Max attempts4 total attempts. Marked as failed after final attempt.
Failed deliveries appear in Settings → Webhooks with error details and the ability to replay individual events.