Webhook Testing & Debugging
This guide covers strategies and tools for testing and debugging webhooks in the GxP platform.
Testing Incoming Webhooks
Using cURL
Test incoming webhook endpoints with cURL:
#!/bin/bash
# Test MQTT Gateway Status webhook
# Configuration
ENDPOINT="https://api.gramercy.cloud/webhook/v1/mqtt-gateway-status"
SECRET="your-webhook-secret"
# Payload
PAYLOAD='{
"gateway_id": "GW-TEST-001",
"status": "online",
"readers": [
{
"reader_id": "READER-001",
"status": "connected",
"last_scan": "2024-01-15T10:30:00Z"
}
],
"timestamp": "2024-01-15T10:31:00Z"
}'
# Generate signature
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
# Send request
curl -X POST "$ENDPOINT" \
-H "Content-Type: application/json" \
-H "X-GxP-Signature: sha256=$SIGNATURE" \
-d "$PAYLOAD" \
-v
# Expected response: {"message": "Success"}
Using Postman
-
Create a new POST request to the webhook endpoint
-
Set up Pre-request Script for automatic signature generation:
// Pre-request Script
const secret = pm.environment.get('webhook_secret');
const body = pm.request.body.raw;
const signature = CryptoJS.HmacSHA256(body, secret).toString();
pm.request.headers.add({
key: 'X-GxP-Signature',
value: 'sha256=' + signature
});
// Add timestamp header
pm.request.headers.add({
key: 'X-GxP-Timestamp',
value: Math.floor(Date.now() / 1000).toString()
});
-
Set the request body with your test payload
-
Send the request and verify the response
Using HTTPie
# Install HTTPie: pip install httpie
# Generate signature first
PAYLOAD='{"gateway_id":"GW-001","status":"online"}'
SIGNATURE=$(echo -n "$PAYLOAD" | openssl dgst -sha256 -hmac "secret" | cut -d' ' -f2)
# Send request
http POST https://api.gramercy.cloud/webhook/v1/mqtt-gateway-status \
Content-Type:application/json \
X-GxP-Signature:"sha256=$SIGNATURE" \
gateway_id=GW-001 \
status=online
Testing Outgoing Webhooks
Using Webhook.site
- Go to webhook.site
- Copy your unique URL
- Configure a webhook subscription with this URL
- Trigger an event (create an attendee, etc.)
- View the received webhook on webhook.site
Using ngrok for Local Development
# Install ngrok: https://ngrok.com/download
# Start your local webhook receiver
php artisan serve --port=8080
# Start ngrok tunnel
ngrok http 8080
# Use the ngrok URL (e.g., https://abc123.ngrok.io/webhooks)
# as your webhook subscription URL
Local Webhook Receiver (PHP)
<?php
// webhook-receiver.php - Simple local webhook receiver for testing
// Log all incoming requests
$logFile = __DIR__ . '/webhooks.log';
// Capture request details
$method = $_SERVER['REQUEST_METHOD'];
$headers = getallheaders();
$body = file_get_contents('php://input');
$logEntry = [
'timestamp' => date('Y-m-d H:i:s'),
'method' => $method,
'headers' => $headers,
'body' => json_decode($body, true) ?? $body,
];
// Log to file
file_put_contents(
$logFile,
json_encode($logEntry, JSON_PRETTY_PRINT) . "\n---\n",
FILE_APPEND
);
// Verify signature if present
$signature = $headers['X-Gxp-Signature'] ?? '';
$secret = getenv('GXP_WEBHOOK_SECRET') ?: 'test-secret';
if ($signature) {
$expectedSignature = 'sha256=' . hash_hmac('sha256', $body, $secret);
$isValid = hash_equals($expectedSignature, $signature);
echo json_encode([
'status' => 'received',
'signature_valid' => $isValid,
'timestamp' => date('Y-m-d H:i:s'),
]);
} else {
echo json_encode([
'status' => 'received',
'timestamp' => date('Y-m-d H:i:s'),
]);
}
Run with: php -S localhost:8080 webhook-receiver.php
Local Webhook Receiver (Node.js)
// webhook-receiver.js
const express = require('express');
const crypto = require('crypto');
const fs = require('fs');
const app = express();
app.use(express.json());
app.use(express.raw({ type: 'application/json' }));
const LOG_FILE = './webhooks.log';
const SECRET = process.env.GXP_WEBHOOK_SECRET || 'test-secret';
function logWebhook(req, extra = {}) {
const entry = {
timestamp: new Date().toISOString(),
method: req.method,
path: req.path,
headers: req.headers,
body: req.body,
...extra
};
fs.appendFileSync(LOG_FILE, JSON.stringify(entry, null, 2) + '\n---\n');
console.log('Received webhook:', entry.timestamp, req.headers['x-gxp-event']);
}
function verifySignature(payload, signature) {
if (!signature) return null;
const expected = 'sha256=' + crypto
.createHmac('sha256', SECRET)
.update(typeof payload === 'string' ? payload : JSON.stringify(payload))
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature),
Buffer.from(expected)
);
}
app.post('/webhooks', (req, res) => {
const signature = req.headers['x-gxp-signature'];
const isValid = verifySignature(req.body, signature);
logWebhook(req, { signatureValid: isValid });
res.json({
status: 'received',
signatureValid: isValid,
event: req.headers['x-gxp-event'],
timestamp: new Date().toISOString()
});
});
app.listen(8080, () => {
console.log('Webhook receiver listening on port 8080');
console.log('Endpoint: http://localhost:8080/webhooks');
});
Run with: node webhook-receiver.js
Debugging Common Issues
Signature Verification Failures
Symptoms: HTTP 401 response, "Invalid signature" error
Possible causes and solutions:
-
Wrong secret: Verify you're using the correct webhook secret
# Check your configured secret
echo $GXP_WEBHOOK_SECRET -
Payload modification: Ensure the payload isn't being modified
// Use raw body, not parsed JSON
$payload = file_get_contents('php://input');
// NOT: $payload = json_encode($_POST); -
Encoding issues: Ensure consistent UTF-8 encoding
// Ensure payload is stringified consistently
const payload = JSON.stringify(req.body); -
Whitespace differences: Check for trailing newlines or whitespace
# Inspect actual bytes received
xxd < webhook_payload.json | head
Webhook Not Received
Symptoms: No webhook arrives at your endpoint
Debugging steps:
-
Check webhook subscription status:
curl -X GET "https://api.gramercy.cloud/api/v1/webhook-subscriptions" \
-H "Authorization: Bearer $TOKEN" -
Verify endpoint accessibility:
# Test if your endpoint is reachable
curl -I https://your-endpoint.com/webhooks -
Check firewall/network rules: Ensure GxP IPs are allowed
-
Review webhook delivery logs in the GxP dashboard
-
Check for SSL certificate issues:
openssl s_client -connect your-endpoint.com:443 -servername your-endpoint.com
Duplicate Webhooks
Symptoms: Same event received multiple times
Solutions:
-
Implement idempotency:
$deliveryId = $_SERVER['HTTP_X_GXP_DELIVERY_ID'];
if (Cache::has("webhook:processed:{$deliveryId}")) {
return response()->json(['status' => 'already processed']);
}
// Process webhook...
Cache::put("webhook:processed:{$deliveryId}", true, 3600); -
Check retry configuration: Webhooks may be retried if your endpoint responds slowly
-
Return 200 quickly: Acknowledge receipt before processing
// Respond immediately
ignore_user_abort(true);
http_response_code(200);
ob_end_flush();
flush();
// Then process async
processWebhookAsync($payload);
Timeout Issues
Symptoms: Webhooks marked as failed despite processing
Solutions:
-
Process asynchronously:
// Queue the work
dispatch(new ProcessWebhookJob($payload));
// Return immediately
return response()->json(['status' => 'queued']); -
Increase timeout (if possible in your infrastructure)
-
Optimize processing: Profile and optimize slow operations
Using the GxP Dashboard
Viewing Webhook Logs
- Navigate to Project Settings > Integrations > Webhooks
- Select your webhook subscription
- Click View Deliveries
Delivery Log Details
Each delivery log entry shows:
- Status: delivered, failed, pending
- HTTP Response Code: Your endpoint's response
- Response Time: How long your endpoint took
- Attempts: Number of delivery attempts
- Payload: The webhook payload (click to expand)
Resending Failed Webhooks
- Find the failed delivery in the logs
- Click Resend
- Monitor for successful delivery
Testing Webhooks from Dashboard
- Navigate to Project Settings > Webhooks
- Select your subscription
- Click Send Test
- Choose an event type
- Review the test payload
- Click Send
Automated Testing
PHPUnit Test for Webhook Handlers
<?php
namespace Tests\Feature\Webhooks;
use Tests\TestCase;
class OutgoingWebhookHandlerTest extends TestCase
{
protected string $secret = 'test-webhook-secret';
protected function generateSignature(string $payload): string
{
return 'sha256=' . hash_hmac('sha256', $payload, $this->secret);
}
public function test_handles_valid_attendee_created_webhook(): void
{
$payload = json_encode([
'id' => 'evt_test123',
'event' => 'attendee.created',
'timestamp' => '2024-01-15T10:30:00Z',
'project_id' => 1,
'data' => [
'attendee_id' => 12345,
'first_name' => 'Test',
'last_name' => 'User',
],
]);
$response = $this->postJson('/api/webhooks/gxp', json_decode($payload, true), [
'X-GxP-Signature' => $this->generateSignature($payload),
'X-GxP-Event' => 'attendee.created',
'X-GxP-Delivery-ID' => 'del_test123',
]);
$response->assertOk();
$response->assertJson(['status' => 'processed']);
}
public function test_rejects_invalid_signature(): void
{
$payload = json_encode(['event' => 'attendee.created']);
$response = $this->postJson('/api/webhooks/gxp', json_decode($payload, true), [
'X-GxP-Signature' => 'sha256=invalid',
]);
$response->assertUnauthorized();
}
public function test_handles_duplicate_delivery(): void
{
$payload = json_encode([
'id' => 'evt_test123',
'event' => 'attendee.created',
'timestamp' => '2024-01-15T10:30:00Z',
'project_id' => 1,
'data' => ['attendee_id' => 12345],
]);
$deliveryId = 'del_duplicate123';
$signature = $this->generateSignature($payload);
// First delivery
$this->postJson('/api/webhooks/gxp', json_decode($payload, true), [
'X-GxP-Signature' => $signature,
'X-GxP-Delivery-ID' => $deliveryId,
])->assertOk();
// Duplicate delivery
$this->postJson('/api/webhooks/gxp', json_decode($payload, true), [
'X-GxP-Signature' => $signature,
'X-GxP-Delivery-ID' => $deliveryId,
])->assertOk()
->assertJson(['status' => 'already processed']);
}
}
Integration Test for Incoming Webhooks
<?php
namespace Tests\Feature\Webhooks;
use Tests\TestCase;
class IncomingWebhookTest extends TestCase
{
public function test_mqtt_gateway_status_webhook(): void
{
$payload = [
'gateway_id' => 'GW-TEST-001',
'status' => 'online',
'readers' => [
['reader_id' => 'R-001', 'status' => 'connected'],
],
'timestamp' => now()->toIso8601String(),
];
$response = $this->postJson('/webhook/v1/mqtt-gateway-status', $payload);
$response->assertOk();
$response->assertJson(['message' => 'Success']);
}
public function test_large_upload_finalized_webhook(): void
{
$payload = [
'kind' => 'storage#object',
'name' => 'uploads/assets/12345/original/file.mp4',
'bucket' => 'gxp-uploads',
'contentType' => 'video/mp4',
'size' => '1048576',
'timeCreated' => now()->toIso8601String(),
];
$response = $this->postJson('/webhook/v1/large-upload-finalized', $payload);
$response->assertOk();
}
}
Monitoring and Alerting
Setting Up Alerts
Configure alerts for webhook issues:
- Failed Delivery Rate: Alert when failure rate exceeds threshold
- Response Time: Alert when endpoint response time increases
- Queue Depth: Alert when webhook queue grows too large
Metrics to Track
- Webhook delivery success rate
- Average response time
- Retry rate
- Signature verification failure rate
Log Aggregation
Aggregate webhook logs for analysis:
// Example: Log to external service
Log::channel('webhook')->info('Webhook processed', [
'event' => $event,
'delivery_id' => $deliveryId,
'processing_time_ms' => $processingTime,
'status' => 'success',
]);