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 TypeRequired ScopeDescription
WIDGET_KYC_INITIATIONVERIFY_KYCTriggered when a user initiates KYC verification from the widget
WIDGET_DEPOSIT_COMPLETEMAKE_PAYOUT_SENDTriggered when a user successfully sends (deposits) into the application
WIDGET_WITHDRAW_COMPLETEMAKE_PAYOUT_RECEIVETriggered when a user successfully receives (withdraws) from the applicatio

Webhook Configuration

You can configure webhooks for your application through the developer portal:

  1. Navigate to For DevelopersOAuthWebhooks
  2. Click Add new URL
  3. Enter your HTTPS webhook endpoint URL
  4. Select which event types you want to receive (or leave empty for all events)
  5. 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


HeaderDescription
Content-TypeAlways application/json
X-HMAC-SignatureHMAC SHA-256 signature of the request body for verification
X-Event-IDUnique 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"
}

FieldTypeDescription
userIdString (UUID)The Fluz user ID
accountIdString(UUID)The Fluz account ID
externalReferenceIdStringYour application's user identifier( from OAuth flow)
appIdString (UUID)Your application ID
eventTypeStringAlways 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
}

FieldTypeDescription
userIdString (UUID)The Fluz user ID
accountIdString(UUID)The Fluz account ID
externalReferenceIdStringYour application's user identifier( from OAuth flow)
appIdString (UUID)Your application ID
eventTypeStringAlways WIDGET_DEPOSIT_COMPLETE
amountNumberThe 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
}
FieldTypeDescription
userIdString (UUID)The Fluz user ID
accountIdString(UUID)The Fluz account ID
externalReferenceIdStringYour application's user identifier( from OAuth flow)
appIdString (UUID)Your application ID
eventTypeStringAlways WIDGET_WITHDRAW_COMPLETE
amountNumberThe 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

  1. Extract the signature from the X-HMAC-Signature header
  2. Compute the HMAC SHA-256 of the raw request body using your application's API key as the secret
  3. 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