Skip to main content
This guide helps you troubleshoot issues with webhook delivery and processing.

How Webhooks Work

Fanfare sends HTTP POST requests to your configured endpoint when events occur:
Fanfare Platform  -->  Your Webhook Endpoint
     Event              POST /webhooks/fanfare
                        Headers + JSON Body

Common Issues

Webhooks Not Being Received

Symptoms: Your endpoint isn’t receiving any webhook requests. Checklist:
  1. Verify webhook URL is correct
    • Check for typos in the URL
    • Ensure the URL is publicly accessible
    • HTTPS is required for production
  2. Check endpoint accessibility
    # Test your endpoint is reachable
    curl -X POST https://your-site.com/webhooks/fanfare \
      -H "Content-Type: application/json" \
      -d '{"test": true}'
    
  3. Verify webhook is enabled
    • Check webhook configuration in the dashboard
    • Ensure the webhook is active, not paused
  4. Check firewall rules
    • Fanfare sends requests from specific IP ranges
    • Contact support for current IP allowlist

Signature Verification Failing

Symptoms: Webhook arrives but signature verification fails. Example Verification (Node.js):
import crypto from "crypto";

function verifyWebhookSignature(payload: string, signature: string, secret: string): boolean {
  const expectedSignature = crypto.createHmac("sha256", secret).update(payload).digest("hex");

  return crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expectedSignature));
}

// In your webhook handler
app.post("/webhooks/fanfare", (req, res) => {
  const signature = req.headers["x-fanfare-signature"];
  const rawBody = req.rawBody; // Must be raw string, not parsed JSON

  if (!verifyWebhookSignature(rawBody, signature, WEBHOOK_SECRET)) {
    return res.status(400).json({ error: "Invalid signature" });
  }

  // Process webhook...
});
Common Causes:
  1. Using parsed body instead of raw body
    // Wrong: JSON parsing changes whitespace
    const body = JSON.stringify(req.body);
    
    // Correct: Use raw body as received
    const body = req.rawBody;
    
  2. Wrong webhook secret
    • Each webhook endpoint has its own secret
    • Secrets are different between test and live modes
  3. Middleware modifying the request
    // Express: Capture raw body before JSON parsing
    app.use("/webhooks", express.raw({ type: "application/json" }));
    

Webhook Timeouts

Symptoms: Webhooks fail with timeout errors. Requirements:
  • Respond within 30 seconds
  • Return 2xx status code for success
Solution: Process asynchronously
// Bad: Long processing blocks response
app.post("/webhooks/fanfare", async (req, res) => {
  await processOrder(req.body); // Takes 45 seconds
  res.json({ received: true });
});

// Good: Acknowledge immediately, process async
app.post("/webhooks/fanfare", async (req, res) => {
  // Acknowledge receipt immediately
  res.json({ received: true });

  // Process in background
  processOrder(req.body).catch((error) => {
    console.error("Webhook processing failed:", error);
  });
});

// Better: Use a queue
app.post("/webhooks/fanfare", async (req, res) => {
  // Queue for processing
  await messageQueue.publish("webhooks", req.body);

  res.json({ received: true });
});

Duplicate Webhook Deliveries

Symptoms: Same webhook received multiple times. Causes:
  • Your endpoint returned non-2xx status
  • Network issues caused retry
  • Your endpoint took too long to respond
Solution: Implement idempotency
const processedWebhooks = new Set<string>();

app.post("/webhooks/fanfare", async (req, res) => {
  const eventId = req.headers["x-fanfare-event-id"];

  // Check if already processed
  if (processedWebhooks.has(eventId)) {
    return res.json({ received: true, duplicate: true });
  }

  // Mark as processed (use database in production)
  processedWebhooks.add(eventId);

  // Process webhook
  await processWebhook(req.body);

  res.json({ received: true });
});
Production-ready idempotency:
// Using database for idempotency
async function processWebhook(eventId: string, payload: unknown) {
  // Try to insert - will fail if duplicate
  try {
    await db.webhookEvents.insert({
      eventId,
      payload,
      status: "processing",
      receivedAt: new Date(),
    });
  } catch (error) {
    if (error.code === "23505") {
      // Unique constraint violation - already processed
      return { duplicate: true };
    }
    throw error;
  }

  // Process the webhook
  await handleWebhookPayload(payload);

  // Mark as processed
  await db.webhookEvents.update({
    where: { eventId },
    data: { status: "processed" },
  });
}

Wrong Event Types Received

Symptoms: Receiving unexpected event types. Solution: Filter events in configuration
// Only subscribe to events you need
const webhookConfig = {
  url: "https://your-site.com/webhooks/fanfare",
  events: ["queue.access_granted", "draw.completed", "order.completed"],
};

Webhook Payload Issues

Symptoms: Cannot parse or understand webhook payload. Debug logging:
app.post("/webhooks/fanfare", (req, res) => {
  // Log full webhook for debugging
  console.log("Webhook received:", {
    headers: req.headers,
    body: req.body,
    rawBody: req.rawBody?.toString(),
  });

  res.json({ received: true });
});
Payload structure:
{
  "id": "evt_abc123",
  "type": "queue.access_granted",
  "created": "2024-01-15T10:30:00Z",
  "data": {
    "queueId": "queue_xyz",
    "consumerId": "consumer_123",
    "handoffToken": "hoff_abc",
    "expiresAt": "2024-01-15T10:45:00Z"
  },
  "organization": {
    "id": "org_abc",
    "name": "Your Store"
  }
}

Testing Webhooks

Local Development

Use a tunneling service to receive webhooks locally:
# Using ngrok
ngrok http 3000

# Using cloudflared
cloudflared tunnel --url http://localhost:3000
Configure the tunnel URL in your webhook settings.

Manual Testing

Send test webhooks from the dashboard or use the CLI:
# Trigger test webhook
fanfare webhooks test --endpoint webhook_abc123

Webhook Log Review

Check webhook delivery history in the dashboard:
  1. Go to Settings > Webhooks
  2. Select your endpoint
  3. View Delivery Attempts
Each attempt shows:
  • Request payload
  • Response received
  • Status code
  • Timing information

Webhook Event Reference

Queue Events

EventDescription
queue.consumer_enteredConsumer joined the queue
queue.position_updatedConsumer’s position changed
queue.access_grantedConsumer granted checkout access
queue.access_expiredConsumer’s access window expired
queue.consumer_leftConsumer left the queue

Draw Events

EventDescription
draw.consumer_enteredConsumer registered for draw
draw.completedDraw selection completed
draw.winner_selectedIndividual winner notification
draw.consumer_leftConsumer withdrew from draw

Order Events

EventDescription
order.createdOrder was created
order.completedOrder was completed
order.cancelledOrder was cancelled

Error Recovery

Retry Policy

Fanfare retries failed webhooks with exponential backoff:
AttemptDelay
1Immediate
21 minute
35 minutes
430 minutes
52 hours
68 hours
724 hours
After 7 failed attempts, the webhook is marked as failed.

Manual Retry

Retry failed webhooks from the dashboard:
  1. Go to webhook delivery history
  2. Find the failed delivery
  3. Click Retry

Webhook Replay

For missed webhooks, use the replay feature:
# Replay events from a time range
fanfare webhooks replay \
  --endpoint webhook_abc123 \
  --from 2024-01-15T00:00:00Z \
  --to 2024-01-15T12:00:00Z

Best Practices

Always Respond Quickly

// Acknowledge immediately
res.status(200).json({ received: true });

// Then process

Use Idempotency Keys

// Store and check event IDs
const eventId = req.headers["x-fanfare-event-id"];

Validate Signatures

// Always verify webhook authenticity
if (!verifySignature(req)) {
  return res.status(400).json({ error: "Invalid signature" });
}

Handle All Event Types

switch (event.type) {
  case "queue.access_granted":
    await handleAccessGranted(event.data);
    break;
  // ... other cases
  default:
    console.log("Unhandled event type:", event.type);
}

Monitor Webhook Health

  • Set up alerts for failed webhooks
  • Monitor response times
  • Track success rates