Skip to main content

Overview

Coupon webhooks notify your system when customers redeem points for rewards or when they actually use the generated coupons. These events are crucial for inventory management, reward fulfillment, customer behavior tracking, and ensuring seamless coupon validation across all channels.

Available Coupon Events

coupon.redeemed

coupon.redeemed

Triggered when a customer converts points into a coupon
  • Use cases: Coupon delivery, inventory management, redemption analytics, email automation
  • Payload includes: Coupon code, reward details, points spent, expiration date

coupon.used

coupon.used

Triggered when a customer actually uses a coupon at your business
  • Use cases: Inventory updates, sales analytics, customer behavior tracking, fraud prevention
  • Payload includes: Usage location, staff member, order details, discount applied

Event Payload Structures

coupon.redeemed Webhook

event
string
required
Event type identifier
coupon.redeemed
timestamp
string
required
Event timestamp (ISO 8601)
2025-08-26T14:20:00Z
company_id
string
required
Company identifier that triggered the event
comp_bella_vista
customer
object
required
Customer information
coupon
object
required
Generated coupon details
discount_details
object
Discount information (for discount-type rewards)

coupon.used Webhook

event
string
required
Event type identifier
coupon.used
timestamp
string
required
Event timestamp (ISO 8601)
2025-08-26T18:45:00Z
company_id
string
required
Company identifier
comp_bella_vista
customer
object
required
Customer who used the coupon
usage
object
required
Coupon usage details

Example Payloads

{
  "event": "coupon.redeemed",
  "timestamp": "2025-08-26T14:20:00Z",
  "company_id": "comp_bella_vista",
  "customer": {
    "member_id": "ZP-4F95FB0A",
    "whatsapp": "+5511965958122",
    "email": "[email protected]",
    "name": "Maria Silva",
    "tier": "silver"
  },
  "coupon": {
    "coupon_code": "CP-A1B2C3D4",
    "reward_id": "rw_pizza_margherita",
    "reward_name": "Pizza Margherita Grátis",
    "reward_description": "Pizza Margherita tradicional completa com molho de tomate, mussarela e manjericão fresco",
    "points_spent": 1000,
    "new_balance": 350,
    "expires_at": "2025-09-26T14:20:00Z",
    "terms": "Válido apenas para consumo no local. Não pode ser combinado com outras promoções.",
    "redemption_method": "app"
  },
  "discount_details": {
    "discount_type": "product_free",
    "discount_value": 35.00,
    "maximum_discount": null,
    "minimum_purchase": 0
  }
}

Integration Patterns

Coupon Delivery System

const handleCouponRedeemed = async (webhook) => {
  const { customer, coupon, discount_details } = webhook;
  
  // Generate coupon delivery method based on customer preference
  const deliveryMethods = [];
  
  // Always store in customer's app/account
  await storeCouponInAccount(customer.member_id, coupon);
  
  // Email delivery with coupon details
  if (customer.email) {
    deliveryMethods.push({
      method: 'email',
      template: 'coupon_delivery',
      recipient: customer.email,
      data: {
        customerName: customer.name,
        couponCode: coupon.coupon_code,
        rewardName: coupon.reward_name,
        rewardDescription: coupon.reward_description,
        expiresAt: formatDate(coupon.expires_at),
        terms: coupon.terms,
        discountDetails: discount_details
      }
    });
  }
  
  // WhatsApp delivery for immediate notification
  if (customer.whatsapp) {
    deliveryMethods.push({
      method: 'whatsapp',
      template: 'coupon_whatsapp',
      recipient: customer.whatsapp,
      data: {
        name: customer.name,
        rewardName: coupon.reward_name,
        couponCode: coupon.coupon_code,
        expiresAt: formatDate(coupon.expires_at, 'short')
      }
    });
  }
  
  // SMS fallback for high-value rewards
  if (discount_details?.discount_value >= 50) {
    deliveryMethods.push({
      method: 'sms',
      recipient: extractPhoneNumber(customer.whatsapp),
      message: `${customer.name}, seu cupom ${coupon.coupon_code} está pronto! ${coupon.reward_name}. Válido até ${formatDate(coupon.expires_at, 'short')}.`
    });
  }
  
  // Execute all delivery methods
  await Promise.allSettled(
    deliveryMethods.map(method => deliverCoupon(method))
  );
  
  // Log redemption for analytics
  await logCouponRedemption({
    customerId: customer.member_id,
    couponCode: coupon.coupon_code,
    rewardId: coupon.reward_id,
    pointsSpent: coupon.points_spent,
    deliveryMethods: deliveryMethods.map(m => m.method)
  });
};

Inventory Management

const handleInventoryUpdate = async (webhook) => {
  const { coupon, usage } = webhook;
  
  // Handle different discount types
  switch (usage ? usage.discount_type : coupon.discount_details?.discount_type) {
    case 'product_free':
      await handleProductInventory(webhook);
      break;
    case 'buy_one_get_one':
      await handleBOGOInventory(webhook);
      break;
    default:
      // No inventory impact for percentage/fixed discounts
      break;
  }
};

const handleProductInventory = async (webhook) => {
  const isUsageEvent = webhook.event === 'coupon.used';
  
  if (isUsageEvent) {
    const { usage } = webhook;
    
    // Decrement actual inventory on usage
    for (const item of usage.items_affected) {
      if (item.discount_applied === item.unit_price) { // Fully free item
        await inventory.decrement(item.name, item.quantity);
        
        // Check for low stock alert
        const currentStock = await inventory.getStock(item.name);
        if (currentStock <= 5) {
          await sendLowStockAlert({
            product: item.name,
            currentStock,
            location: usage.location,
            lastUsedBy: webhook.customer.name
          });
        }
      }
    }
    
    // Update reserved inventory (release reservation)
    await inventory.releaseReservation(usage.coupon_code);
  } else {
    // On redemption, reserve inventory
    const { coupon } = webhook;
    await inventory.reserve(coupon.reward_name, 1, coupon.coupon_code, {
      expiresAt: coupon.expires_at,
      customerId: webhook.customer.member_id
    });
  }
};

Fraud Detection

const detectCouponFraud = async (webhook) => {
  const { customer, coupon, usage } = webhook;
  const suspiciousActivity = [];
  
  // Check for rapid redemptions
  if (webhook.event === 'coupon.redeemed') {
    const recentRedemptions = await getRecentRedemptions(customer.member_id, '1h');
    if (recentRedemptions.length >= 5) {
      suspiciousActivity.push({
        type: 'rapid_redemption',
        severity: 'medium',
        details: `${recentRedemptions.length} redemptions in last hour`
      });
    }
  }
  
  // Check for unusual usage patterns
  if (webhook.event === 'coupon.used') {
    const { usage } = webhook;
    
    // Same coupon used multiple times (should be impossible)
    const previousUsages = await getCouponUsageHistory(usage.coupon_code);
    if (previousUsages.length > 0) {
      suspiciousActivity.push({
        type: 'duplicate_usage',
        severity: 'high',
        details: `Coupon ${usage.coupon_code} used multiple times`
      });
    }
    
    // Usage after expiration
    const couponDetails = await getCouponDetails(usage.coupon_code);
    if (new Date(couponDetails.expires_at) < new Date(webhook.timestamp)) {
      suspiciousActivity.push({
        type: 'expired_usage',
        severity: 'high',
        details: `Coupon used after expiration: ${couponDetails.expires_at}`
      });
    }
    
    // Unusual discount amount
    if (usage.discount_applied > usage.order_value) {
      suspiciousActivity.push({
        type: 'excessive_discount',
        severity: 'critical',
        details: `Discount (${usage.discount_applied}) exceeds order value (${usage.order_value})`
      });
    }
  }
  
  // Alert if suspicious activity detected
  if (suspiciousActivity.length > 0) {
    await sendFraudAlert({
      customerId: customer.member_id,
      event: webhook.event,
      suspiciousActivity,
      webhook
    });
    
    // Auto-flag high severity issues
    const criticalIssues = suspiciousActivity.filter(a => a.severity === 'critical');
    if (criticalIssues.length > 0) {
      await flagCustomerAccount(customer.member_id, 'fraud_suspected', criticalIssues);
    }
  }
  
  return suspiciousActivity;
};

Analytics and Reporting

const trackCouponMetrics = async (webhook) => {
  const { customer, coupon, usage } = webhook;
  
  if (webhook.event === 'coupon.redeemed') {
    // Track redemption metrics
    await analytics.track('coupon_redeemed', {
      customer_id: customer.member_id,
      customer_tier: customer.tier,
      reward_id: coupon.reward_id,
      reward_name: coupon.reward_name,
      points_spent: coupon.points_spent,
      discount_type: coupon.discount_details?.discount_type,
      discount_value: coupon.discount_details?.discount_value,
      redemption_method: coupon.redemption_method,
      expires_days: calculateDaysToExpiry(coupon.expires_at),
      timestamp: webhook.timestamp
    });
    
    // Update customer redemption behavior
    await updateCustomerProfile(customer.member_id, {
      last_redemption_date: webhook.timestamp,
      total_redemptions: '+1',
      total_points_spent: `+${coupon.points_spent}`,
      preferred_reward_category: await calculatePreferredCategory(customer.member_id),
      redemption_frequency: await calculateRedemptionFrequency(customer.member_id)
    });
    
  } else if (webhook.event === 'coupon.used') {
    // Track usage metrics
    await analytics.track('coupon_used', {
      customer_id: customer.member_id,
      customer_tier: customer.tier,
      coupon_code: usage.coupon_code,
      reward_name: usage.reward_name,
      discount_type: usage.discount_type,
      discount_applied: usage.discount_applied,
      order_value: usage.order_value,
      final_value: usage.final_value,
      location: usage.location,
      platform: usage.platform,
      staff_member: usage.staff_member,
      items_count: usage.items_affected?.length || 0,
      time_to_usage: await calculateTimeToUsage(usage.coupon_code, webhook.timestamp)
    });
    
    // Update usage patterns
    await updateLocationMetrics(usage.location, {
      coupons_processed: '+1',
      total_discount_given: `+${usage.discount_applied}`,
      average_order_value: await calculateLocationAOV(usage.location)
    });
  }
};

const calculateTimeToUsage = async (couponCode, usageTimestamp) => {
  const couponDetails = await getCouponDetails(couponCode);
  const redemptionTime = new Date(couponDetails.created_at);
  const usageTime = new Date(usageTimestamp);
  
  return Math.round((usageTime - redemptionTime) / (1000 * 60 * 60)); // Hours
};

Customer Satisfaction Tracking

const handleCustomerSatisfaction = async (webhook) => {
  const { customer, coupon, usage } = webhook;
  
  if (webhook.event === 'coupon.used') {
    // Schedule satisfaction survey after usage
    const surveyDelay = 2 * 60 * 60 * 1000; // 2 hours after usage
    
    await scheduleSurvey({
      customerId: customer.member_id,
      type: 'coupon_satisfaction',
      scheduledFor: new Date(Date.now() + surveyDelay),
      data: {
        couponCode: usage.coupon_code,
        rewardName: usage.reward_name,
        location: usage.location,
        staffMember: usage.staff_member,
        discountValue: usage.discount_applied
      },
      questions: [
        {
          type: 'rating',
          question: 'Como foi sua experiência ao usar o cupom?',
          scale: 5
        },
        {
          type: 'rating',
          question: 'Qual a probabilidade de você recomendar nosso programa de fidelidade?',
          scale: 10 // NPS
        },
        {
          type: 'text',
          question: 'Comentários adicionais (opcional)'
        }
      ]
    });
  }
  
  // Track redemption satisfaction (immediate)
  if (webhook.event === 'coupon.redeemed') {
    await trackImplicitSatisfaction({
      customerId: customer.member_id,
      event: 'redemption',
      rewardType: coupon.discount_details?.discount_type,
      pointsSpent: coupon.points_spent,
      redemptionMethod: coupon.redemption_method
    });
  }
};

Business Intelligence Dashboards

Coupon Performance Metrics

const generateCouponReport = async (timeframe = '30d') => {
  const metrics = await Promise.all([
    // Redemption metrics
    analytics.aggregate('coupon_redeemed', timeframe, {
      groupBy: ['reward_name', 'discount_type'],
      metrics: ['count', 'sum:points_spent', 'avg:discount_value']
    }),
    
    // Usage metrics
    analytics.aggregate('coupon_used', timeframe, {
      groupBy: ['reward_name', 'location', 'platform'],
      metrics: ['count', 'sum:discount_applied', 'avg:order_value']
    }),
    
    // Conversion rates
    calculateCouponConversionRates(timeframe),
    
    // Time to usage analysis
    calculateUsagePatterns(timeframe)
  ]);
  
  return {
    redemptions: metrics[0],
    usage: metrics[1],
    conversion: metrics[2],
    patterns: metrics[3],
    generatedAt: new Date().toISOString()
  };
};

const calculateCouponConversionRates = async (timeframe) => {
  const redemptions = await getCouponRedemptions(timeframe);
  const usages = await getCouponUsages(timeframe);
  
  const conversionByReward = {};
  
  redemptions.forEach(redemption => {
    const key = redemption.reward_id;
    if (!conversionByReward[key]) {
      conversionByReward[key] = {
        reward_name: redemption.reward_name,
        redeemed: 0,
        used: 0,
        conversion_rate: 0
      };
    }
    conversionByReward[key].redeemed += 1;
  });
  
  usages.forEach(usage => {
    const redemption = redemptions.find(r => r.coupon_code === usage.coupon_code);
    if (redemption) {
      const key = redemption.reward_id;
      conversionByReward[key].used += 1;
    }
  });
  
  // Calculate conversion rates
  Object.keys(conversionByReward).forEach(key => {
    const data = conversionByReward[key];
    data.conversion_rate = data.redeemed > 0 ? (data.used / data.redeemed) * 100 : 0;
  });
  
  return Object.values(conversionByReward);
};

Error Handling and Validation

Webhook Validation

const validateCouponWebhook = (webhook) => {
  const errors = [];
  
  // Validate common fields
  if (!webhook.event || !['coupon.redeemed', 'coupon.used'].includes(webhook.event)) {
    errors.push('Invalid or missing event type');
  }
  
  if (!webhook.customer?.member_id) {
    errors.push('Missing customer member_id');
  }
  
  // Validate event-specific fields
  if (webhook.event === 'coupon.redeemed') {
    if (!webhook.coupon?.coupon_code) {
      errors.push('Missing coupon code');
    }
    
    if (!webhook.coupon?.points_spent || webhook.coupon.points_spent <= 0) {
      errors.push('Invalid points_spent value');
    }
    
    if (!webhook.coupon?.expires_at) {
      errors.push('Missing coupon expiration date');
    }
  }
  
  if (webhook.event === 'coupon.used') {
    if (!webhook.usage?.coupon_code) {
      errors.push('Missing usage coupon code');
    }
    
    if (!webhook.usage?.discount_applied || webhook.usage.discount_applied < 0) {
      errors.push('Invalid discount_applied value');
    }
    
    if (!webhook.usage?.order_value || webhook.usage.order_value <= 0) {
      errors.push('Invalid order_value');
    }
  }
  
  return errors;
};

const processCouponWebhook = async (webhook) => {
  try {
    // Validate webhook structure
    const validationErrors = validateCouponWebhook(webhook);
    if (validationErrors.length > 0) {
      throw new Error(`Webhook validation failed: ${validationErrors.join(', ')}`);
    }
    
    // Process based on event type
    const results = await Promise.allSettled([
      handleCouponDelivery(webhook),
      handleInventoryUpdate(webhook),
      detectCouponFraud(webhook),
      trackCouponMetrics(webhook),
      handleCustomerSatisfaction(webhook)
    ]);
    
    // Log any processing failures
    results.forEach((result, index) => {
      if (result.status === 'rejected') {
        console.error(`Coupon webhook handler ${index} failed:`, result.reason);
      }
    });
    
    return { processed: true, errors: results.filter(r => r.status === 'rejected') };
    
  } catch (error) {
    console.error('Coupon webhook processing failed:', error);
    
    // Queue for retry if it's a transient error
    if (isTransientError(error)) {
      await queueWebhookRetry(webhook, error.message);
    }
    
    throw error;
  }
};

Testing Coupon Webhooks

Sample Test Payloads

# Test coupon.redeemed webhook
curl -X POST http://localhost:3000/webhooks/zupy \
  -H "Content-Type: application/json" \
  -H "X-Zupy-Signature: sha256=test_signature" \
  -d '{
    "event": "coupon.redeemed",
    "timestamp": "2025-08-26T14:20:00Z",
    "company_id": "comp_test",
    "customer": {
      "member_id": "ZP-TEST001",
      "whatsapp": "+5511999999999",
      "email": "[email protected]",
      "name": "Test Customer",
      "tier": "bronze"
    },
    "coupon": {
      "coupon_code": "CP-TEST123",
      "reward_id": "rw_test_pizza",
      "reward_name": "Pizza Test Grátis",
      "reward_description": "Pizza test para validação",
      "points_spent": 500,
      "new_balance": 250,
      "expires_at": "2025-09-26T14:20:00Z",
      "terms": "Válido apenas para teste.",
      "redemption_method": "manual"
    },
    "discount_details": {
      "discount_type": "product_free",
      "discount_value": 25.00
    }
  }'

# Test coupon.used webhook  
curl -X POST http://localhost:3000/webhooks/zupy \
  -H "Content-Type: application/json" \
  -H "X-Zupy-Signature: sha256=test_signature" \
  -d '{
    "event": "coupon.used",
    "timestamp": "2025-08-26T18:45:00Z",
    "company_id": "comp_test",
    "customer": {
      "member_id": "ZP-TEST001",
      "whatsapp": "+5511999999999",
      "email": "[email protected]",
      "name": "Test Customer",
      "tier": "bronze"
    },
    "usage": {
      "coupon_code": "CP-TEST123",
      "reward_name": "Pizza Test Grátis",
      "discount_type": "product_free",
      "discount_applied": 25.00,
      "order_value": 45.00,
      "final_value": 20.00,
      "location": "Test Location",
      "staff_member": "Test Staff",
      "platform": "manual",
      "order_id": "ord_test_001",
      "items_affected": [
        {
          "name": "Pizza Test",
          "quantity": 1,
          "unit_price": 25.00,
          "discount_applied": 25.00
        }
      ]
    }
  }'

Unit Test Examples

describe('Coupon Webhook Handlers', () => {
  describe('coupon.redeemed', () => {
    it('should process coupon redemption correctly', async () => {
      const webhook = createTestRedemptionWebhook();
      
      const result = await handleCouponRedeemed(webhook);
      
      expect(result.processed).toBe(true);
      expect(result.deliveryMethods).toContain('email');
      expect(result.deliveryMethods).toContain('whatsapp');
    });
    
    it('should reserve inventory for product_free coupons', async () => {
      const webhook = createTestRedemptionWebhook({
        discount_type: 'product_free',
        reward_name: 'Pizza Margherita'
      });
      
      await handleInventoryUpdate(webhook);
      
      const reservation = await getInventoryReservation(webhook.coupon.coupon_code);
      expect(reservation.product).toBe('Pizza Margherita');
      expect(reservation.quantity).toBe(1);
    });
  });
  
  describe('coupon.used', () => {
    it('should process coupon usage correctly', async () => {
      const webhook = createTestUsageWebhook();
      
      const result = await processCouponWebhook(webhook);
      
      expect(result.processed).toBe(true);
      expect(result.errors).toHaveLength(0);
    });
    
    it('should detect fraud for duplicate usage', async () => {
      const webhook = createTestUsageWebhook();
      
      // Simulate previous usage
      await recordCouponUsage(webhook.usage.coupon_code, '2025-08-25T12:00:00Z');
      
      const fraudResults = await detectCouponFraud(webhook);
      
      expect(fraudResults).toContainEqual(
        expect.objectContaining({
          type: 'duplicate_usage',
          severity: 'high'
        })
      );
    });
  });
});

Performance Optimization

Batch Processing

// Process multiple coupon webhooks efficiently
const processCouponWebhooksBatch = async (webhooks) => {
  const batchSize = 5;
  const batches = chunkArray(webhooks, batchSize);
  
  for (const batch of batches) {
    await Promise.allSettled(
      batch.map(webhook => processCouponWebhook(webhook))
    );
    
    // Small delay to prevent overwhelming external services
    await new Promise(resolve => setTimeout(resolve, 100));
  }
};

// Optimize inventory operations
const batchInventoryUpdates = async (updates) => {
  const groupedUpdates = groupBy(updates, 'product');
  
  const inventoryPromises = Object.entries(groupedUpdates).map(([product, updates]) => {
    const totalQuantity = updates.reduce((sum, update) => sum + update.quantity, 0);
    return inventory.batchUpdate(product, totalQuantity);
  });
  
  await Promise.all(inventoryPromises);
};
Best Practice: Process coupon webhooks asynchronously and return HTTP 200 immediately to ensure reliable webhook delivery from Zupy.
Security: Always validate coupon codes and expiration dates before processing usage events to prevent fraud and abuse.