Skip to main content

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-Signature header for verification
  • JSON payload with event type and data
  • Retry logic (3 attempts with exponential backoff)

Your handler should:

  1. Verify the signature
  2. Respond with 200 OK quickly (within 5 seconds)
  3. 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

  1. Respond quickly - Return 200 OK within 5 seconds
  2. Process asynchronously - Use a queue for heavy processing
  3. Handle duplicates - Store event IDs to detect retries
  4. Log everything - Log signatures, events, and processing results
  5. Monitor failures - Set up alerts for webhook delivery failures

See Webhooks Reference for full documentation.