Webhooks are the backbone of event-driven architectures, and if you’re evaluating Filestack for your file handling infrastructure, understanding how webhooks work is crucial for building responsive applications. This refresher goes into a deep implementation to cover practical debugging strategies, common pitfalls, and architectural patterns that work in production environments.
Whether you’re migrating from another service or setting up webhooks for the first time, this guide provides battle-tested approaches for working with Filestack webhooks reliably.
Key Takeaways
- Filestack retries failed webhooks three times (at 5 minutes, 30 minutes, and 12 hours) before marking them as undelivered
- Idempotency is critical, as the same webhook may arrive multiple times due to retries or network issues
- Security validation uses HMAC-SHA256 with a timestamp and signature in the request headers
- Most webhook failures stem from parsing issues you must use the raw request body for validation
- The FS-Timestamp header differs from the timestamp in the payload and must be used for signature validation
Setting Up Webhooks in the Developer Portal
Before diving into code, you’ll need to configure your webhooks in the Filestack Developer Portal. Navigate to the Webhooks section where you can:
- Add your endpoint URL
- Generate a secret key for signature validation
- Select which events to subscribe to
- Test your endpoint with sample payloads
<img class="alignnone size-large wp-image-14404" src="https://blog.filestack.com/wp-content/uploads/2025/10/webhooks-1024x402.png" alt="" width="1024" height="402" />
Keep your secret key secure, treat it like a password. You’ll need it to validate incoming webhooks, and anyone with access to it could forge requests to your endpoint.
Understanding Webhook Event Types
Filestack triggers webhooks for six distinct events. Here’s how to think about each one in practice:
Upload Events: The Workhorse
This fires for every file that hits your application. In production, this is your highest-volume webhook:
Real-world pattern: Use this to update
{
"id": 30813791,
"action": "fp.upload",
"timestamp": 1521733983,
"text": {
"container": "filestack-uploads-persist-production",
"url": "https://cdn.filestackcontent.com/HANDLE",
"filename": "profile_image.jpg",
"client": "Computer",
"key": "user_123/uploads/abc123_profile_image.jpg",
"type": "image/jpeg",
"status": "Stored",
"size": 158584
}
}
your database immediately when users upload files. Store the handle, filename, and size in your application’s database so you don’t need to query Filestack’s API later.
Workflow Events: Complex Pipeline Results
Workflows are where Filestack shines for complex processing:
{
"id": 61380634,
"action": "fs.workflow",
"timestamp": 1548214263,
"text": {
"workflow": "687r07d2-5f84-44a0-b20b-1c29a1deb2ab",
"status": "Finished",
"results": {
"virus_detection_1548213592150": {
"data": {
"infected": false,
"infections_list": []
}
},
"resize_thumbnail": {
"url": "https://cdn.filestackcontent.com/THUMB_HANDLE",
"mimetype": "image/jpeg"
}
}
}
}
Pro tip: The task IDs in results include timestamps, making them unique even if you use the same task type multiple times in a workflow. Parse these results to update your UI or trigger downstream processes.
Webhook Reliability and Idempotency
Here’s the reality: webhooks can arrive multiple times. Network hiccups, load balancer restarts, or your server returning a 500 status all trigger retries. Your endpoint must handle duplicate webhooks gracefully.
The Retry Timeline
Filestack implements this retry sequence:
- First retry: 5 minutes after failure
- Second retry: 30 minutes after failure
- Final retry: 12 hours after failure
Critical insight: Use the webhook from flask import Flask, request, abort
from filestack import Client
import redis
import os
app = Flask(__name__)
redis_client = redis.Redis(host='localhost', port=6379, db=0)
@app.route('/webhook', methods=['POST'])
def webhook():
# Validate signature first
resp = Client.verify_webhook_signature(
os.environ['FILESTACK_SECRET'],
request.data,
dict(request.headers)
)
if not resp['valid']:
abort(403)
payload = request.json
webhook_id = str(payload['id'])
# Check if we've processed this webhook before
if redis_client.exists(f"webhook:{webhook_id}"):
print(f"Duplicate webhook {webhook_id} - already processed")
return '', 200 # Return success to prevent retries
# Process the webhook
process_upload(payload)
# Mark as processed (expire after 30 days)
redis_client.setex(f"webhook:{webhook_id}", 2592000, "1")
return '', 200
This pattern prevents processing the same upload twice, which could create duplicate database entries or trigger duplicate emails to users.
Security Validation: Getting It Right
Most webhook integration problems stem from incorrect signature validation. Here’s what trips people up.
The Raw Body Requirement
The signature is computed on the exact bytes Filestack sent. If you parse the JSON first, you’ve lost the original formatting:
// ❌ WRONG - This won't work
app.use(express.json());
app.post('/webhook', (req, res) => {
const body = JSON.stringify(req.body); // Wrong order, wrong spacing
// Signature validation will fail
});
// ✅ CORRECT - Capture raw body first
app.use(express.raw({ type: 'application/json' }));
app.post('/webhook', (req, res) => {
const rawBody = req.body; // This is a Buffer with exact bytes
const timestamp = req.headers['fs-timestamp'];
const signature = req.headers['fs-signature'];
const signingString = `${timestamp}.${rawBody}`;
const hash = crypto
.createHmac('sha256', process.env.WEBHOOK_SECRET)
.update(signingString)
.digest('hex');
if (hash !== signature) {
return res.status(403).send('Invalid signature');
}
// Now you can parse the JSON
const payload = JSON.parse(rawBody);
// Process webhook...
});
Node.js Complete Implementation
Here’s a production-ready Express endpoint with proper error handling:
const express = require('express');
const crypto = require('crypto');
const app = express();
// Middleware to capture raw body for signature validation
app.use('/webhook', express.raw({ type: 'application/json' }));
app.post('/webhook', async (req, res) => {
const timestamp = req.headers['fs-timestamp'];
const signature = req.headers['fs-signature'];
if (!timestamp || !signature) {
console.error('Missing FS headers');
return res.status(400).send('Missing required headers');
}
// Validate signature
const signingString = `${timestamp}.${req.body}`;
const expectedSignature = crypto
.createHmac('sha256', process.env.FILESTACK_WEBHOOK_SECRET)
.update(signingString)
.digest('hex');
if (expectedSignature !== signature) {
console.error('Invalid signature');
return res.status(403).send('Invalid signature');
}
try {
const payload = JSON.parse(req.body);
// Route to appropriate handler
switch (payload.action) {
case 'fp.upload':
await handleUpload(payload);
break;
case 'fs.workflow':
await handleWorkflow(payload);
break;
case 'fp.delete':
await handleDelete(payload);
break;
default:
console.log(`Unhandled action: ${payload.action}`);
}
res.status(200).send('OK');
} catch (error) {
console.error('Error processing webhook:', error);
// Return 500 to trigger retry
res.status(500).send('Processing error');
}
});
Python with Proper Error Handling
from flask import Flask, request, abort
from filestack import Client
import os
import logging
app = Flask(__name__)
logging.basicConfig(level=logging.INFO)
@app.route('/webhook', methods=['POST'])
def webhook():
# Validate signature
secret = os.environ.get('FILESTACK_WEBHOOK_SECRET')
if not secret:
logging.error('FILESTACK_WEBHOOK_SECRET not configured')
abort(500)
try:
resp = Client.verify_webhook_signature(
secret,
request.data,
dict(request.headers)
)
if not resp['valid']:
logging.warning(f"Invalid webhook signature: {resp.get('error')}")
abort(403)
except Exception as e:
logging.error(f"Signature validation error: {e}")
abort(403)
payload = request.json
action = payload.get('action')
webhook_id = payload.get('id')
logging.info(f"Processing webhook {webhook_id} - action: {action}")
try:
if action == 'fp.upload':
handle_upload(payload)
elif action == 'fs.workflow':
handle_workflow(payload)
else:
logging.info(f"Unhandled action: {action}")
return '', 200
except Exception as e:
logging.error(f"Error processing webhook {webhook_id}: {e}")
# Return 500 to trigger Filestack retry
abort(500)
Common Pitfalls and Debugging
Problem: Webhooks Timing Out
If your processing takes more than 30 seconds, you’ll hit timeout issues:
@app.route('/webhook', methods=['POST'])
def webhook():
# Validate signature...
payload = request.json
# ❌ BAD - Slow processing blocks the response
# process_images(payload) # Takes 2 minutes
# send_notifications(payload) # Takes 30 seconds
# ✅ GOOD - Queue and respond immediately
task_queue.enqueue('process_webhook', payload)
return '', 200 # Acknowledge within seconds
Use job queues (Celery, Bull, AWS SQS) for heavy processing. Acknowledge the webhook immediately, then process asynchronously.
Problem: Testing Webhooks Locally
Filestack needs a public URL to send webhooks. During development, use ngrok:
# Terminal 1: Start your local server
python app.py
# Terminal 2: Expose it publicly
ngrok http 5000
Use the ngrok URL (e.g.,
https://abc123.ngrok.io/webhook
) in the Developer Portal. You’ll see all webhook traffic in the ngrok inspector at Without idempotency checks, retries create duplicate records: Store every webhook as an immutable event in your database. This gives you a complete audit trail: When processing fails repeatedly, move the webhook to a dead letter queue for manual inspection: Track these metrics in production: Set alerts for: Filestack webhooks are powerful when implemented correctly. The keys to success: validate signatures using raw request bodies, implement idempotency using webhook IDs, respond quickly and process asynchronously, and monitor your endpoint’s health. Most integration issues come from signature validation problems, use the exact raw body, not parsed JSON. Most production issues come from missing idempotency checks—store processed webhook IDs to prevent duplicates. With these patterns in place, webhooks become a reliable foundation for event-driven file handling workflows that scale. A Product Marketing Manager at Filestack with four years of dedicated experience. As a true technology enthusiast, they pair marketing expertise with a deep technical background. This allows them to effectively translate complex product capabilities into clear value for a developer-focused audience.Problem: Duplicate Event Processing
# Simple database-based deduplication
def process_webhook(payload):
webhook_id = payload['id']
# Check if already processed
existing = db.query(ProcessedWebhook).filter_by(webhook_id=webhook_id).first()
if existing:
return # Already handled
# Process the webhook
handle_upload(payload)
# Mark as processed
db.add(ProcessedWebhook(webhook_id=webhook_id, processed_at=datetime.now()))
db.commit()
Architecture Patterns That Work
Pattern 1: Event Sourcing
def store_webhook_event(payload):
event = WebhookEvent(
webhook_id=payload['id'],
action=payload['action'],
timestamp=payload['timestamp'],
raw_payload=json.dumps(payload),
processed=False
)
db.add(event)
db.commit()
return event.id
Pattern 2: Dead Letter Queue
def handle_webhook_with_retry(payload):
max_attempts = 3
attempt = get_attempt_count(payload['id'])
try:
process_webhook(payload)
clear_attempt_count(payload['id'])
except Exception as e:
if attempt >= max_attempts:
move_to_dead_letter_queue(payload, error=str(e))
logging.error(f"Webhook {payload['id']} moved to DLQ after {attempt} attempts")
else:
increment_attempt_count(payload['id'])
raise # Re-raise to return 500 and trigger Filestack retry
Monitoring and Alerting
from prometheus_client import Counter, Histogram
webhook_counter = Counter('webhooks_received', 'Webhooks received', ['action'])
webhook_processing_time = Histogram('webhook_processing_seconds', 'Time to process webhook')
webhook_errors = Counter('webhook_errors', 'Webhook processing errors', ['error_type'])
@app.route('/webhook', methods=['POST'])
def webhook():
action = request.json.get('action')
webhook_counter.labels(action=action).inc()
with webhook_processing_time.time():
try:
# Process webhook...
pass
except ValidationError:
webhook_errors.labels(error_type='validation').inc()
raise
Conclusion
Read More →