Skip to main content
Version: v1 (Current)

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

  1. Create a new POST request to the webhook endpoint

  2. 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()
});
  1. Set the request body with your test payload

  2. 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

  1. Go to webhook.site
  2. Copy your unique URL
  3. Configure a webhook subscription with this URL
  4. Trigger an event (create an attendee, etc.)
  5. 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:

  1. Wrong secret: Verify you're using the correct webhook secret

    # Check your configured secret
    echo $GXP_WEBHOOK_SECRET
  2. 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);
  3. Encoding issues: Ensure consistent UTF-8 encoding

    // Ensure payload is stringified consistently
    const payload = JSON.stringify(req.body);
  4. 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:

  1. Check webhook subscription status:

    curl -X GET "https://api.gramercy.cloud/api/v1/webhook-subscriptions" \
    -H "Authorization: Bearer $TOKEN"
  2. Verify endpoint accessibility:

    # Test if your endpoint is reachable
    curl -I https://your-endpoint.com/webhooks
  3. Check firewall/network rules: Ensure GxP IPs are allowed

  4. Review webhook delivery logs in the GxP dashboard

  5. 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:

  1. 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);
  2. Check retry configuration: Webhooks may be retried if your endpoint responds slowly

  3. 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:

  1. Process asynchronously:

    // Queue the work
    dispatch(new ProcessWebhookJob($payload));

    // Return immediately
    return response()->json(['status' => 'queued']);
  2. Increase timeout (if possible in your infrastructure)

  3. Optimize processing: Profile and optimize slow operations

Using the GxP Dashboard

Viewing Webhook Logs

  1. Navigate to Project Settings > Integrations > Webhooks
  2. Select your webhook subscription
  3. 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

  1. Find the failed delivery in the logs
  2. Click Resend
  3. Monitor for successful delivery

Testing Webhooks from Dashboard

  1. Navigate to Project Settings > Webhooks
  2. Select your subscription
  3. Click Send Test
  4. Choose an event type
  5. Review the test payload
  6. 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:

  1. Failed Delivery Rate: Alert when failure rate exceeds threshold
  2. Response Time: Alert when endpoint response time increases
  3. 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',
]);