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.
Install ngrok
# Using Homebrew
brew install ngrok/ngrok/ngrok
# Or download from https://ngrok.com/downloadAuthenticate (optional, for persistent URLs)
# Sign up at https://ngrok.com and get your authtoken
ngrok config add-authtoken YOUR_AUTHTOKENStart your local server
# Start your development server
npm run dev
# Server running at http://localhost:3000Start ngrok tunnel
# Create a tunnel to your local server
ngrok http 3000
# Output:
# Forwarding https://abc123.ngrok.io -> http://localhost:3000Configure your webhook
Use the ngrok HTTPS URL as your webhook endpoint in the console.
https://abc123.ngrok.io/api/webhooksngrok Web Interface
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.
# 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.comOption 3: localtunnel
localtunnel is an open-source alternative that requires no signup.
# Install localtunnel globally
npm install -g localtunnel
# Create a tunnel
lt --port 3000
# Output:
# your url is: https://brave-fish-42.loca.lt| Tool | Free Tier | Persistent URLs | Web Inspector |
|---|---|---|---|
| ngrok | Yes | Paid | Yes |
| Cloudflare Tunnel | Yes | Yes (with account) | Via Cloudflare |
| localtunnel | Yes | No | No |
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
2xx response received
Non-2xx or timeout
Awaiting retry
Event filtered
Programmatic Access
You can also access delivery logs programmatically via the SDK or API.
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
Navigate to your webhook
Go to Console → Webhooks → Select your webhook
Click 'Send Test Event'
Choose an event type from the dropdown (e.g., user.created)
View the result
The test delivery will appear in your delivery log with full request/response details
From the SDK
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.
{
"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
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
| Property | Type | Description |
|---|---|---|
idrequired | string | Unique delivery attempt identifier |
webhook_idrequired | string | Associated webhook |
event_idrequired | string | Event that triggered the delivery |
event_typerequired | string | Type of event (e.g., user.created) |
statusrequired | "success" | "failed" | "pending" | Delivery status |
status_code | number | null | HTTP response status code |
duration | number | Request duration in milliseconds |
attemptrequired | number | Attempt number (1-5) |
error | string | null | Error message if failed |
request_urlrequired | string | Webhook endpoint URL |
request_headers | object | Headers sent with the request |
request_bodyrequired | string | JSON payload sent |
response_headers | object | null | Headers received in response |
response_body | string | null | Response body (truncated at 10KB) |
created_atrequired | string | When the delivery was attempted |
next_retry_at | string | null | When the next retry will occur |
Filtering 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 timeoutEndpoint took too long to respond
Fix: Optimize your handler to respond within 30 seconds. Process webhooks asynchronously.
Connection refusedServer not running or firewall blocking
Fix: Verify your server is running and accessible. Check firewall rules and security groups.
SSL certificate errorInvalid or expired certificate
Fix: Install a valid SSL certificate from a trusted CA. Ensure it is not expired.
4xx status codeClient error (bad request, unauthorized)
Fix: Check your signature verification. Ensure endpoint accepts POST requests with JSON.
5xx status codeServer error in your handler
Fix: Check your server logs for exceptions. Add error handling to your webhook handler.
DNS resolution failedDomain not found
Fix: Verify the webhook URL is correct and DNS is properly configured.
Debugging Checklist
Testing with curl
Test your endpoint manually to isolate issues.
# 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.
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
__fixtures__ directory and load them in your tests.Development Workflow
Follow this recommended workflow when developing webhook integrations.
Set up local tunnel
Use ngrok to expose your local server
Create development webhook
Point to your ngrok URL in the console
Implement handler
Write and test your webhook processing code
Send test events
Verify handling with test events from the console
Write automated tests
Add unit and integration tests with mocked webhooks
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.