Test webhooks locally using ngrok, request inspectors, and mock payloads. Strategies for development, staging, and production testing.
Best for: Testing with real Field Nation webhooks locally
# Install ngrok
brew install ngrok/ngrok/ngrok
# Or download from https://ngrok.com/download
# Authenticate
ngrok authtoken YOUR_NGROK_TOKEN
# Start tunnel to local port
ngrok http 3000Output:
Session Status online
Account your-account (Plan: Free)
Version 3.x.x
Region United States (us)
Latency -
Web Interface http://127.0.0.1:4040
Forwarding https://abc123.ngrok-free.app -> http://localhost:3000
Connections ttl opn rt1 rt5 p50 p90
0 0 0.00 0.00 0.00 0.00https://abc123.ngrok-free.apphttps://abc123.ngrok-free.app/webhooks/fieldnationWeb Interface: Visit http://127.0.0.1:4040 to see all requests in real-time, including headers and payloads.
version: "2"
authtoken: YOUR_NGROK_TOKEN
tunnels:
webhook:
proto: http
addr: 3000
hostname: your-custom-domain.ngrok-free.app
inspect: true
bind_tls: trueStart named tunnel:
ngrok start webhookAlternative to ngrok - no account required
# Install
npm install -g localtunnel
# Start tunnel
lt --port 3000 --subdomain my-webhook
# Output: https://my-webhook.loca.ltPerfect for viewing webhook payloads without writing code.
Features:
Similar to webhook.site:
const express = require('express');
const crypto = require('crypto');
const app = express();
// Environment
const PORT = process.env.PORT || 3000;
const WEBHOOK_SECRET = process.env.WEBHOOK_SECRET || 'test-secret';
// Middleware
app.use(express.raw({ type: 'application/json' }));
// Health check
app.get('/health', (req, res) => {
res.json({ status: 'ok', timestamp: new Date().toISOString() });
});
// Webhook endpoint with full logging
app.post('/webhooks/fieldnation', (req, res) => {
console.log('\n=== WEBHOOK RECEIVED ===');
console.log('Timestamp:', new Date().toISOString());
// Log headers
console.log('\nHeaders:');
Object.entries(req.headers).forEach(([key, value]) => {
if (key.startsWith('x-fn-') || key === 'content-type') {
console.log(` ${key}: ${value}`);
}
});
// Verify signature
const signature = req.headers['x-fn-signature'];
const isValid = verifySignature(req.body, signature, WEBHOOK_SECRET);
console.log(`\nSignature Valid: ${isValid}`);
if (!isValid) {
console.log('❌ SIGNATURE VERIFICATION FAILED');
return res.status(401).send('Unauthorized');
}
// Parse payload
const payload = JSON.parse(req.body.toString());
// Log event details
console.log('\nEvent Details:');
console.log(` Event ID: ${payload.eventId}`);
console.log(` Event Name: ${payload.eventName}`);
console.log(` Work Order ID: ${payload.workOrderId}`);
console.log(` Timestamp: ${payload.timestamp}`);
// Log payload preview
console.log('\nPayload Preview:');
console.log(JSON.stringify(payload, null, 2).substring(0, 500) + '...');
console.log('\n✅ Webhook processed successfully');
console.log('=======================\n');
// Respond
res.status(200).send('OK');
});
function verifySignature(rawBody, signature, secret) {
if (!signature) return false;
const [algorithm, hash] = signature.split('=');
const expectedHash = crypto
.createHmac(algorithm, secret)
.update(rawBody)
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(expectedHash),
Buffer.from(hash)
);
}
app.listen(PORT, () => {
console.log(`🚀 Webhook test server running on port ${PORT}`);
console.log(`📝 Health check: http://localhost:${PORT}/health`);
console.log(`📨 Webhook endpoint: http://localhost:${PORT}/webhooks/fieldnation`);
console.log(`\n🔐 Using secret: ${WEBHOOK_SECRET}`);
console.log('\n⏳ Waiting for webhooks...\n');
});Run:
# Set secret (get from webhook creation)
export WEBHOOK_SECRET=your-webhook-secret
# Start server
node test-webhook-server.js
# In another terminal, start ngrok
ngrok http 3000Test your processing logic with realistic payloads:
{
"eventName": "workorder.status.published",
"eventId": "evt_test_001",
"workOrderId": 99999,
"timestamp": "2025-01-15T10:30:00.000Z",
"data": {
"id": 99999,
"title": "Test Router Installation",
"description": "This is a test work order for webhook testing",
"status": "published",
"type": "installation",
"schedule": {
"serviceWindow": {
"start": "2025-01-20T09:00:00Z",
"end": "2025-01-20T17:00:00Z",
"mode": "hours"
}
},
"location": {
"address1": "123 Test Street",
"city": "San Francisco",
"state": "CA",
"zip": "94105",
"coordinates": {
"latitude": 37.7749,
"longitude": -122.4194
}
},
"pay": {
"type": "fixed",
"amount": 250.00,
"currency": "USD"
},
"buyer": {
"id": 456,
"name": "Test Company",
"companyId": 789
},
"tags": ["test", "router"],
"customFields": {
"testField": "test-value"
},
"createdAt": "2025-01-15T10:00:00Z",
"updatedAt": "2025-01-15T10:30:00Z"
}
}{
"eventName": "workorder.status.assigned",
"eventId": "evt_test_002",
"workOrderId": 99999,
"timestamp": "2025-01-15T11:00:00.000Z",
"data": {
"id": 99999,
"title": "Test Router Installation",
"status": "assigned",
"provider": {
"id": 12345,
"name": "John Test Provider",
"userId": 67890,
"rating": 4.8,
"completedJobs": 150
},
"schedule": {
"serviceWindow": {
"start": "2025-01-20T09:00:00Z",
"end": "2025-01-20T17:00:00Z"
}
}
}
}#!/bin/bash
WEBHOOK_URL="http://localhost:3000/webhooks/fieldnation"
WEBHOOK_SECRET="your-webhook-secret"
PAYLOAD_FILE="mock-payloads/workorder-published.json"
# Read payload
PAYLOAD=$(cat $PAYLOAD_FILE)
# Generate signature
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$WEBHOOK_SECRET" | awk '{print $2}')
# Send request
curl -X POST "$WEBHOOK_URL" \
-H "Content-Type: application/json" \
-H "x-fn-signature: sha256=$SIGNATURE" \
-H "x-fn-webhook-id: wh_test_123" \
-H "x-fn-event-name: workorder.status.published" \
-H "x-fn-delivery-id: del_test_001" \
-H "x-fn-timestamp: $(date -u +"%Y-%m-%dT%H:%M:%SZ")" \
-d "$PAYLOAD" \
-vMake executable and run:
chmod +x send-mock-webhook.sh
./send-mock-webhook.shconst crypto = require('crypto');
const fetch = require('node-fetch');
const fs = require('fs');
async function sendMockWebhook(payloadPath, webhookUrl, secret) {
// Read payload
const payload = JSON.parse(fs.readFileSync(payloadPath, 'utf8'));
const payloadString = JSON.stringify(payload);
// Generate signature
const signature = crypto
.createHmac('sha256', secret)
.update(payloadString)
.digest('hex');
// Send request
const response = await fetch(webhookUrl, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-fn-signature': `sha256=${signature}`,
'x-fn-webhook-id': 'wh_test_123',
'x-fn-event-name': payload.eventName,
'x-fn-delivery-id': `del_test_${Date.now()}`,
'x-fn-timestamp': new Date().toISOString()
},
body: payloadString
});
console.log('Response Status:', response.status);
console.log('Response Body:', await response.text());
}
// Usage
sendMockWebhook(
'./mock-payloads/workorder-published.json',
'http://localhost:3000/webhooks/fieldnation',
'your-webhook-secret'
);Goal: Rapid iteration and debugging
Start with webhook.site to see raw payloads without writing code.
Once you understand the payload structure, build your handler locally.
Test specific scenarios without triggering real events.
Always test signature validation logic.
Test invalid signatures, malformed payloads, timeouts.
Goal: Full integration testing before production
const assert = require('assert');
describe('Webhook Integration Tests', () => {
it('should receive and process work order published event', async () => {
// 1. Create test work order in sandbox
const workOrder = await createTestWorkOrder();
// 2. Publish work order (triggers webhook)
await publishWorkOrder(workOrder.id);
// 3. Wait for webhook processing
await sleep(5000);
// 4. Verify webhook was received
const logs = await getProcessedEvents();
const event = logs.find(log =>
log.workOrderId === workOrder.id &&
log.eventName === 'workorder.status.published'
);
assert.ok(event, 'Webhook event not found');
assert.equal(event.status, 'processed');
// 5. Verify downstream effects
const salesforceRecord = await getSalesforceRecord(workOrder.id);
assert.ok(salesforceRecord, 'Work order not synced to Salesforce');
});
it('should handle duplicate webhooks idempotently', async () => {
const payload = getMockPayload();
// Send same webhook twice
await sendWebhook(payload);
await sendWebhook(payload);
// Verify processed only once
const processCount = await getProcessingCount(payload.eventId);
assert.equal(processCount, 1, 'Webhook processed more than once');
});
it('should retry failed processing', async () => {
// Temporarily break downstream service
await disableSalesforceSync();
// Send webhook
const payload = getMockPayload();
await sendWebhook(payload);
// Verify failed
await sleep(2000);
let status = await getProcessingStatus(payload.eventId);
assert.equal(status, 'failed');
// Fix service
await enableSalesforceSync();
// Verify retry succeeded
await sleep(15000); // Wait for retry
status = await getProcessingStatus(payload.eventId);
assert.equal(status, 'processed');
});
});Run staging tests:
# Set staging environment
export NODE_ENV=staging
export WEBHOOK_URL=https://staging.example.com/webhooks
export FN_CLIENT_ID=staging-client-id
export FN_CLIENT_SECRET=staging-secret
# Run tests
npm testGoal: Verify production deployment without disrupting operations
async function runSmokeTests() {
const tests = {
webhookEndpoint: false,
signatureVerification: false,
queueHealth: false,
downstreamServices: false
};
// Test 1: Webhook endpoint responsive
try {
const response = await fetch('https://api.example.com/health');
tests.webhookEndpoint = response.ok;
} catch (error) {
console.error('Webhook endpoint test failed:', error);
}
// Test 2: Signature verification working
try {
const testPayload = getMockPayload();
const signature = generateSignature(testPayload, process.env.WEBHOOK_SECRET);
const response = await sendWebhook(testPayload, signature);
tests.signatureVerification = response.status === 200;
} catch (error) {
console.error('Signature test failed:', error);
}
// Test 3: Queue healthy
try {
const queueSize = await getQueueSize();
tests.queueHealth = queueSize < 1000;
} catch (error) {
console.error('Queue health test failed:', error);
}
// Test 4: Downstream services accessible
try {
await Promise.all([
checkSalesforce(),
checkDatabase(),
checkRedis()
]);
tests.downstreamServices = true;
} catch (error) {
console.error('Downstream services test failed:', error);
}
// Report
const allPassed = Object.values(tests).every(t => t);
console.log('\n=== Smoke Test Results ===');
Object.entries(tests).forEach(([test, passed]) => {
console.log(` ${passed ? '✅' : '❌'} ${test}`);
});
console.log(`\nOverall: ${allPassed ? '✅ PASSED' : '❌ FAILED'}`);
return allPassed;
}
// Run after deployment
runSmokeTests().then(passed => {
process.exit(passed ? 0 : 1);
});// Create separate webhook for testing
const canaryWebhook = await createWebhook({
url: 'https://api-canary.example.com/webhooks',
status: 'active',
events: ['workorder.status.published'], // Single event for testing
notificationEmail: 'canary-alerts@example.com'
});
// Monitor canary for issues
await monitorCanary(canaryWebhook.webhookId, {
duration: '1 hour',
successRateThreshold: 99,
onSuccess: () => {
console.log('Canary successful, promoting to production');
// Update main webhook to new version
},
onFailure: () => {
console.log('Canary failed, rolling back');
// Deactivate canary, keep old version
}
});Test webhook handling under high volume:
const autocannon = require('autocannon');
async function loadTest() {
const result = await autocannon({
url: 'http://localhost:3000/webhooks/fieldnation',
connections: 100, // Concurrent connections
duration: 30, // 30 seconds
pipelining: 1,
method: 'POST',
headers: {
'Content-Type': 'application/json',
'x-fn-signature': 'sha256=test-signature',
'x-fn-webhook-id': 'wh_load_test',
'x-fn-event-name': 'workorder.status.published'
},
body: JSON.stringify(getMockPayload())
});
console.log('\n=== Load Test Results ===');
console.log(`Requests: ${result.requests.total}`);
console.log(`Duration: ${result.duration}s`);
console.log(`RPS: ${result.requests.average}`);
console.log(`Latency p50: ${result.latency.p50}ms`);
console.log(`Latency p95: ${result.latency.p95}ms`);
console.log(`Latency p99: ${result.latency.p99}ms`);
console.log(`Errors: ${result.errors}`);
console.log(`Timeouts: ${result.timeouts}`);
}
loadTest();Last updated on