Webhooks
Webhooks for OAuth Applications
Webhooks allow your application to receive real-time notifications when events occur in user accounts. When enabled, Fluz will send HTTP POST requests to your configured endpoint URL with event details.
Available Events
Webhooks are sent based on the scopes granted by the user during OAuth authorization. Events are only delivered if the user has granted the required scope.
| Event Type | Required Scope | Description |
|---|---|---|
WIDGET_KYC_INITIATION | VERIFY_KYC | Triggered when a user initiates KYC verification from the widget |
WIDGET_DEPOSIT_COMPLETE | MAKE_PAYOUT_SEND | Triggered when a user successfully sends (deposits) into the application |
WIDGET_WITHDRAW_COMPLETE | MAKE_PAYOUT_RECEIVE | Triggered when a user successfully receives (withdraws) from the applicatio |
Webhook Configuration
You can configure webhooks for your application through the developer portal:
- Navigate to For Developers → OAuth → Webhooks
- Click Add new URL
- Enter your HTTPS webhook endpoint URL
- Select which event types you want to receive (or leave empty for all events)
- Click Create Webhook
Requirements
When configuring your webhook:
- ✅ Must use HTTPS (HTTP endpoints will be rejected)
- ✅ Must be a publicly accessible URL
- ✅ Must respond to POST requests
- ✅ Must implement signature verification (see below)
- ✅ Should respond within 30 seconds
Managing Webhooks
- Add multiple webhooks - You can configure multiple webhook URLs for the same application
- Update events - Delete and recreate a webhook to change subscribed events
- Remove webhooks - Click the "Remove" button next to any webhook to delete it
Webhook Delivery
HTTP Request Format
POST /your/webhook/endpoint HTTP/1.1
Host: your-app.com
Content-Type: application/json
X-HMAC-Signature: <hmac_sha256_signature>
X-Event-ID: <event_correlation_id>
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"accountId": "550e8400-e29b-41d4-a716-446655440001",
"externalReferenceId": "550e8400-e29b-41d4-a716-446655440002",
"eventType": "WIDGET_DEPOSIT_COMPLETE",
"amount": 50.00
}Headers
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-HMAC-Signature | HMAC SHA-256 signature of the request body for verification |
X-Event-ID | Unique identifier for this event (UUID format) |
Event Payloads
WIDGET_KYC_INITIATION
Sent when a KYC verification is initiated in the widget.
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"accountId": "550e8400-e29b-41d4-a716-446655440001",
"externalReferenceId": "550e8400-e29b-41d4-a716-446655440002",
"eventType": "WIDGET_KYC_INITIATION",
"appId": "550e8400-e29b-41d4-a716-446655440003"
}
| Field | Type | Description |
|---|---|---|
userId | String (UUID) | The Fluz user ID |
accountId | String(UUID) | The Fluz account ID |
externalReferenceId | String | Your application's user identifier( from OAuth flow) |
appId | String (UUID) | Your application ID |
eventType | String | Always WIDGET_KYC_INITIATION |
WIDGET_DEPOSIT_COMPLETE
Sent when a user successfully deposits or sends money to the application.
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"accountId": "550e8400-e29b-41d4-a716-446655440001",
"externalReferenceId": "550e8400-e29b-41d4-a716-446655440002",
"appId": "550e8400-e29b-41d4-a716-446655440003",
"eventType": "WIDGET_DEPOSIT_COMPLETE",
"amount": 50.00
}
| Field | Type | Description |
|---|---|---|
userId | String (UUID) | The Fluz user ID |
accountId | String(UUID) | The Fluz account ID |
externalReferenceId | String | Your application's user identifier( from OAuth flow) |
appId | String (UUID) | Your application ID |
eventType | String | Always WIDGET_DEPOSIT_COMPLETE |
amount | Number | The deposit amount in USD(e.g., 50, 100.50) |
WIDGET_WITHDRAW_COMPLETE
Send when a user successfully withdraws or receives money from the application
{
"userId": "550e8400-e29b-41d4-a716-446655440000",
"accountId": "550e8400-e29b-41d4-a716-446655440001",
"externalReferenceId": "550e8400-e29b-41d4-a716-446655440002",
"appId": "550e8400-e29b-41d4-a716-446655440003",
"eventType": "WIDGET_WITHDRAW_COMPLETE",
"amount": 25.00
}
| Field | Type | Description |
|---|---|---|
userId | String (UUID) | The Fluz user ID |
accountId | String(UUID) | The Fluz account ID |
externalReferenceId | String | Your application's user identifier( from OAuth flow) |
appId | String (UUID) | Your application ID |
eventType | String | Always WIDGET_WITHDRAW_COMPLETE |
amount | Number | The deposit amount in USD(e.g., 25, 100.50) |
Verifying Webhook Signatures
Every webhook request includes an HMAC signature that you must verify to ensure the request came from Fluz.
Verification Steps
- Extract the signature from the
X-HMAC-Signatureheader - Compute the HMAC SHA-256 of the raw request body using your application's API key as the secret
- Compare your computed signature with the received signature
Example Verification (Node.js)
const crypto = require('crypto');
function verifyWebhookSignature(payload, signature, apiKey) {
const computedSignature = crypto
.createHmac('sha256', apiKey)
.update(JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(computedSignature)
);
}
// Express.js example
app.post('/webhook', express.json(), (req, res) => {
const signature = req.headers['x-hmac-signature'];
const eventId = req.headers['x-event-id'];
if (!verifyWebhookSignature(req.body, signature, YOUR_API_KEY)) {
return res.status(401).send('Invalid signature');
}
// Process the webhook
const { eventType, userId, amount } = req.body;
console.log(`Received ${eventType} for user ${userId}`);
// Always respond with 200 to acknowledge receipt
res.status(200).send('OK');
});
Example Verification (Python)
import hmac
import hashlib
import json
def verify_webhook_signature(payload: dict, signature: str, api_key: str) -> bool:
payload_string = json.dumps(payload, separators=(',', ':'))
computed_signature = hmac.new(
api_key.encode('utf-8'),
payload_string.encode('utf-8'),
hashlib.sha256
).hexdigest()
return hmac.compare_digest(signature, computed_signature)
# Flask example
@app.route('/webhook', methods=['POST'])
def webhook():
signature = request.headers.get('X-HMAC-Signature')
event_id = request.headers.get('X-Event-ID')
payload = request.json
if not verify_webhook_signature(payload, signature, YOUR_API_KEY):
return 'Invalid signature', 401
# Process the webhook
event_type = payload['eventType']
user_id = payload['userId']
print(f"Received {event_type} for user {user_id}")
# Always respond with 200
return 'OK', 200
Responding to webhooks
Your endpoint must:
- ✅ Respond with HTTP status 200-299 within 30 seconds
- ✅ Return quickly (process events asynchronously if needed)
- ✅ Be publicly accessible via HTTPS
- ❌ Do NOT respond with redirects (3xx)
- ❌ Do NOT respond with client errors (4xx) or server errors (5xx) for valid webhooks
Example Response Handler
app.post('/webhook', express.json(), async (req, res) => {
const signature = req.headers['x-hmac-signature'];
// 1. Verify signature
if (!verifyWebhookSignature(req.body, signature, YOUR_API_KEY)) {
return res.status(401).send('Invalid signature');
}
// 2. Respond immediately
res.status(200).send('OK');
// 3. Process asynchronously
processWebhookAsync(req.body).catch(err => {
console.error('Error processing webhook:', err);
});
});
async function processWebhookAsync(payload) {
const { eventType, userId, accountId, amount } = payload;
switch (eventType) {
case 'WIDGET_DEPOSIT_COMPLETE':
await updateUserBalance(userId, amount);
break;
case 'WIDGET_WITHDRAW_COMPLETE':
await recordWithdrawal(userId, amount);
break;
// ... handle other events
}
}
Retry Behavior
If your endpoint fails to response with a 2xx status code, Fluz will automatically retry the webhook delivery:
- Maximum attempts: 5
- Retry schedule: Exponential backoff managed by Google Cloud Tasks
- Timeout: 30 seconds per attempt
After 5 failed attempts, the webhook event is marked as FAILEDand delivery stops. Monitor your webhook endpoint logs to ensure you're receiving and processing events successfully.
Best Practices
Security
- ✅ Always verify HMAC signatures before processing webhook data
- ✅ Use HTTPS endpoints only
- ✅ Keep your API key secret and rotate periodically
- ❌ Do not expose your webhook endpoint to the public without authentication
Reliability
- ✅ Respond with 200 immediately, then process asynchronously
- ✅ Implement idempotency using the X-Event-ID header (events may be retried)
- ✅ Store processed event IDs to prevent duplicate processing
- ✅ Monitor webhook failures and set up alerting
Performance
- ✅ Keep your endpoint fast (respond within 5 seconds ideally)
- ✅ Use a queue for heavy processing
- ✅ Scale your webhook endpoint to handle bursts
Idempotency
Webhooks may be delivered more than once. Use the X-Event-ID header to detect and handle duplicate deliveries:
const processedEvents = new Set(); // Use Redis/DB in production
app.post('/webhook', async (req, res) => {
const eventId = req.headers['x-event-id'];
// Check if already processed
if (processedEvents.has(eventId)) {
console.log(`Event ${eventId} already processed, skipping`);
return res.status(200).send('OK');
}
// Verify signature
if (!verifyWebhookSignature(req.body, req.headers['x-hmac-signature'], API_KEY)) {
return res.status(401).send('Invalid signature');
}
// Respond immediately
res.status(200).send('OK');
// Mark as processed
processedEvents.add(eventId);
// Process event
await processWebhookAsync(req.body);
});
FAQ
How do I know which user triggered the event?
Use the externalReferenceId field, which contains the user identifier you provided during the OAuth authorization flow. This maps to your application's user ID.
Can I receive webhooks for multiple applications?
Yes, configure separate webhook endpoints for each application, or use a single endpoint and differentiate by the appId field included in all webhook payloads.
What if my endpoint is temporarily down?
Fluz will retry the webhook up to 5 times with exponential backoff. After 5 failures, the event is marked as failed. Contact support if you need events re-sent.
Are webhooks sent in order?
Webhooks are sent as events occur, but delivery order is not guaranteed. Use timestamps and event IDs to order events if needed.
Need Help?
- Setup assistance: Contact [email protected]
- Technical issues: Check your webhook logs and contact support with the X-Event-ID
- Scope questions: See the OAuth Scopes documentation
Updated 24 days ago
