Webhooks
Receive real-time notifications when events occur in your Flow account.
What are Webhooks?
Webhooks are HTTP callbacks that notify your application when events happen. Instead of polling the API, Flow sends POST requests to your URL when events occur.
Supported Events
post.created- A new post has been scheduledpost.delivered- A post has been successfully published to all platformspost.failed- A post failed to publish (will be retried)post.blocked- A post was blocked (e.g., rate limit, content policy)channel.created- A new channel has been createdchannel.updated- A channel has been updatedwebhook.test- Test event (for testing webhook configuration)
Creating a Webhook
const webhook = await flow.webhooks.create({
url: 'https://example.com/webhooks/flow',
events: ['post.delivered', 'post.failed'],
});
console.log('Webhook secret:', webhook.secret); // Save this!
Important: Save the secret immediately - it's only shown once!
Webhook Payload
When an event occurs, Flow sends a POST request to your webhook URL:
{
"event": "post.delivered",
"data": {
"post_id": "post_abc123",
"channel_id": "channel_xyz",
"delivered_at": 1703123456000
},
"timestamp": 1703123456000
}
Webhook Headers
Every webhook request includes:
Content-Type: application/jsonFlow-Signature: t=1703123456,v1=abc123...- HMAC-SHA256 signatureFlow-Event-Type: post.delivered- The event type
Verifying Signatures
Always verify webhook signatures to ensure authenticity:
TypeScript
import crypto from 'crypto';
function verifyWebhookSignature(
payload: string,
signature: string,
secret: string
): boolean {
const parts = signature.split(',');
const timestampPart = parts.find(p => p.startsWith('t='));
const signaturePart = parts.find(p => p.startsWith('v1='));
if (!timestampPart || !signaturePart) {
return false;
}
const timestamp = timestampPart.split('=')[1];
const receivedSignature = signaturePart.split('=')[1];
// Compute expected signature
const hmac = crypto.createHmac('sha256', secret);
hmac.update(`${timestamp}.${payload}`);
const expectedSignature = hmac.digest('hex');
// Constant-time comparison
return crypto.timingSafeEqual(
Buffer.from(expectedSignature),
Buffer.from(receivedSignature)
);
}
// Express.js example
app.post('/webhooks/flow', express.raw({ type: 'application/json' }), (req, res) => {
const signature = req.headers['flow-signature'] as string;
const secret = process.env.FLOW_WEBHOOK_SECRET!;
if (!verifyWebhookSignature(req.body.toString(), signature, secret)) {
return res.status(401).send('Invalid signature');
}
const event = JSON.parse(req.body.toString());
console.log('Received event:', event.event, event.data);
res.status(200).send('OK');
});
Python
import hmac
import hashlib
import json
def verify_webhook_signature(payload: str, signature: str, secret: str) -> bool:
parts = signature.split(',')
timestamp_part = next((p for p in parts if p.startswith('t=')), None)
signature_part = next((p for p in parts if p.startswith('v1=')), None)
if not timestamp_part or not signature_part:
return False
timestamp = timestamp_part.split('=')[1]
received_signature = signature_part.split('=')[1]
# Compute expected signature
message = f"{timestamp}.{payload}"
expected_signature = hmac.new(
secret.encode(),
message.encode(),
hashlib.sha256
).hexdigest()
# Constant-time comparison
return hmac.compare_digest(expected_signature, received_signature)
# Flask example
@app.route('/webhooks/flow', methods=['POST'])
def webhook():
signature = request.headers.get('Flow-Signature')
secret = os.environ.get('FLOW_WEBHOOK_SECRET')
payload = request.get_data(as_text=True)
if not verify_webhook_signature(payload, signature, secret):
return 'Invalid signature', 401
event = json.loads(payload)
print(f"Received event: {event['event']}", event['data'])
return 'OK', 200
Handling Events
Post Delivered
if (event.event === 'post.delivered') {
const { post_id, channel_id, delivered_at } = event.data;
// Update your database
await db.updatePostStatus(post_id, 'delivered');
// Send notification
await sendNotification(`Post ${post_id} delivered!`);
}
Post Failed
if (event.event === 'post.failed') {
const { post_id, channel_id, error, platform } = event.data;
// Log error
console.error(`Post ${post_id} failed on ${platform}:`, error);
// Alert team
await alertTeam(`Post failed: ${post_id}`);
}
Retry Logic
Flow automatically retries failed webhook deliveries:
- Attempt 1: Immediate
- Attempt 2: 1 second delay
- Attempt 3: 2 seconds delay
- Attempt 4: 4 seconds delay
- Attempt 5: 8 seconds delay
- Attempt 6: 16 seconds delay
- Attempt 7: 32 seconds delay
After 7 failed attempts, the delivery is marked as failed.
Testing Webhooks
Test your webhook configuration:
await flow.webhooks.test(webhookId);
This sends a test event with event: "webhook.test".
Best Practices
- Always verify signatures - Never trust unverified requests
- Respond quickly - Your endpoint should respond within 5 seconds
- Handle idempotency - Same event may be delivered multiple times
- Log everything - Log all webhook deliveries for debugging
- Use HTTPS - Webhook URLs must use HTTPS in production
- Monitor deliveries - Check delivery history regularly
Delivery History
Check webhook delivery status:
const deliveries = await flow.webhooks.getDeliveries(webhookId);
deliveries.forEach(delivery => {
console.log(`Event: ${delivery.eventType}`);
console.log(`Status: ${delivery.status}`);
console.log(`Attempts: ${delivery.attemptCount}`);
});
Security
Signature Verification
Always verify signatures to prevent:
- Replay attacks
- Request tampering
- Unauthorized requests
HTTPS Only
Webhook URLs must use HTTPS in production. HTTP URLs are blocked.
URL Validation
Flow validates webhook URLs to prevent SSRF attacks:
- Blocks internal IP addresses
- Blocks localhost and private IP ranges
Next Steps
- Learn about Error Handling Guide
- Understand Best Practices Guide
- Explore Webhooks API Reference