Webhook Handler Templates
Copy-paste webhook handlers for popular frameworks. These templates handle signature verification, event parsing, and response handling.
Overview
Flow webhooks deliver events with:
Flow-Signatureheader for verification- JSON payload with event type and data
- Retry logic (3 attempts with exponential backoff)
Your handler should:
- Verify the signature
- Respond with 200 OK quickly (within 5 seconds)
- Process the event asynchronously if needed
TypeScript / Node.js (Express)
import express from 'express';
import crypto from 'crypto';
const app = express();
app.use(express.json());
const WEBHOOK_SECRET = process.env.FLOW_WEBHOOK_SECRET!;
// Signature verification
function verifySignature(payload: string, signature: string): boolean {
const parts = signature.split(',').reduce((acc, part) => {
const [key, value] = part.split('=');
return { ...acc, [key.trim()]: value };
}, {} as Record<string, string>);
const timestamp = parts['t'];
const sig = parts['v1'];
if (!timestamp || !sig) return false;
// Check timestamp is within 5 minutes
const timestampAge = Date.now() - parseInt(timestamp) * 1000;
if (timestampAge > 5 * 60 * 1000) return false;
const expectedSig = crypto
.createHmac('sha256', WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(sig),
Buffer.from(expectedSig)
);
}
app.post('/webhooks/flow', (req, res) => {
const signature = req.headers['flow-signature'] as string;
const rawBody = JSON.stringify(req.body);
// Verify signature
if (!verifySignature(rawBody, signature)) {
console.error('Invalid webhook signature');
return res.status(401).send('Invalid signature');
}
// Acknowledge receipt immediately
res.status(200).send('OK');
// Process event asynchronously
const { type, data, timestamp } = req.body;
switch (type) {
case 'post.created':
console.log('Post created:', data.postId);
// Handle post created
break;
case 'post.delivered':
console.log('Post delivered:', data.postId);
// Handle successful delivery
break;
case 'post.failed':
console.error('Post failed:', data.postId, data.error);
// Handle failure - maybe notify user or retry
break;
case 'post.blocked':
console.warn('Post blocked:', data.postId, data.reason);
// Handle blocked post
break;
default:
console.log('Unknown event type:', type);
}
});
app.listen(3000);
Python (Flask)
import os
import hmac
import hashlib
import time
from flask import Flask, request, jsonify
app = Flask(__name__)
WEBHOOK_SECRET = os.environ.get('FLOW_WEBHOOK_SECRET')
def verify_signature(payload: bytes, signature: str) -> bool:
"""Verify Flow webhook signature."""
parts = dict(part.split('=') for part in signature.split(','))
timestamp = parts.get('t')
sig = parts.get('v1')
if not timestamp or not sig:
return False
# Check timestamp is within 5 minutes
timestamp_age = time.time() - int(timestamp)
if timestamp_age > 5 * 60:
return False
expected_sig = hmac.new(
WEBHOOK_SECRET.encode(),
f"{timestamp}.{payload.decode()}".encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(sig, expected_sig)
@app.route('/webhooks/flow', methods=['POST'])
def handle_webhook():
signature = request.headers.get('Flow-Signature', '')
raw_body = request.get_data()
# Verify signature
if not verify_signature(raw_body, signature):
return jsonify({'error': 'Invalid signature'}), 401
event = request.json
event_type = event.get('type')
data = event.get('data', {})
if event_type == 'post.created':
print(f"Post created: {data.get('postId')}")
# Handle post created
elif event_type == 'post.delivered':
print(f"Post delivered: {data.get('postId')}")
# Handle successful delivery
elif event_type == 'post.failed':
print(f"Post failed: {data.get('postId')} - {data.get('error')}")
# Handle failure
elif event_type == 'post.blocked':
print(f"Post blocked: {data.get('postId')} - {data.get('reason')}")
# Handle blocked
return jsonify({'received': True}), 200
if __name__ == '__main__':
app.run(port=3000)
Go (net/http)
package main
import (
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"strconv"
"strings"
"time"
)
var webhookSecret = os.Getenv("FLOW_WEBHOOK_SECRET")
type WebhookEvent struct {
Type string `json:"type"`
Data map[string]interface{} `json:"data"`
Timestamp int64 `json:"timestamp"`
}
func verifySignature(payload []byte, signature string) bool {
parts := make(map[string]string)
for _, part := range strings.Split(signature, ",") {
kv := strings.SplitN(part, "=", 2)
if len(kv) == 2 {
parts[strings.TrimSpace(kv[0])] = kv[1]
}
}
timestamp := parts["t"]
sig := parts["v1"]
if timestamp == "" || sig == "" {
return false
}
// Check timestamp is within 5 minutes
ts, _ := strconv.ParseInt(timestamp, 10, 64)
if time.Now().Unix()-ts > 5*60 {
return false
}
// Compute expected signature
mac := hmac.New(sha256.New, []byte(webhookSecret))
mac.Write([]byte(fmt.Sprintf("%s.%s", timestamp, string(payload))))
expectedSig := hex.EncodeToString(mac.Sum(nil))
return hmac.Equal([]byte(sig), []byte(expectedSig))
}
func webhookHandler(w http.ResponseWriter, r *http.Request) {
signature := r.Header.Get("Flow-Signature")
body, err := io.ReadAll(r.Body)
if err != nil {
http.Error(w, "Failed to read body", http.StatusBadRequest)
return
}
// Verify signature
if !verifySignature(body, signature) {
http.Error(w, "Invalid signature", http.StatusUnauthorized)
return
}
// Parse event
var event WebhookEvent
if err := json.Unmarshal(body, &event); err != nil {
http.Error(w, "Invalid JSON", http.StatusBadRequest)
return
}
// Acknowledge receipt
w.WriteHeader(http.StatusOK)
w.Write([]byte("OK"))
// Process event
switch event.Type {
case "post.created":
fmt.Printf("Post created: %v\n", event.Data["postId"])
case "post.delivered":
fmt.Printf("Post delivered: %v\n", event.Data["postId"])
case "post.failed":
fmt.Printf("Post failed: %v - %v\n", event.Data["postId"], event.Data["error"])
case "post.blocked":
fmt.Printf("Post blocked: %v - %v\n", event.Data["postId"], event.Data["reason"])
default:
fmt.Printf("Unknown event: %s\n", event.Type)
}
}
func main() {
http.HandleFunc("/webhooks/flow", webhookHandler)
fmt.Println("Webhook server listening on :3000")
http.ListenAndServe(":3000", nil)
}
Testing Webhooks Locally
Using ngrok
# Start ngrok tunnel
ngrok http 3000
# Copy the HTTPS URL (e.g., https://abc123.ngrok.io)
# Use this as your webhook URL when creating the webhook
Using the Flow Test Endpoint
# Send a test event to your webhook
curl -X POST https://api.flowsocial.app/v1/webhooks/{id}/test \
-H "Authorization: Bearer flow_sk_live_..."
Best Practices
- Respond quickly - Return 200 OK within 5 seconds
- Process asynchronously - Use a queue for heavy processing
- Handle duplicates - Store event IDs to detect retries
- Log everything - Log signatures, events, and processing results
- Monitor failures - Set up alerts for webhook delivery failures
See Webhooks Reference for full documentation.