Skip to main content

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 scheduled
  • post.delivered - A post has been successfully published to all platforms
  • post.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 created
  • channel.updated - A channel has been updated
  • webhook.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/json
  • Flow-Signature: t=1703123456,v1=abc123... - HMAC-SHA256 signature
  • Flow-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

  1. Always verify signatures - Never trust unverified requests
  2. Respond quickly - Your endpoint should respond within 5 seconds
  3. Handle idempotency - Same event may be delivered multiple times
  4. Log everything - Log all webhook deliveries for debugging
  5. Use HTTPS - Webhook URLs must use HTTPS in production
  6. 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