Webhook Documentation
Real-time notifications for Swappable Images
Overview
Webhooks allow you to receive real-time HTTP POST notifications when events happen to your swappable images. Instead of polling our API, we'll push updates directly to your server.
Real-time
Get notified instantly when images are swapped
Secure
HMAC signatures verify authenticity
Reliable
Automatic retries on failure
Setup Guide
Create an endpoint on your server
Your endpoint must accept POST requests and return a 2xx status code within 10 seconds.
// Express.js example
app.post('/webhooks/foldr', (req, res) => {
const event = req.body;
console.log('Received webhook:', event.event);
console.log('Image ID:', event.image_id);
// Process the webhook...
res.status(200).json({ received: true });
});Configure your webhook URL
Go to your swappable image settings → Webhooks tab → Enter your endpoint URL. A signing secret will be automatically generated.
Select events to receive
Choose which events trigger webhooks. You can enable/disable events at any time.
Test your webhook
Click "Send Test" to send a test payload and verify your endpoint is working.
Webhook Events
| Event | Description |
|---|---|
image.swapped | Triggered when a new image version is uploaded and becomes the current version |
image.reverted | Triggered when the image is reverted to a previous version |
schedule.executed | Triggered when a scheduled swap is automatically executed |
test | Test event sent when you click "Send Test" in the dashboard |
Payload Format
All webhook payloads follow the same structure:
{
"event": "image.swapped",
"image_id": "550e8400-e29b-41d4-a716-446655440000",
"version_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"version_number": 5,
"timestamp": "2025-12-05T14:30:00.000Z",
"data": {
"original_name": "banner-v5.png",
"file_size": 245678,
"mime_type": "image/png"
}
}Revert Event Example
{
"event": "image.reverted",
"image_id": "550e8400-e29b-41d4-a716-446655440000",
"version_id": "6ba7b810-9dad-11d1-80b4-00c04fd430c8",
"version_number": 3,
"timestamp": "2025-12-05T14:30:00.000Z",
"data": {
"from_version": 5,
"to_version": 3
}
}HTTP Headers
Each webhook request includes these headers:
| Header | Description |
|---|---|
Content-Type | Always application/json |
User-Agent | Foldr-Webhooks/1.0 |
X-Webhook-Event | The event type (e.g., swap) |
X-Webhook-Delivery | Unique ID for this delivery (UUID) |
X-Webhook-Signature | HMAC signature for verification |
Signature Verification
Always verify webhook signatures to ensure requests are from Foldr. The signature is in theX-Webhook-Signature header.
Security Warning
Never process webhooks without verifying the signature. This protects you from spoofed requests.
Signature Format
X-Webhook-Signature: t=1701782345,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bdThe signature contains a timestamp (t=) and the HMAC-SHA256 signature (v1=).
Node.js Verification Example
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, secret) {
// Parse signature header
const parts = signature.split(',');
const timestamp = parts.find(p => p.startsWith('t='))?.slice(2);
const expectedSig = parts.find(p => p.startsWith('v1='))?.slice(3);
if (!timestamp || !expectedSig) {
return false;
}
// Check timestamp is recent (within 5 minutes)
const now = Math.floor(Date.now() / 1000);
if (Math.abs(now - parseInt(timestamp)) > 300) {
return false; // Replay attack protection
}
// Compute expected signature
const signedPayload = `${timestamp}.${payload}`;
const computedSig = crypto
.createHmac('sha256', secret)
.update(signedPayload)
.digest('hex');
// Use timing-safe comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSig),
Buffer.from(computedSig)
);
}
// Express middleware
app.post('/webhooks/foldr', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['x-webhook-signature'];
const payload = req.body.toString();
if (!verifyWebhookSignature(payload, signature, process.env.WEBHOOK_SECRET)) {
return res.status(401).json({ error: 'Invalid signature' });
}
const event = JSON.parse(payload);
// Process verified webhook...
res.status(200).json({ received: true });
});Python Verification Example
import hmac
import hashlib
import time
def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
# Parse signature
parts = dict(p.split('=') for p in signature.split(','))
timestamp = parts.get('t')
expected_sig = parts.get('v1')
if not timestamp or not expected_sig:
return False
# Check timestamp (within 5 minutes)
if abs(time.time() - int(timestamp)) > 300:
return False
# Compute signature
signed_payload = f"{timestamp}.{payload}"
computed_sig = hmac.new(
secret.encode(),
signed_payload.encode(),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(expected_sig, computed_sig)
# Flask example
@app.route('/webhooks/foldr', methods=['POST'])
def handle_webhook():
signature = request.headers.get('X-Webhook-Signature')
payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, WEBHOOK_SECRET):
return jsonify({'error': 'Invalid signature'}), 401
event = request.get_json()
# Process verified webhook...
return jsonify({'received': True}), 200Retry Behavior
If your endpoint returns a non-2xx status code or times out, we'll retry the webhook:
| Attempt | Delay |
|---|---|
| 1st retry | 1 minute after initial failure |
| 2nd retry | 5 minutes after 1st retry |
| 3rd retry (final) | 15 minutes after 2nd retry |
Timeout
Your endpoint must respond within 10 seconds. If processing takes longer, acknowledge receipt immediately and process asynchronously.
Best Practices
Return 2xx quickly
Acknowledge receipt immediately, then process the webhook asynchronously if needed.
Handle duplicates
Use the X-Webhook-Delivery header to deduplicate in case of retries.
Use HTTPS
Always use HTTPS endpoints in production to protect webhook payloads in transit.
Log everything
Keep logs of received webhooks for debugging. You can also view delivery logs in your dashboard.
Testing Webhooks
For local development, use a tunneling service to expose your local server:
# Using ngrok
ngrok http 3000
# Your webhook URL will be something like:
# https://abc123.ngrok.io/webhooks/foldrPopular options include ngrok, localtunnel, or webhook.site for quick testing.
Ready to get started?
Configure webhooks for your swappable images in just a few clicks.
Go to Dashboard