refVenuerefVenue
Docs
Webhook API

Webhook API

Receive real-time notifications when conversions, approvals, and other events occur

Webhook API

Receive real-time notifications from refVenue when important events occur in your affiliate program.

What are Webhooks?

Webhooks are automated messages sent from refVenue to your server when specific events happen:

  • New conversion tracked
  • Conversion approved/rejected
  • Refund processed
  • Affiliate joins program
  • Commission payment made

Instead of polling our API, webhooks push data to you in real-time.

Use Cases

When to use webhooks:

  • Sync conversion data to your CRM
  • Trigger custom notifications
  • Update your internal analytics
  • Automate affiliate onboarding
  • Track commission payments

When NOT to use webhooks:

  • Simple conversion tracking (use JavaScript or server-side API instead)
  • One-way tracking only (webhooks are for receiving data)

Setup Guide

Step 1: Create a Webhook Endpoint

Create an endpoint on your server to receive webhook events:

// Express.js example
app.post('/webhooks/refvenue', async (req, res) => {
  const event = req.body;
 
  // Process the event
  console.log('Received event:', event.type);
 
  // Return 200 to acknowledge receipt
  res.status(200).send('OK');
});

Requirements:

  • Accepts POST requests
  • Returns 200 status code within 5 seconds
  • Publicly accessible URL (not localhost)
  • HTTPS recommended for production

Step 2: Register Your Webhook URL

  1. Log in to refVenue dashboard
  2. Go to SettingsWebhooks
  3. Click Add Webhook Endpoint
  4. Enter your webhook URL (e.g., https://yourdomain.com/webhooks/refvenue)
  5. Select events to receive
  6. Save

Verify that webhook requests actually come from refVenue:

const crypto = require('crypto');
 
function verifyWebhookSignature(payload, signature, secret) {
  const expectedSignature = crypto
    .createHmac('sha256', secret)
    .update(JSON.stringify(payload))
    .digest('hex');
 
  return crypto.timingSafeEqual(
    Buffer.from(signature),
    Buffer.from(expectedSignature)
  );
}
 
app.post('/webhooks/refvenue', (req, res) => {
  const signature = req.headers['x-refvenue-signature'];
  const webhookSecret = process.env.REFVENUE_WEBHOOK_SECRET;
 
  if (!verifyWebhookSignature(req.body, signature, webhookSecret)) {
    return res.status(401).send('Invalid signature');
  }
 
  // Process event...
  res.status(200).send('OK');
});

Get your webhook secret from SettingsWebhooksSigning Secret.

Webhook Events

conversion.created

Triggered when a new conversion is tracked.

{
  "type": "conversion.created",
  "data": {
    "id": "conv_abc123",
    "program_id": "prog_xyz789",
    "affiliate_id": "aff_def456",
    "affiliate_code": "JOHN123",
    "customer_email": "customer@example.com",
    "conversion_type": "purchase",
    "revenue_amount": 99.99,
    "commission_amount": 9.99,
    "status": "pending",
    "created_at": "2024-01-15T10:30:00Z"
  }
}

conversion.approved

Triggered when you approve a conversion.

{
  "type": "conversion.approved",
  "data": {
    "id": "conv_abc123",
    "program_id": "prog_xyz789",
    "affiliate_id": "aff_def456",
    "commission_amount": 9.99,
    "approved_at": "2024-01-16T14:20:00Z"
  }
}

conversion.rejected

Triggered when you reject a conversion.

{
  "type": "conversion.rejected",
  "data": {
    "id": "conv_abc123",
    "program_id": "prog_xyz789",
    "affiliate_id": "aff_def456",
    "reason": "Fraud detected",
    "rejected_at": "2024-01-16T14:20:00Z"
  }
}

conversion.refunded

Triggered when a purchase is refunded.

{
  "type": "conversion.refunded",
  "data": {
    "id": "conv_abc123",
    "program_id": "prog_xyz789",
    "affiliate_id": "aff_def456",
    "original_amount": 99.99,
    "refund_amount": 99.99,
    "commission_reversed": 9.99,
    "refunded_at": "2024-01-20T09:15:00Z"
  }
}

affiliate.joined

Triggered when an affiliate joins your program.

{
  "type": "affiliate.joined",
  "data": {
    "id": "aff_def456",
    "program_id": "prog_xyz789",
    "name": "John Doe",
    "email": "john@example.com",
    "referral_code": "JOHN123",
    "status": "active",
    "joined_at": "2024-01-10T08:00:00Z"
  }
}

payout.processed

Triggered when you process commission payouts.

{
  "type": "payout.processed",
  "data": {
    "id": "payout_ghi789",
    "program_id": "prog_xyz789",
    "affiliate_id": "aff_def456",
    "amount": 150.00,
    "currency": "USD",
    "period_start": "2024-01-01T00:00:00Z",
    "period_end": "2024-01-31T23:59:59Z",
    "processed_at": "2024-02-01T10:00:00Z"
  }
}

Implementation Examples

Node.js / Express

const express = require('express');
const crypto = require('crypto');
 
const app = express();
app.use(express.json());
 
const WEBHOOK_SECRET = process.env.REFVENUE_WEBHOOK_SECRET;
 
app.post('/webhooks/refvenue', (req, res) => {
  // Verify signature
  const signature = req.headers['x-refvenue-signature'];
  const expectedSig = crypto
    .createHmac('sha256', WEBHOOK_SECRET)
    .update(JSON.stringify(req.body))
    .digest('hex');
 
  if (signature !== expectedSig) {
    return res.status(401).send('Invalid signature');
  }
 
  const event = req.body;
 
  // Handle different event types
  switch (event.type) {
    case 'conversion.created':
      handleNewConversion(event.data);
      break;
 
    case 'conversion.approved':
      handleConversionApproved(event.data);
      break;
 
    case 'affiliate.joined':
      handleNewAffiliate(event.data);
      break;
 
    default:
      console.log('Unhandled event type:', event.type);
  }
 
  res.status(200).send('OK');
});
 
function handleNewConversion(data) {
  console.log('New conversion:', data.id);
  // Send to CRM, update analytics, etc.
}
 
function handleConversionApproved(data) {
  console.log('Conversion approved:', data.id);
  // Notify affiliate, update dashboard, etc.
}
 
function handleNewAffiliate(data) {
  console.log('New affiliate:', data.email);
  // Send welcome email, add to mailing list, etc.
}
 
app.listen(3000);

Python / Flask

from flask import Flask, request, jsonify
import hmac
import hashlib
import json
import os
 
app = Flask(__name__)
WEBHOOK_SECRET = os.environ['REFVENUE_WEBHOOK_SECRET']
 
def verify_signature(payload, signature):
    expected = hmac.new(
        WEBHOOK_SECRET.encode(),
        json.dumps(payload).encode(),
        hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(signature, expected)
 
@app.route('/webhooks/refvenue', methods=['POST'])
def webhook():
    # Verify signature
    signature = request.headers.get('X-Refvenue-Signature')
    if not verify_signature(request.json, signature):
        return 'Invalid signature', 401
 
    event = request.json
 
    # Handle events
    if event['type'] == 'conversion.created':
        handle_new_conversion(event['data'])
    elif event['type'] == 'conversion.approved':
        handle_conversion_approved(event['data'])
    elif event['type'] == 'affiliate.joined':
        handle_new_affiliate(event['data'])
 
    return 'OK', 200
 
def handle_new_conversion(data):
    print(f"New conversion: {data['id']}")
    # Your logic here
 
def handle_conversion_approved(data):
    print(f"Conversion approved: {data['id']}")
    # Your logic here
 
def handle_new_affiliate(data):
    print(f"New affiliate: {data['email']}")
    # Your logic here
 
if __name__ == '__main__':
    app.run(port=3000)

PHP

<?php
$webhookSecret = getenv('REFVENUE_WEBHOOK_SECRET');
 
// Get raw POST body
$payload = file_get_contents('php://input');
$data = json_decode($payload, true);
 
// Verify signature
$signature = $_SERVER['HTTP_X_REFVENUE_SIGNATURE'];
$expectedSignature = hash_hmac('sha256', $payload, $webhookSecret);
 
if (!hash_equals($signature, $expectedSignature)) {
    http_response_code(401);
    exit('Invalid signature');
}
 
// Handle event
switch ($data['type']) {
    case 'conversion.created':
        handleNewConversion($data['data']);
        break;
    case 'conversion.approved':
        handleConversionApproved($data['data']);
        break;
    case 'affiliate.joined':
        handleNewAffiliate($data['data']);
        break;
}
 
http_response_code(200);
echo 'OK';
 
function handleNewConversion($data) {
    error_log("New conversion: " . $data['id']);
    // Your logic here
}
 
function handleConversionApproved($data) {
    error_log("Conversion approved: " . $data['id']);
    // Your logic here
}
 
function handleNewAffiliate($data) {
    error_log("New affiliate: " . $data['email']);
    // Your logic here
}
?>

Best Practices

1. Return 200 Quickly

Process events asynchronously to avoid timeouts:

app.post('/webhooks/refvenue', async (req, res) => {
  // Acknowledge immediately
  res.status(200).send('OK');
 
  // Process asynchronously
  processWebhook(req.body).catch(console.error);
});

2. Handle Duplicate Events

Webhooks may be sent multiple times. Make operations idempotent:

async function handleNewConversion(data) {
  // Check if already processed
  const exists = await db.conversions.findOne({ id: data.id });
  if (exists) {
    console.log('Already processed:', data.id);
    return;
  }
 
  // Process and store
  await db.conversions.create(data);
}

3. Verify Signatures

Always verify webhook signatures in production:

if (process.env.NODE_ENV === 'production') {
  if (!verifySignature(req.body, req.headers['x-refvenue-signature'])) {
    return res.status(401).send('Invalid signature');
  }
}

4. Log Events

Keep a record of all webhook events:

app.post('/webhooks/refvenue', async (req, res) => {
  // Log the raw webhook
  await db.webhookLogs.create({
    type: req.body.type,
    payload: req.body,
    received_at: new Date()
  });
 
  // Process...
  res.status(200).send('OK');
});

5. Handle Failures Gracefully

Don't let webhook failures break your application:

async function processWebhook(event) {
  try {
    await handleEvent(event);
  } catch (error) {
    console.error('Webhook processing failed:', error);
    // Log to monitoring service
    // Don't throw - already sent 200
  }
}

Testing Webhooks

Local Development

Use ngrok or similar to expose localhost:

# Install ngrok
npm install -g ngrok
 
# Expose port 3000
ngrok http 3000
 
# Use the ngrok URL in refVenue webhook settings
# https://abc123.ngrok.io/webhooks/refvenue

Test Events

Send test events from the refVenue dashboard:

  1. Go to SettingsWebhooks
  2. Click on your webhook endpoint
  3. Click Send Test Event
  4. Select event type
  5. Check your server logs

Manual Testing

Use curl to test your endpoint:

curl -X POST https://yourdomain.com/webhooks/refvenue \
  -H "Content-Type: application/json" \
  -H "X-Refvenue-Signature: test_signature" \
  -d '{
    "type": "conversion.created",
    "data": {
      "id": "conv_test123",
      "revenue_amount": 100.00,
      "commission_amount": 10.00
    }
  }'

Troubleshooting

Webhooks Not Received

Check:

  1. Endpoint is publicly accessible (not localhost)
  2. Endpoint returns 200 status code
  3. No firewall blocking requests
  4. Webhook is enabled in settings
  5. Check webhook delivery logs in dashboard

Signature Verification Fails

Ensure:

  1. Using correct webhook secret
  2. Verifying raw body (not parsed JSON)
  3. Using same hashing algorithm (SHA-256)
  4. Comparing signatures with timing-safe function

Timeout Errors

Solutions:

  1. Return 200 immediately
  2. Process events asynchronously
  3. Optimize database queries
  4. Increase server timeout limits

Duplicate Events

This is normal:

  • refVenue retries failed deliveries
  • Network issues can cause duplicates
  • Make handlers idempotent (safe to run multiple times)

Webhook Delivery

Retry Policy

If your endpoint fails (non-200 response or timeout):

  1. Immediate retry after 1 second
  2. Retry after 5 seconds
  3. Retry after 30 seconds
  4. Retry after 2 minutes
  5. Retry after 15 minutes
  6. Stop after 5 failed attempts

Timeout

Webhooks timeout after 5 seconds. Return 200 faster than that.

Ordering

Events are not guaranteed to arrive in order. Use timestamps to determine sequence.

Security

HTTPS Only (Production)

Always use HTTPS endpoints in production:

  • https://yourdomain.com/webhooks
  • http://yourdomain.com/webhooks

Verify Signatures

Don't trust webhook data without signature verification:

// ❌ Bad - accepts any POST request
app.post('/webhooks/refvenue', (req, res) => {
  handleEvent(req.body);  // Could be from anyone!
  res.send('OK');
});
 
// ✅ Good - verifies signature
app.post('/webhooks/refvenue', (req, res) => {
  if (!verifySignature(req.body, req.headers['x-refvenue-signature'])) {
    return res.status(401).send('Invalid');
  }
  handleEvent(req.body);
  res.send('OK');
});

IP Allowlisting (Optional)

For extra security, only accept requests from refVenue IPs:

const REFVENUE_IPS = [
  '203.0.113.0/24',
  '198.51.100.0/24'
];
 
app.post('/webhooks/refvenue', (req, res) => {
  const clientIP = req.ip;
  if (!isIPAllowed(clientIP, REFVENUE_IPS)) {
    return res.status(403).send('Forbidden');
  }
  // Process webhook...
});

Contact support for current IP ranges.

Alternative Integration Methods

Webhooks are just one way to integrate:

Support