Skip to content

Testing Webhooks

Testing webhooks can be challenging since they require a publicly accessible HTTPS endpoint. This guide covers strategies for testing webhooks during development and debugging production issues.

Using the Test Endpoint

The easiest way to test your webhook integration is using the built-in test endpoint.

Send a Test Webhook

bash
curl -X POST https://api.prod.trstinc.ca/v1/project/{project_id}/webhooks/{webhook_id}/test \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

json
{
	"delivery_id": "550e8400-e29b-41d4-a716-446655440000",
	"status": "pending",
	"webhook_id": "webhook_abc123",
	"event_type": "webhook.test",
	"created_at": "2024-11-02T15:30:00Z"
}

This sends a webhook.test event to your configured endpoint:

json
{
	"event_id": "test_550e8400",
	"event_type": "webhook.test",
	"timestamp": "2024-11-02T15:30:00Z",
	"project_id": "proj_abc123",
	"data": {
		"test_id": "test_550e8400",
		"message": "This is a test webhook",
		"triggered_at": "2024-11-02T15:30:00Z"
	}
}

Verify Test Delivery

Check if the test webhook was delivered successfully:

bash
curl https://api.prod.trstinc.ca/v1/project/{project_id}/webhooks/{webhook_id}/deliveries \
  -H "Authorization: Bearer YOUR_API_KEY"

Look for the delivery with the matching delivery_id from the test response.

Local Development

ngrok creates a secure tunnel to your localhost:

  1. Install ngrok:

    bash
    brew install ngrok  # macOS
    # or download from ngrok.com
  2. Start your local server:

    bash
    node server.js  # Listening on port 3000
  3. Create ngrok tunnel:

    bash
    ngrok http 3000

    You'll see output like:

    Forwarding: https://abc123.ngrok.io -> http://localhost:3000
  4. Use the ngrok URL for your webhook:

    bash
    curl -X POST https://api.prod.trstinc.ca/v1/project/{project_id}/webhooks \
      -H "Authorization: Bearer YOUR_API_KEY" \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://abc123.ngrok.io/webhooks/trst",
        "event_types": ["enrollment.completed"],
        "enabled": true
      }'
  5. Monitor requests in ngrok dashboard:

    • Open http://localhost:4040 in your browser to see all requests
    • Inspect headers, payloads, and responses
    • Replay requests for testing

Option 2: Using Cloudflare Tunnel

Cloudflare Tunnel is a free alternative to ngrok:

  1. Install cloudflared:

    bash
    brew install cloudflare/cloudflare/cloudflared
  2. Start tunnel:

    bash
    cloudflared tunnel --url http://localhost:3000
  3. Use the provided URL for your webhook

Option 3: Using LocalTunnel

LocalTunnel is another simple option:

  1. Install:

    bash
    npm install -g localtunnel
  2. Start tunnel:

    bash
    lt --port 3000
  3. Use the provided URL

Debugging Failed Deliveries

View Delivery History

View delivery histroy in the web interface or get a list of all webhook deliveries:

bash
curl "https://api.prod.trstinc.ca/v1/project/{project_id}/webhooks/{webhook_id}/deliveries?from=2024-11-01T00:00:00Z&to=2024-11-02T23:59:59Z" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

json
{
	"items": [
		{
			"id": "550e8400-e29b-41d4-a716-446655440000",
			"subscription_id": "webhook_abc123",
			"payload": {
				"event_id": "event_xyz789",
				"timestamp": "2024-11-02T15:30:00Z",
				"project_id": "proj_abc123",
				"data": {
					"event_type": "enrollment.completed",
					"session_id": "session_123",
					"member_id": "member_456",
					"device_id": "device_789",
					"completed_at": "2024-11-02T15:30:00Z"
				}
			},
			"attempt_count": 1,
			"attempt_pending": false,
			"status": "success",
			"response_status": 200,
			"response_body": "OK",
			"created_at": "2024-11-02T15:30:00Z",
			"last_attempt_at": "2024-11-02T15:30:01Z"
		},
		{
			"id": "660e8400-e29b-41d4-a716-446655440001",
			"subscription_id": "webhook_abc123",
			"payload": {
				"event_id": "event_uvw456",
				"timestamp": "2024-11-02T14:00:00Z",
				"project_id": "proj_abc123",
				"data": {
					"event_type": "demo.scan.success",
					"member_id": "member_789",
					"device_id": "device_123",
					"scanned_at": "2024-11-02T14:00:00Z"
				}
			},
			"attempt_count": 3,
			"attempt_pending": true,
			"status": "failed",
			"response_status": 500,
			"response_body": "Internal Server Error",
			"created_at": "2024-11-02T14:00:00Z",
			"last_attempt_at": "2024-11-02T14:05:00Z"
		}
	],
	"total_count": 2
}

View Delivery Attempt Details

For failed deliveries, view detailed attempt information:

bash
curl "https://api.prod.trstinc.ca/v1/project/{project_id}/webhooks/{webhook_id}/deliveries/{delivery_id}/attempts" \
  -H "Authorization: Bearer YOUR_API_KEY"

Response:

json
{
	"attempts": [
		{
			"attempt_number": 1,
			"attempted_at": "2024-11-02T14:00:00Z",
			"response_status": 500,
			"response_body": "Internal Server Error",
			"duration_ms": 1523,
			"error": "HTTP 500 - Internal Server Error"
		},
		{
			"attempt_number": 2,
			"attempted_at": "2024-11-02T14:00:30Z",
			"response_status": 500,
			"response_body": "Internal Server Error",
			"duration_ms": 1842,
			"error": "HTTP 500 - Internal Server Error"
		},
		{
			"attempt_number": 3,
			"attempted_at": "2024-11-02T14:01:30Z",
			"response_status": null,
			"duration_ms": 20000,
			"error": "Request timeout after 20000ms"
		}
	]
}

Common Failure Scenarios

1. Signature Verification Failed

Symptom: Deliveries show 401 Unauthorized

Possible causes:

  • Using wrong webhook secret
  • Not using raw request body
  • Incorrect signature comparison

Debug steps:

  1. Log the received signature and computed signature
  2. Verify you're using the correct secret
  3. Check that you're using the raw body (not parsed JSON)
  4. See Security Guide for correct implementation

2. Request Timeout

Symptom: Deliveries show "Request timeout"

Possible causes:

  • Endpoint takes >20 seconds to respond
  • Synchronous processing of events
  • Slow database queries or external API calls

Solution:

javascript
app.post("/webhooks/trst", async (req, res) => {
	// Verify signature
	verifySignature(req)

	// Acknowledge IMMEDIATELY
	res.status(200).send("OK")

	// Process asynchronously (don't await!)
	processWebhook(req.body).catch(console.error)
})

3. Connection Refused

Symptom: Deliveries show "Connection refused"

Possible causes:

  • Server is down
  • Firewall blocking requests
  • Wrong URL/port

Debug steps:

  1. Verify your server is running
  2. Check that the webhook URL is correct
  3. Test with curl from outside your network
  4. Check firewall rules

4. SSL Certificate Errors

Symptom: Deliveries show SSL/TLS errors

Possible causes:

  • Self-signed certificate
  • Expired certificate
  • Certificate chain issues

Solution:

  • Use a valid SSL certificate (Let's Encrypt is free)
  • Ensure certificate chain is complete
  • Test with: curl -v https://your-endpoint.com

Testing Checklist

Before Going Live

  • [ ] Signature verification implemented and tested

    • [ ] Using raw request body
    • [ ] Correct secret from webhook creation
    • [ ] Constant-time comparison
  • [ ] Endpoint responds within 20 seconds

    • [ ] Immediate acknowledgment (200 OK)
    • [ ] Asynchronous event processing
  • [ ] Idempotency handling

    • [ ] Check event_id before processing
    • [ ] Skip duplicate events gracefully
  • [ ] Error handling

    • [ ] Catch and log all errors
    • [ ] Return appropriate HTTP status codes
    • [ ] Don't expose internal errors in response
  • [ ] Monitoring

    • [ ] Log all webhook receipts
    • [ ] Alert on repeated failures
    • [ ] Track processing time
  • [ ] Test all event types

    • [ ] enrollment.completed
    • [ ] demo.scan.success
    • [ ] webhook.test

Testing Script

javascript
// test-webhook.js
const crypto = require('crypto');

function generateTestWebhook(secret) {
  const payload = {
    event_id: "test_" + Date.now(),
    event_type: "webhook.test",
    timestamp: new Date().toISO String(),
    project_id: "proj_test",
    data: {
      test_id: "test_" + Date.now(),
      message: "Test webhook",
      triggered_at: new Date().toISOString()
    }
  };

  const body = JSON.stringify(payload);

  // Generate signature
  const hmac = crypto.createHmac('sha256', secret);
  hmac.update(body);
  const signature = 'sha256=' + hmac.digest('hex');

  return { body, signature };
}

// Usage
const { body, signature } = generateTestWebhook('your_webhook_secret');

console.log('POST to your endpoint with:');
console.log('Header: X-Webhook-Signature:', signature);
console.log('Body:', body);

Monitoring Production Webhooks

Set Up Alerts

Monitor for webhook issues:

javascript
// Example: Alert on 3 consecutive failures
async function checkWebhookHealth(webhookId) {
	const deliveries = await fetch(
		`https://api.prod.trstinc.ca/v1/project/${projectId}/webhooks/${webhookId}/deliveries?per_page=10`,
		{
			headers: { Authorization: `Bearer ${apiKey}` },
		},
	).then((r) => r.json())

	const recentFailures = deliveries.deliveries.slice(0, 3).filter((d) => d.status === "failed")

	if (recentFailures.length === 3) {
		// Alert team
		await sendAlert({
			severity: "high",
			message: `Webhook ${webhookId} has 3 consecutive failures`,
			details: recentFailures,
		})
	}
}

Dashboard Metrics

Track key metrics:

  • Delivery success rate (last 24h, 7d, 30d)
  • Average delivery latency
  • Failed delivery count
  • Events by type

Log Structured Data

javascript
// Log webhook events in structured format
function logWebhookEvent(event, metadata) {
	logger.info({
		type: "webhook_received",
		event_id: event.event_id,
		event_type: event.event_type,
		project_id: event.project_id,
		delivery_id: metadata.deliveryId,
		processing_time_ms: metadata.processingTime,
		timestamp: new Date().toISOString(),
	})
}

Troubleshooting Tools

1. Webhook.site

Webhook.site provides a temporary URL to inspect webhooks:

  1. Go to webhook.site
  2. Copy the unique URL
  3. Create a webhook subscription with that URL
  4. Trigger events and inspect the requests

2. RequestBin

RequestBin is similar to Webhook.site:

  1. Create a request bin
  2. Use the bin URL for your webhook
  3. View all requests in real-time

3. Ngrok Inspector

ngrok includes a web interface at http://localhost:4040:

  • See all requests/responses
  • Replay requests
  • Inspect headers and bodies

4. Server Logs

Always log comprehensive webhook data:

javascript
app.post("/webhooks/trst", (req, res) => {
	const event = JSON.parse(req.body.toString())

	logger.info("Webhook received", {
		eventId: event.event_id,
		eventType: event.data.event_type,
		timestamp: new Date().toISOString(),
		ip: req.ip,
	})

	// ... verification and processing
})

Next Steps