Skip to main content
Receive real-time notifications when events happen in your BaClique account.

Overview

Webhooks allow your application to be notified immediately when:
  • A new conversion is tracked
  • A conversion is approved or rejected
  • An affiliate joins your campaign
Instead of polling the API, you receive a POST request to your endpoint.

Setting Up Webhooks

1. Create an Endpoint

Go to Developer Settings and:
  1. Click “Add Endpoint”
  2. Enter your HTTPS URL
  3. Select the events to subscribe to
  4. Save and copy the signing secret
⚠️ Important: The secret is only shown once. Store it securely!

2. Handle Incoming Webhooks

Your endpoint will receive POST requests like this:
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
X-BaClique-Signature: t=1705765200,v1=5d41402abc4b2a76b9719d911017c592
X-BaClique-Event: conversion.created

{
  "id": "evt_abc123",
  "type": "conversion.created",
  "created": 1705765200,
  "data": {
    "id": "conv_xyz789",
    "campaign_id": "cmp_abc123",
    "affiliate_id": "aff_user123",
    "amount": 99.99,
    "commission": 9.99,
    "external_id": "ORDER-12345",
    "status": "pending"
  }
}

Available Events

EventTriggered When
conversion.createdNew conversion is tracked
conversion.approvedConversion is approved
conversion.rejectedConversion is rejected
affiliate.joinedAffiliate joins a campaign
affiliate.approvedPending affiliate is approved
payout.createdPayout is initiated

Event Payloads

conversion.created

{
  "id": "evt_abc123",
  "type": "conversion.created",
  "created": 1705765200,
  "data": {
    "id": "conv_xyz789",
    "campaign_id": "cmp_abc123",
    "affiliate_id": "aff_user123",
    "amount": 99.99,
    "commission": 9.99,
    "external_id": "ORDER-12345",
    "status": "pending"
  }
}

conversion.approved / conversion.rejected

{
  "id": "evt_def456",
  "type": "conversion.approved",
  "created": 1705765300,
  "data": {
    "id": "conv_xyz789",
    "campaign_id": "cmp_abc123",
    "affiliate_id": "aff_user123",
    "amount": 99.99,
    "commission": 9.99,
    "external_id": "ORDER-12345",
    "status": "approved",
    "reason": null
  }
}

affiliate.joined

{
  "id": "evt_ghi789",
  "type": "affiliate.joined",
  "created": 1705765400,
  "data": {
    "affiliation_id": "aff_xyz789",
    "campaign_id": "cmp_abc123",
    "affiliate_email": "john@example.com",
    "affiliate_name": "John Doe",
    "status": "active"
  }
}

Verifying Signatures

Always verify webhook signatures to ensure requests are from BaClique.

Signature Format

X-BaClique-Signature: t=1705765200,v1=5d41402abc4b2a76b9719d911017c592
  • t = Unix timestamp when signed
  • v1 = HMAC-SHA256 signature

Verification Algorithm

signed_payload = "${timestamp}.${request_body}"
expected_signature = HMAC_SHA256(signing_secret, signed_payload)

Node.js Example

const crypto = require('crypto');

function verifyWebhookSignature(payload, signature, secret) {
  const parts = signature.split(',');
  const timestamp = parts[0].split('=')[1];
  const receivedSig = parts[1].split('=')[1];
  
  // Check timestamp (reject if > 5 minutes old)
  const now = Math.floor(Date.now() / 1000);
  if (now - parseInt(timestamp) > 300) {
    throw new Error('Webhook timestamp too old');
  }
  
  // Compute expected signature
  const signedPayload = `${timestamp}.${payload}`;
  const expectedSig = crypto
    .createHmac('sha256', secret)
    .update(signedPayload)
    .digest('hex');
  
  // Timing-safe comparison
  if (!crypto.timingSafeEqual(
    Buffer.from(receivedSig),
    Buffer.from(expectedSig)
  )) {
    throw new Error('Invalid webhook signature');
  }
  
  return JSON.parse(payload);
}

// Express.js handler
app.post('/webhook', express.raw({type: 'application/json'}), (req, res) => {
  try {
    const event = verifyWebhookSignature(
      req.body.toString(),
      req.headers['x-baclique-signature'],
      process.env.WEBHOOK_SECRET
    );
    
    switch (event.type) {
      case 'conversion.created':
        console.log('New conversion:', event.data.id);
        break;
      case 'conversion.approved':
        console.log('Conversion approved:', event.data.id);
        break;
    }
    
    res.status(200).send('OK');
  } catch (err) {
    console.error('Webhook error:', err.message);
    res.status(400).send('Invalid signature');
  }
});

Python Example

import hmac
import hashlib
import time
import json

def verify_webhook(payload: bytes, signature: str, secret: str) -> dict:
    parts = dict(p.split('=') for p in signature.split(','))
    timestamp = parts['t']
    received_sig = parts['v1']
    
    # Check timestamp
    if int(time.time()) - int(timestamp) > 300:
        raise ValueError('Webhook timestamp too old')
    
    # Compute signature
    signed_payload = f"{timestamp}.{payload.decode()}"
    expected_sig = hmac.new(
        secret.encode(),
        signed_payload.encode(),
        hashlib.sha256
    ).hexdigest()
    
    if not hmac.compare_digest(received_sig, expected_sig):
        raise ValueError('Invalid signature')
    
    return json.loads(payload)

# Flask handler
@app.route('/webhook', methods=['POST'])
def webhook():
    try:
        event = verify_webhook(
            request.data,
            request.headers['X-BaClique-Signature'],
            os.environ['WEBHOOK_SECRET']
        )
        
        if event['type'] == 'conversion.created':
            print(f"New conversion: {event['data']['id']}")
        
        return 'OK', 200
    except ValueError as e:
        return str(e), 400

Retry Policy

If your endpoint returns a non-2xx status code, we retry:
AttemptDelay
1st retry1 minute
2nd retry5 minutes
3rd retry30 minutes
After 3 failed attempts, the webhook is marked as failed.

Best Practices

  1. Always verify signatures - Don’t trust unverified webhooks
  2. Return 200 quickly - Process asynchronously if needed
  3. Handle duplicates - Use event.id for idempotency
  4. Use HTTPS - We reject HTTP endpoints
  5. Implement retry handling - Your endpoint should be idempotent