Reevit

Webhooks

Receive real-time notifications when payment events occur

Webhooks

Webhooks are HTTP callbacks that notify your application in real-time when payment events occur. Instead of constantly polling the API to check payment status, Reevit pushes events to your server as they happen.

https://dashboard.reevit.io
Live

Webhook Endpoint

Configure where Reevit sends webhook events

HTTPSyourapp.com/webhooks

Signing Secret

Use this secret to verify webhook signatures

whsec_9l2k...••••••••••••••••

Subscribed Events

Events currently being sent to your endpoint

payment.succeededpayment.failedsubscription.createdsubscription.cancelledpayout.completed
Recent Events
payment.succeeded
Just now
200
subscription.created
3m ago
200
payment.failed
10m ago
500

Understanding Webhook Architecture

Reevit handles two types of webhooks. Understanding the difference is crucial:

Inbound Webhooks (PSP → Reevit)

These are webhooks from payment providers (Paystack, Flutterwave, M-Pesa, etc.) to Reevit.

Key Points:

  • Reevit handles these automatically - you don't need to do anything
  • Configure the Reevit webhook URL in each PSP's dashboard
  • Reevit processes provider events and updates payment status

PSP Webhook URLs:

ProviderWebhook URL to configure in PSP Dashboard
Paystackhttps://api.reevit.com/webhooks/paystack
Flutterwavehttps://api.reevit.com/webhooks/flutterwave
Hubtelhttps://api.reevit.com/webhooks/hubtel
M-Pesahttps://api.reevit.com/webhooks/mpesa
Stripehttps://api.reevit.com/webhooks/stripe

You configure these URLs in the payment provider's dashboard (e.g., Paystack Dashboard), NOT in Reevit.

Required Metadata for Webhook Routing

Critical: For inbound webhooks to route correctly, your payments must include specific metadata fields when created. Without these fields, webhooks will fail with a 400 Bad Request error.

When creating payments (via SDK or API), you must include the following metadata fields:

FieldRequiredDescription
org_id✅ YesYour organization ID (from Reevit Dashboard)
connection_id✅ YesThe connection ID used for the payment
payment_id✅ YesYour internal payment/order ID (Required for Stripe & routing)
customer_emailOptionalCustomer email for receipts

Example: SDK Usage

<ReevitCheckout
  publicKey="pfk_live_xxx"
  amount={5000}
  currency="GHS"
  email="customer@example.com"
  metadata={{
    org_id: "org_abc123",           // Required for webhook routing
    connection_id: "conn_xyz789",    // Required for webhook routing
    payment_id: "order_12345",       // Your internal reference
    customer_email: "customer@example.com"
  }}
  // ...
/>

Example: API Usage

curl -X POST https://api.reevit.com/v1/payments/intents \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_abc123" \
  -d '{
    "amount": 5000,
    "currency": "GHS",
    "method": "card",
    "country": "GH",
    "metadata": {
      "org_id": "org_abc123",
      "connection_id": "conn_xyz789",
      "payment_id": "order_12345"
    }
  }'

When using Payment Links, Reevit automatically populates these fields from the payment link configuration. You don't need to manually set them.

Outbound Webhooks (Reevit → Your App)

These are webhooks from Reevit to your application.

Key Points:

  • You must create a webhook handler in your application
  • Configure your webhook URL in the Reevit Dashboard
  • Reevit sends you normalized events (same format regardless of PSP)
  • You verify signatures using your organization's signing secret

This is the webhook you configure in Reevit Dashboard > Developers > Webhooks.


Why Webhooks Matter

The Problem with Polling

Without webhooks, you'd need to:

  • Poll the API every few seconds to check payment status
  • Waste server resources on unnecessary requests
  • Experience delays between payment completion and order fulfillment
  • Miss events if your polling interval is too long

Example: Customer completes payment at 10:00:00, but you only poll at 10:00:30 → 30-second delay before order fulfillment.

The Webhook Solution

With webhooks:

  • Instant notifications: Know immediately when payments succeed or fail
  • Efficient: No wasted API calls
  • Reliable: Events are delivered even if you're not actively checking
  • Scalable: Works with thousands of payments per minute

Result: Faster order fulfillment, better customer experience, lower server costs.


How Outbound Webhooks Work

The Flow

1. Payment Event Occurs → 2. Reevit Sends Webhook → 3. Your Server Processes → 4. You Respond
  1. Event Occurs: Payment succeeds, fails, or changes status
  2. Reevit Sends: POST request to your webhook URL with event data
  3. Your Server Processes: Verify signature, update database, fulfill order
  4. You Respond: Return 2xx status code to acknowledge receipt

Key Concepts

  • Webhook URL: Your server endpoint that receives events
  • Signing Secret: Unique secret per organization for signature verification
  • Event Types: Different events for different payment states
  • Signatures: Cryptographic signatures to verify events are from Reevit
  • Idempotency: Events include IDs to prevent duplicate processing
  • Retries: Failed deliveries are automatically retried

Setting Up Webhooks

  1. Go to Settings → Webhooks in your Reevit dashboard
  2. Enter your webhook endpoint URL (must be HTTPS)
  3. Select which events you want to receive
  4. Copy the webhook signing secret
  5. Save configuration

Benefits:

  • Visual interface
  • Easy to update
  • See delivery status

Via API

curl -X POST https://api.reevit.com/v1/webhooks/config \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_123" \
  -H "Idempotency-Key: webhook-config-001" \
  -d '{
    "url": "https://yourapp.com/webhooks/reevit",
    "events": [
      "payment.succeeded",
      "payment.failed",
      "payment.refunded",
      "subscription.renewed"
    ],
    "enabled": true
  }'

Webhook URL Requirements

  • HTTPS only: HTTP endpoints are not supported for security
  • Publicly accessible: Must be reachable from the internet
  • Fast response: Return 2xx within 30 seconds
  • Idempotent: Handle duplicate events gracefully

Get Configuration

curl https://api.reevit.com/v1/webhooks/config \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_123"

Response:

{
  "url": "https://yourapp.com/webhooks/reevit",
  "events": ["payment.succeeded", "payment.failed"],
  "enabled": true,
  "signing_secret": "whsec_xxx",
  "created_at": "2025-02-10T10:00:00Z"
}

Event Types

Reevit sends different events for different scenarios. Subscribe only to events you need.

Payment Events

EventWhen It FiresUse Case
payment.createdPayment intent createdLog payment attempts
payment.succeededPayment completed successfullyFulfill order, send confirmation
payment.failedPayment failedNotify customer, allow retry
payment.canceledPayment canceledRelease inventory, update order
payment.refundedRefund completedUpdate order status, notify customer

Subscription Events

EventWhen It FiresUse Case
subscription.createdNew subscription createdActivate subscription, send welcome email
subscription.updatedSubscription modifiedUpdate subscription status
subscription.canceledSubscription canceledDeactivate access, send cancellation email
subscription.pausedSubscription pausedSuspend access temporarily
subscription.resumedSubscription resumedReactivate access

Invoice Events

EventWhen It FiresUse Case
invoice.createdInvoice generatedNotify customer of upcoming charge
invoice.paidInvoice payment succeededExtend subscription, grant access
invoice.failedInvoice payment failedNotify customer, retry payment
invoice.canceledInvoice canceledUpdate subscription status

Webhook Payload

Every webhook includes a consistent payload structure:

{
  "id": "evt_abc123",
  "type": "payment.succeeded",
  "org_id": "org_123",
  "created_at": "2025-02-10T10:05:00Z",
  "data": {
    "id": "pay_xyz789",
    "amount": 5000,
    "currency": "GHS",
    "status": "succeeded",
    "provider": "paystack",
    "customer_id": "cust_456",
    "metadata": {
      "order_id": "12345"
    }
  }
}

Payload Fields

  • id: Unique event identifier (use for idempotency)
  • type: Event type (e.g., payment.succeeded)
  • org_id: Your organization ID
  • created_at: When the event occurred (ISO 8601)
  • data: Event-specific data (varies by event type)

Using Event Data

Example: Fulfill Order on Payment Success

if (event.type === 'payment.succeeded') {
  const payment = event.data;
  const orderId = payment.metadata.order_id;
  
  // Fulfill order
  await fulfillOrder(orderId);
  
  // Send confirmation email
  await sendConfirmationEmail(payment.customer_id);
  
  // Update analytics
  await trackPurchase(payment);
}

🔒 Webhook Security

Webhooks deliver sensitive payment data to your application. Follow these security practices to protect your integration.

Signature Verification (Critical)

Never process webhooks without signature verification. This is the most critical security step.

Reevit signs each webhook with HMAC-SHA256. Your server must verify this signature before processing any event.

What happens without verification?

  • Attackers could send fake "payment succeeded" events
  • Orders could be fulfilled without actual payment
  • Financial losses and fraud

Anti-Replay Protection

Reebit's webhooks include a timestamp field to prevent replay attacks:

const WEBHOOK_TOLERANCE = 300; // 5 minutes

function verifyWebhook(payload: string, signature: string, secret: string): boolean {
  // 1. Verify signature
  if (!verifySignature(payload, signature, secret)) {
    return false;
  }

  // 2. Check timestamp to prevent replay attacks
  const parsed = JSON.parse(payload);
  const timestamp = parsed.timestamp || 0;
  const now = Math.floor(Date.now() / 1000);

  if (Math.abs(now - timestamp) > WEBHOOK_TOLERANCE) {
    console.log('Webhook timestamp outside tolerance window');
    return false;
  }

  return true;
}

Best Practices

  1. Always use raw request body for verification

    // ❌ Wrong - Express parses body, breaking signature
    app.post('/webhooks', (req, res) => {
      verifySignature(req.body, req.headers['x-reevit-signature'], secret);
    });
    
    // ✅ Correct - use raw body
    app.post('/webhooks', express.raw({ type: 'application/json' }), (req, res) => {
      const signature = req.headers['x-reevit-signature'];
      verifySignature(req.body.toString(), signature, secret);
    });
  2. Respond quickly (under 5 seconds)

    • Return 2xx immediately after verification
    • Process events asynchronously
    • Reevit will retry failed deliveries
  3. Implement idempotent handlers

    async function handleWebhook(event) {
      const eventId = event.id;
    
      // Check if already processed
      const processed = await db.webhookEvents.findUnique({ where: { id: eventId } });
      if (processed) {
        return res.send({ status: 'already_processed' });
      }
    
      // Mark as processing
      await db.webhookEvents.create({ id: eventId, status: 'processing' });
    
      try {
        await processEvent(event);
        await db.webhookEvents.update({ where: { id: eventId }, data: { status: 'completed' } });
      } catch (err) {
        await db.webhookEvents.update({ where: { id: eventId }, data: { status: 'failed' } });
        throw err;
      }
    }
  4. Use HTTPS endpoints

    • Webhook URLs must use HTTPS
    • Reevit rejects HTTP endpoints
    • Use valid TLS certificates (not self-signed)
  5. Limit webhook IP ranges

    • Reevit webhooks come from Cloudflare IPs
    • Configure firewall to allow only these ranges

Security Checklist

Review this checklist before going live:

  • Signature verification implemented
  • Anti-replay protection (timestamp check)
  • Idempotent event handlers
  • HTTPS endpoint with valid certificate
  • Fast response time (< 5 seconds)
  • Proper error logging without sensitive data
  • Webhook secret stored securely (env vars, vault)
  • Test endpoint configured in Reevit Dashboard

Verifying Webhooks

Always verify webhook signatures. This ensures events are actually from Reevit and haven't been tampered with.

Why Verification Matters

Without verification, attackers could:

  • Send fake payment success events
  • Trigger order fulfillment without payment
  • Cause financial losses
  • Compromise your system

How Verification Works

Reevit signs each webhook using HMAC-SHA256 with your organization's signing secret:

  1. Header: X-Reevit-Signature: sha256=<hex-signature>
  2. Signature: HMAC-SHA256(request_body, signing_secret)
  3. Verification: Compare using constant-time comparison

Getting Your Signing Secret

Your signing secret is automatically generated when you first configure a webhook endpoint:

  1. Go to Reevit Dashboard > Developers > Webhooks
  2. Add your webhook endpoint URL
  3. Click the copy button next to "Signing Secret"
  4. Add to your environment: REEVIT_WEBHOOK_SECRET=whsec_xxx...

The signing secret is unique per organization and starts with whsec_. It's generated once and persists across webhook URL updates.

// app/api/webhooks/reevit/route.ts
import { NextRequest, NextResponse } from "next/server";
import crypto from "crypto";

function verifySignature(payload: string, signature: string, secret: string): boolean {
  if (!signature.startsWith("sha256=")) return false;
  
  const expected = crypto
    .createHmac("sha256", secret)
    .update(payload)
    .digest("hex");
  
  const received = signature.slice(7); // Remove "sha256=" prefix
  if (received.length !== expected.length) return false;
  
  return crypto.timingSafeEqual(Buffer.from(received), Buffer.from(expected));
}

export async function POST(request: NextRequest) {
  const rawBody = await request.text();
  const signature = request.headers.get("x-reevit-signature") || "";
  const secret = process.env.REEVIT_WEBHOOK_SECRET!;

  // Verify signature
  if (!verifySignature(rawBody, signature, secret)) {
    return NextResponse.json({ error: "Invalid signature" }, { status: 401 });
  }

  const event = JSON.parse(rawBody);

  // Handle events
  switch (event.type) {
    case "reevit.webhook.test":
      console.log("Test webhook received");
      break;
    case "payment.succeeded":
      await handlePaymentSucceeded(event.data);
      break;
    case "payment.failed":
      await handlePaymentFailed(event.data);
      break;
  }

  return NextResponse.json({ received: true });
}

async function handlePaymentSucceeded(data: any) {
  const orderId = data.metadata?.order_id;
  // Update order status, send confirmation email, etc.
  console.log(`Payment ${data.id} succeeded for order ${orderId}`);
}

async function handlePaymentFailed(data: any) {
  // Notify customer, allow retry, etc.
  console.log(`Payment ${data.id} failed`);
}

Express.js Example

const crypto = require('crypto');
const express = require('express');

function verifySignature(payload, signature, secret) {
  if (!signature.startsWith('sha256=')) return false;
  
  const expected = crypto
    .createHmac('sha256', secret)
    .update(payload)
    .digest('hex');
  
  const received = signature.slice(7);
  if (received.length !== expected.length) return false;
  
  return crypto.timingSafeEqual(
    Buffer.from(received),
    Buffer.from(expected)
  );
}

// IMPORTANT: Use raw body for signature verification
app.post('/webhooks/reevit', express.raw({ type: 'application/json' }), (req, res) => {
  const signature = req.headers['x-reevit-signature'];
  const payload = req.body.toString();
  const secret = process.env.REEVIT_WEBHOOK_SECRET;
  
  if (!verifySignature(payload, signature, secret)) {
    return res.status(401).json({ error: 'Invalid signature' });
  }
  
  const event = JSON.parse(payload);
  
  switch (event.type) {
    case 'payment.succeeded':
      // Fulfill order
      break;
    case 'payment.failed':
      // Notify customer
      break;
  }
  
  res.status(200).json({ received: true });
});

Go Example

package main

import (
    "crypto/hmac"
    "crypto/sha256"
    "encoding/hex"
    "encoding/json"
    "io"
    "net/http"
    "os"
    "strings"
)

func verifySignature(payload []byte, signature, secret string) bool {
    if !strings.HasPrefix(signature, "sha256=") {
        return false
    }
    
    mac := hmac.New(sha256.New, []byte(secret))
    mac.Write(payload)
    expected := hex.EncodeToString(mac.Sum(nil))
    received := signature[7:] // Remove "sha256=" prefix
    
    return hmac.Equal([]byte(received), []byte(expected))
}

func webhookHandler(w http.ResponseWriter, r *http.Request) {
    body, _ := io.ReadAll(r.Body)
    signature := r.Header.Get("X-Reevit-Signature")
    secret := os.Getenv("REEVIT_WEBHOOK_SECRET")
    
    if !verifySignature(body, signature, secret) {
        http.Error(w, "Invalid signature", http.StatusUnauthorized)
        return
    }
    
    var event map[string]interface{}
    json.Unmarshal(body, &event)
    
    // Handle event based on type
    switch event["type"] {
    case "payment.succeeded":
        // Fulfill order
    case "payment.failed":
        // Notify customer
    }
    
    w.WriteHeader(http.StatusOK)
    json.NewEncoder(w).Encode(map[string]bool{"received": true})
}

Python (Flask) Example

import hmac
import hashlib
import os
from flask import Flask, request, jsonify

app = Flask(__name__)

def verify_signature(payload: bytes, signature: str, secret: str) -> bool:
    if not signature.startswith('sha256='):
        return False
    
    expected = hmac.new(
        secret.encode('utf-8'),
        payload,
        hashlib.sha256
    ).hexdigest()
    
    received = signature[7:]  # Remove "sha256=" prefix
    return hmac.compare_digest(received, expected)

@app.route('/webhooks/reevit', methods=['POST'])
def webhook():
    payload = request.get_data()
    signature = request.headers.get('X-Reevit-Signature', '')
    secret = os.environ['REEVIT_WEBHOOK_SECRET']
    
    if not verify_signature(payload, signature, secret):
        return jsonify({'error': 'Invalid signature'}), 401
    
    event = request.get_json()
    
    if event['type'] == 'payment.succeeded':
        # Fulfill order
        pass
    elif event['type'] == 'payment.failed':
        # Notify customer
        pass
    
    return jsonify({'received': True})

PHP Example

<?php

function verifySignature(string $payload, string $signature, string $secret): bool {
    if (strpos($signature, 'sha256=') !== 0) {
        return false;
    }
    
    $expected = hash_hmac('sha256', $payload, $secret);
    $received = substr($signature, 7); // Remove "sha256=" prefix
    
    return hash_equals($expected, $received);
}

// Webhook handler
$payload = file_get_contents('php://input');
$signature = $_SERVER['HTTP_X_REEVIT_SIGNATURE'] ?? '';
$secret = getenv('REEVIT_WEBHOOK_SECRET');

if (!verifySignature($payload, $signature, $secret)) {
    http_response_code(401);
    echo json_encode(['error' => 'Invalid signature']);
    exit;
}

$event = json_decode($payload, true);

switch ($event['type']) {
    case 'payment.succeeded':
        // Fulfill order
        break;
    case 'payment.failed':
        // Notify customer
        break;
}

http_response_code(200);
echo json_encode(['received' => true]);

Processing Webhooks

Best Practices

  1. Respond Quickly: Return 2xx within 30 seconds
  2. Process Async: Queue events for background processing
  3. Handle Duplicates: Use event ID for idempotency
  4. Verify Signatures: Always verify webhook authenticity
  5. Log Events: Log all events for debugging and audit

Idempotency

Events include unique IDs. Use these to prevent duplicate processing:

// Check if event already processed
const existingEvent = await db.events.findOne({ id: event.id });
if (existingEvent) {
  return res.status(200).send('Already processed');
}

// Process event
await processEvent(event);

// Store event ID
await db.events.insert({ id: event.id, processed_at: new Date() });

Error Handling

Return 2xx for success: Even if processing fails internally, return 2xx to acknowledge receipt. Process failures in background.

Return 4xx/5xx for retry: Only return error codes if you want Reevit to retry:

  • 4xx: Client error (won't retry)
  • 5xx: Server error (will retry)

Example:

app.post('/webhooks/reevit', async (req, res) => {
  try {
    // Verify signature
    if (!verifyWebhook(req.body, signature, secret)) {
      return res.status(401).send('Invalid signature');
    }
    
    const event = JSON.parse(req.body);
    
    // Queue for processing (don't block webhook response)
    await queue.add('process-webhook', event);
    
    // Return success immediately
    res.status(200).send('OK');
  } catch (error) {
    // Log error but still return 200
    console.error('Webhook error:', error);
    res.status(200).send('OK');
  }
});

Testing Webhooks

Test Events

Send a test event to verify your endpoint:

curl -X POST https://api.reevit.com/v1/webhooks/test \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_123"

Use Cases:

  • Initial setup: Verify endpoint is working
  • After changes: Test after updating webhook URL
  • Debugging: Test signature verification

Local Development

For local development, use tools like:

  • ngrok: Expose local server to internet
  • webhook.site: Temporary webhook URLs for testing
  • Stripe CLI: Forward webhooks to local server

Example with ngrok:

# Start ngrok
ngrok http 3000

# Use ngrok URL in webhook config
# https://abc123.ngrok.io/webhooks/reevit

Viewing Webhook Events

List Events

curl "https://api.reevit.com/v1/webhooks/events?limit=20" \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_123"

Use Cases:

  • Debugging: See what events were sent
  • Audit: Review event history
  • Monitoring: Check event delivery

Get Single Event

curl https://api.reevit.com/v1/webhooks/events/evt_abc123 \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_123"

Use Cases:

  • Investigation: Review specific event details
  • Replay: Get event data for replaying

Replaying Events

Re-send a webhook event if delivery failed or you need to reprocess:

curl -X POST https://api.reevit.com/v1/webhooks/events/evt_abc123/replay \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_123"

Use Cases:

  • Failed delivery: Retry after fixing endpoint
  • Reprocessing: Reprocess event after bug fix
  • Testing: Test event processing logic

Delivery Status

View Delivery Attempts

curl https://api.reevit.com/v1/webhooks/outbound \
  -H "X-Reevit-Key: pfk_live_xxx" \
  -H "X-Org-Id: org_123"

Response:

{
  "deliveries": [
    {
      "id": "del_123",
      "event_id": "evt_abc123",
      "url": "https://yourapp.com/webhooks/reevit",
      "status": "delivered",
      "response_code": 200,
      "attempts": 1,
      "delivered_at": "2025-02-10T10:05:01Z"
    },
    {
      "id": "del_124",
      "event_id": "evt_def456",
      "url": "https://yourapp.com/webhooks/reevit",
      "status": "failed",
      "response_code": 500,
      "attempts": 5,
      "last_attempt_at": "2025-02-10T10:10:00Z"
    }
  ]
}

Delivery Statuses

  • delivered: Successfully delivered and acknowledged
  • pending: Queued for delivery
  • failed: Failed after all retry attempts
  • retrying: Currently retrying after failure

Retry Behavior

Reevit automatically retries failed webhook deliveries with exponential backoff:

AttemptDelayTotal Time
1Immediate0s
21 minute1m
35 minutes6m
430 minutes36m
52 hours2h 36m

After 5 failed attempts, the event is marked as failed and won't be retried automatically.

Why Retries Matter

  • Network issues: Temporary network problems resolve themselves
  • Server restarts: Your server may be restarting
  • Rate limiting: Temporary rate limits may clear
  • Transient errors: Some errors are temporary

Best Practice: Make your webhook endpoint idempotent so retries are safe.


Security Best Practices

  1. Always Verify Signatures: Never process unverified webhooks
  2. Use HTTPS: Only accept webhooks over HTTPS
  3. Validate Event Data: Don't trust event data blindly
  4. Rate Limiting: Implement rate limiting on webhook endpoint
  5. Monitor Failures: Alert on repeated webhook failures
  6. Keep Secrets Secure: Never expose webhook secrets
  7. Use Idempotency: Handle duplicate events gracefully

Common Patterns

Order Fulfillment

if (event.type === 'payment.succeeded') {
  const orderId = event.data.metadata.order_id;
  await fulfillOrder(orderId);
  await sendConfirmationEmail(event.data.customer_id);
}

Subscription Management

if (event.type === 'subscription.renewed') {
  const subscription = event.data;
  await extendAccess(subscription.customer_id);
  await sendRenewalConfirmation(subscription);
}

Refund Processing

if (event.type === 'payment.refunded') {
  const refund = event.data;
  await updateOrderStatus(refund.metadata.order_id, 'refunded');
  await notifyCustomer(refund.customer_id, 'refund_processed');
}

Failed Payment Handling

if (event.type === 'payment.failed') {
  const payment = event.data;
  await notifyCustomer(payment.customer_id, 'payment_failed');
  await allowRetry(payment.id);
}

Troubleshooting

Webhooks Not Received

  1. Check URL: Verify webhook URL is correct and accessible
  2. Check HTTPS: Ensure URL uses HTTPS
  3. Check Firewall: Ensure firewall allows Reevit IPs
  4. Check Logs: Review delivery logs for errors
  5. Test Endpoint: Use test endpoint to verify connectivity

Signature Verification Failing

  1. Check Secret: Verify webhook secret is correct
  2. Check Payload: Ensure you're using raw request body
  3. Check Encoding: Ensure proper string encoding
  4. Check Headers: Verify signature header name

Duplicate Events

  1. Use Event IDs: Check event ID before processing
  2. Implement Idempotency: Make handlers idempotent
  3. Check Retries: Verify retry logic isn't causing duplicates

Next Steps