Skip to main content

Webhook Testing Guide

Development

Test webhooks locally during development, use the webhook inspector to monitor deliveries, and debug common delivery issues.

Local Testing

Receive webhooks on localhost with tunneling

Webhook Inspector

Monitor deliveries in real-time from the console

Test Events

Trigger sample events to verify your endpoint

Debugging Tools

View request/response details for failed deliveries

Testing Webhooks Locally

During development, you need a way to receive webhooks on your local machine. Since localhost is not accessible from the internet, you will need to use a tunneling service.

Option 1: ngrok (Recommended)

ngrok creates a secure tunnel from a public URL to your local server. It is the most popular option with a generous free tier.

1

Install ngrok

# Using Homebrew
brew install ngrok/ngrok/ngrok

# Or download from https://ngrok.com/download
2

Authenticate (optional, for persistent URLs)

# Sign up at https://ngrok.com and get your authtoken
ngrok config add-authtoken YOUR_AUTHTOKEN
3

Start your local server

# Start your development server
npm run dev
# Server running at http://localhost:3000
4

Start ngrok tunnel

# Create a tunnel to your local server
ngrok http 3000

# Output:
# Forwarding    https://abc123.ngrok.io -> http://localhost:3000
5

Configure your webhook

Use the ngrok HTTPS URL as your webhook endpoint in the console.

https://abc123.ngrok.io/api/webhooks

ngrok Web Interface

ngrok provides a local web interface at http://localhost:4040 where you can inspect all requests, replay them, and see response details.

Option 2: Cloudflare Tunnel

Cloudflare Tunnel (cloudflared) is a free alternative that integrates with Cloudflare security features.

Quick Start
# Install cloudflared
brew install cloudflare/cloudflare/cloudflared

# Create a quick tunnel (no account required)
cloudflared tunnel --url http://localhost:3000

# Output:
# Your quick Tunnel has been created!
# https://random-words.trycloudflare.com

Option 3: localtunnel

localtunnel is an open-source alternative that requires no signup.

Quick Start
# Install localtunnel globally
npm install -g localtunnel

# Create a tunnel
lt --port 3000

# Output:
# your url is: https://brave-fish-42.loca.lt
ToolFree TierPersistent URLsWeb Inspector
ngrokYesPaidYes
Cloudflare TunnelYesYes (with account)Via Cloudflare
localtunnelYesNoNo

Using the Webhook Inspector

The webhook inspector in the Sylphx console provides real-time visibility into all webhook deliveries, including request/response details and retry status.

Webhook Inspector

Console → Webhooks → Select webhook → Deliveries

Filter & Search

Filter by status, event type, or date range

Request Details

View full request headers and payload

Retry Controls

Manually retry failed deliveries

Response Info

See status codes, response body, and timing

Delivery Status Indicators

Delivered

2xx response received

Failed

Non-2xx or timeout

Pending

Awaiting retry

Skipped

Event filtered

Programmatic Access

You can also access delivery logs programmatically via the SDK or API.

Fetching Delivery Logs
import { platform } from '@/lib/platform'

// Get recent deliveries for a webhook
const deliveries = await platform.webhooks.getDeliveries('whk_xyz789', {
  limit: 50,
  status: 'failed', // 'all' | 'success' | 'failed' | 'pending'
  eventType: 'user.created', // optional filter
  from: new Date('2024-01-01'),
  to: new Date(),
})

// Inspect a specific delivery
for (const delivery of deliveries) {
  console.log({
    id: delivery.id,
    eventType: delivery.eventType,
    status: delivery.status,
    statusCode: delivery.statusCode,
    duration: delivery.duration, // ms
    attempt: delivery.attempt,
    error: delivery.error,
    requestBody: delivery.requestBody,
    responseBody: delivery.responseBody,
    createdAt: delivery.createdAt,
  })
}

// Retry a failed delivery
await platform.webhooks.retryDelivery(deliveryId)

// Retry all failed deliveries for a webhook
await platform.webhooks.retryAllFailed('whk_xyz789')

Sending Test Events

Send test events to verify your webhook endpoint is working correctly before relying on it for production events.

From the Console

1

Navigate to your webhook

Go to Console → Webhooks → Select your webhook

2

Click 'Send Test Event'

Choose an event type from the dropdown (e.g., user.created)

3

View the result

The test delivery will appear in your delivery log with full request/response details

From the SDK

Sending Test Events
import { platform } from '@/lib/platform'

// Send a test event to a specific webhook
const result = await platform.webhooks.test('whk_xyz789', {
  eventType: 'user.created', // optional, defaults to test.ping
})

console.log({
  success: result.success,
  statusCode: result.statusCode,
  duration: result.duration,
  responseBody: result.responseBody,
  error: result.error,
})

// Send a specific test payload
const customResult = await platform.webhooks.test('whk_xyz789', {
  eventType: 'payment.succeeded',
  payload: {
    id: 'pay_test123',
    user_id: 'usr_test456',
    amount: 2900,
    currency: 'usd',
  },
})

Test Event Payloads

Test events use realistic sample data but are marked with a test flag in the metadata.

Test Event Payload
{
  "id": "evt_test_1234567890",
  "type": "user.created",
  "timestamp": "2024-01-15T10:30:00.000Z",
  "version": "2024-01-01",
  "app_id": "app_xyz789",
  "environment": "development",
  "data": {
    "id": "usr_test_abc123",
    "email": "test@example.com",
    "name": "Test User",
    "created_at": "2024-01-15T10:30:00.000Z"
  },
  "metadata": {
    "webhook_id": "whk_xyz789",
    "attempt": 1,
    "max_attempts": 1,
    "test": true  // <-- Indicates this is a test event
  }
}

Handling Test Events

Check for metadata.test === true if you want to handle test events differently (e.g., skip database writes or external API calls).

Viewing Delivery Logs

Delivery logs contain detailed information about each webhook delivery attempt, including timing, status codes, and any errors.

Log Entry Details

PropertyTypeDescription
idrequiredstringUnique delivery attempt identifier
webhook_idrequiredstringAssociated webhook
event_idrequiredstringEvent that triggered the delivery
event_typerequiredstringType of event (e.g., user.created)
statusrequired"success" | "failed" | "pending"Delivery status
status_codenumber | nullHTTP response status code
durationnumberRequest duration in milliseconds
attemptrequirednumberAttempt number (1-5)
errorstring | nullError message if failed
request_urlrequiredstringWebhook endpoint URL
request_headersobjectHeaders sent with the request
request_bodyrequiredstringJSON payload sent
response_headersobject | nullHeaders received in response
response_bodystring | nullResponse body (truncated at 10KB)
created_atrequiredstringWhen the delivery was attempted
next_retry_atstring | nullWhen the next retry will occur

Filtering Logs

Filtering Delivery Logs
// Get failed deliveries from the last 24 hours
const failures = await platform.webhooks.getDeliveries(webhookId, {
  status: 'failed',
  from: new Date(Date.now() - 24 * 60 * 60 * 1000),
  limit: 100,
})

// Get deliveries for a specific event type
const userEvents = await platform.webhooks.getDeliveries(webhookId, {
  eventType: 'user.*',
  limit: 50,
})

// Get all deliveries with pagination
let cursor: string | undefined
do {
  const { deliveries, nextCursor } = await platform.webhooks.getDeliveries(
    webhookId,
    { limit: 100, cursor }
  )

  for (const delivery of deliveries) {
    console.log(delivery)
  }

  cursor = nextCursor
} while (cursor)

Debugging Failed Deliveries

When webhooks fail, use these techniques to diagnose and resolve the issue.

Common Failure Reasons

Connection timeout

Endpoint took too long to respond

Fix: Optimize your handler to respond within 30 seconds. Process webhooks asynchronously.

Connection refused

Server not running or firewall blocking

Fix: Verify your server is running and accessible. Check firewall rules and security groups.

SSL certificate error

Invalid or expired certificate

Fix: Install a valid SSL certificate from a trusted CA. Ensure it is not expired.

4xx status code

Client error (bad request, unauthorized)

Fix: Check your signature verification. Ensure endpoint accepts POST requests with JSON.

5xx status code

Server error in your handler

Fix: Check your server logs for exceptions. Add error handling to your webhook handler.

DNS resolution failed

Domain not found

Fix: Verify the webhook URL is correct and DNS is properly configured.

Debugging Checklist

Verify your endpoint URL is correct and uses HTTPS
Check that your server is running and accessible from the internet
Confirm your SSL certificate is valid and not expired
Ensure your endpoint accepts POST requests with Content-Type: application/json
Verify your signature verification code is correct
Check your server logs for exceptions or errors
Confirm you are returning a 2xx status code within 30 seconds
Test your endpoint manually with curl or Postman

Testing with curl

Test your endpoint manually to isolate issues.

Manual Testing
# Test basic connectivity
curl -X POST https://your-endpoint.com/webhooks \
  -H "Content-Type: application/json" \
  -d '{"test": true}'

# Test with a sample webhook payload
curl -X POST https://your-endpoint.com/webhooks \
  -H "Content-Type: application/json" \
  -H "X-Webhook-Signature: t=1705315800,v1=test" \
  -d '{
    "id": "evt_test",
    "type": "test.ping",
    "timestamp": "2024-01-15T10:30:00.000Z",
    "data": {}
  }'

# Check response headers and timing
curl -X POST https://your-endpoint.com/webhooks \
  -H "Content-Type: application/json" \
  -w "\nStatus: %{http_code}\nTime: %{time_total}s\n" \
  -d '{"test": true}'

Mocking Webhooks in Tests

Test your webhook handlers in unit and integration tests by mocking the webhook payload and signature.

webhook.test.ts
import { createMockWebhook, signWebhook } from '@sylphx/sdk/testing'
import { POST } from '@/app/api/webhooks/route'

describe('Webhook Handler', () => {
  const secret = 'whsec_test_secret'

  it('should process user.created event', async () => {
    // Create a mock webhook payload
    const payload = createMockWebhook('user.created', {
      id: 'usr_test123',
      email: 'test@example.com',
      name: 'Test User',
    })

    // Sign the payload
    const { body, signature } = signWebhook(payload, secret)

    // Create mock request
    const request = new Request('http://localhost/api/webhooks', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': signature,
      },
      body,
    })

    // Call handler
    const response = await POST(request)

    expect(response.status).toBe(200)
    // Assert side effects (database writes, etc.)
  })

  it('should reject invalid signatures', async () => {
    const request = new Request('http://localhost/api/webhooks', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': 'invalid',
      },
      body: '{"type":"test"}',
    })

    const response = await POST(request)

    expect(response.status).toBe(401)
  })

  it('should handle idempotent retries', async () => {
    const payload = createMockWebhook('payment.succeeded', {
      id: 'pay_test123',
      amount: 2900,
    })
    const { body, signature } = signWebhook(payload, secret)

    const request = () => new Request('http://localhost/api/webhooks', {
      method: 'POST',
      headers: {
        'Content-Type': 'application/json',
        'X-Webhook-Signature': signature,
      },
      body,
    })

    // First request should process
    const response1 = await POST(request())
    expect(response1.status).toBe(200)

    // Duplicate request should succeed but not reprocess
    const response2 = await POST(request())
    expect(response2.status).toBe(200)

    // Verify only processed once
    const payments = await db.payments.findMany({
      where: { webhookEventId: payload.id },
    })
    expect(payments).toHaveLength(1)
  })
})

Test Fixtures

Create reusable test fixtures for common webhook scenarios. Store sample payloads in a __fixtures__ directory and load them in your tests.

Development Workflow

Follow this recommended workflow when developing webhook integrations.

1

Set up local tunnel

Use ngrok to expose your local server

2

Create development webhook

Point to your ngrok URL in the console

3

Implement handler

Write and test your webhook processing code

4

Send test events

Verify handling with test events from the console

5

Write automated tests

Add unit and integration tests with mocked webhooks

6

Deploy and monitor

Deploy to production and monitor delivery logs

Ready to go live?

Once you have tested your webhooks thoroughly, create a production webhook pointing to your deployed endpoint.