Webhook management dashboard development

Our company is engaged in the development, support and maintenance of sites of any complexity. From simple one-page sites to large-scale cluster systems built on micro services. Experience of developers is confirmed by certificates from vendors.
Development and maintenance of all types of websites:
Informational websites or web applications
Business card websites, landing pages, corporate websites, online catalogs, quizzes, promo websites, blogs, news resources, informational portals, forums, aggregators
E-commerce websites or web applications
Online stores, B2B portals, marketplaces, online exchanges, cashback websites, exchanges, dropshipping platforms, product parsers
Business process management web applications
CRM systems, ERP systems, corporate portals, production management systems, information parsers
Electronic service websites or web applications
Classified ads platforms, online schools, online cinemas, website builders, portals for electronic services, video hosting platforms, thematic portals

These are just some of the technical types of websites we work with, and each of them can have its own specific features and functionality, as well as be customized to meet the specific needs and goals of the client.

Showing 1 of 1 servicesAll 2065 services
Webhook management dashboard development
Medium
~3-5 business days
FAQ
Our competencies:
Development stages
Latest works
  • image_web-applications_feedme_466_0.webp
    Development of a web application for FEEDME
    1161
  • image_ecommerce_furnoro_435_0.webp
    Development of an online store for the company FURNORO
    1041
  • image_crm_enviok_479_0.webp
    Development of a web application for Enviok
    822
  • image_crm_chasseurs_493_0.webp
    CRM development for Chasseurs
    847
  • image_website-sbh_0.png
    Website development for SBH Partners
    999
  • image_website-_0.png
    Website development for Red Pear
    451

Developing Webhook Management (Dashboard for Hook Management)

A webhook without a dashboard is a black box. You don't know if a notification was delivered, how many attempts were made, what the recipient responded with. A webhook management dashboard provides full visibility: all sends, statuses, request and response bodies, retries.

Database Schema

CREATE TABLE webhook_endpoints (
    id           SERIAL PRIMARY KEY,
    name         VARCHAR(255) NOT NULL,
    url          TEXT NOT NULL,
    secret       VARCHAR(64) NOT NULL,     -- for HMAC signature
    events       TEXT[] NOT NULL,          -- ['order.created', 'order.shipped']
    is_active    BOOLEAN DEFAULT TRUE,
    created_at   TIMESTAMPTZ DEFAULT NOW(),
    updated_at   TIMESTAMPTZ DEFAULT NOW(),
    -- Delivery settings
    timeout_ms   INTEGER DEFAULT 10000,
    max_retries  SMALLINT DEFAULT 5,
    -- Statistics (denormalized for fast display)
    total_sent   INTEGER DEFAULT 0,
    total_failed INTEGER DEFAULT 0,
    last_sent_at TIMESTAMPTZ,
    last_error   TEXT
);

CREATE TABLE webhook_deliveries (
    id              BIGSERIAL PRIMARY KEY,
    endpoint_id     INTEGER REFERENCES webhook_endpoints(id),
    event_type      VARCHAR(100) NOT NULL,
    event_id        VARCHAR(100) NOT NULL,   -- idempotent event key
    payload         JSONB NOT NULL,
    status          VARCHAR(20) DEFAULT 'pending',  -- pending, delivered, failed, retrying
    attempt_count   SMALLINT DEFAULT 0,
    next_retry_at   TIMESTAMPTZ,
    created_at      TIMESTAMPTZ DEFAULT NOW(),
    delivered_at    TIMESTAMPTZ,
    -- HTTP details of last attempt
    last_http_status    SMALLINT,
    last_response_body  TEXT,
    last_request_ms     INTEGER,
    last_error_message  TEXT
);

CREATE TABLE webhook_delivery_attempts (
    id            BIGSERIAL PRIMARY KEY,
    delivery_id   BIGINT REFERENCES webhook_deliveries(id),
    attempt_num   SMALLINT NOT NULL,
    attempted_at  TIMESTAMPTZ DEFAULT NOW(),
    http_status   SMALLINT,
    request_ms    INTEGER,
    request_body  TEXT,
    response_body TEXT,
    error         TEXT
);

CREATE INDEX ON webhook_deliveries (endpoint_id, created_at DESC);
CREATE INDEX ON webhook_deliveries (status, next_retry_at) WHERE status = 'retrying';
CREATE INDEX ON webhook_deliveries (event_id, endpoint_id) UNIQUE;

Delivery Service

class WebhookDeliveryService
{
    public function dispatch(string $eventType, string $eventId, array $payload): void
    {
        // Find active endpoints subscribed to this event
        $endpoints = $this->endpointRepo->findActiveForEvent($eventType);

        foreach ($endpoints as $endpoint) {
            // Idempotency: don't duplicate if we already created a record for this event
            $delivery = $this->deliveryRepo->findOrCreate(
                $endpoint->id,
                $eventId,
                [
                    'event_type' => $eventType,
                    'payload'    => $payload,
                    'status'     => 'pending',
                ]
            );

            // Queue for async delivery
            dispatch(new DeliverWebhookJob($delivery->id));
        }
    }
}

class DeliverWebhookJob implements ShouldQueue
{
    use Dispatchable, InteractsWithQueue, Queueable, SerializesModels;

    public int $tries = 1; // retry logic managed manually
    public int $timeout = 30;

    public function handle(WebhookDeliveryService $service): void
    {
        $delivery = WebhookDelivery::with('endpoint')->find($this->deliveryId);
        if (!$delivery || $delivery->status === 'delivered') return;

        $endpoint = $delivery->endpoint;
        $attempt  = $delivery->attempt_count + 1;

        $body = json_encode([
            'id'         => $delivery->event_id,
            'type'       => $delivery->event_type,
            'created_at' => $delivery->created_at->toIso8601String(),
            'data'       => $delivery->payload,
        ]);

        // HMAC signature
        $signature = 'sha256=' . hash_hmac('sha256', $body, $endpoint->secret);

        $startTime = microtime(true);
        try {
            $response = Http::timeout($endpoint->timeout_ms / 1000)
                ->withHeaders([
                    'Content-Type'       => 'application/json',
                    'X-Webhook-ID'       => $delivery->id,
                    'X-Webhook-Event'    => $delivery->event_type,
                    'X-Webhook-Signature-256' => $signature,
                    'User-Agent'         => 'YourApp-Webhooks/1.0',
                ])
                ->post($endpoint->url, json_decode($body, true));

            $elapsed = (int)((microtime(true) - $startTime) * 1000);

            $this->recordAttempt($delivery, $attempt, $response->status(), $body, $response->body(), $elapsed);

            if ($response->successful()) {
                $delivery->update([
                    'status'          => 'delivered',
                    'delivered_at'    => now(),
                    'last_http_status' => $response->status(),
                    'last_request_ms'  => $elapsed,
                    'attempt_count'    => $attempt,
                ]);
            } else {
                $this->scheduleRetry($delivery, $attempt, $endpoint, "HTTP {$response->status()}");
            }
        } catch (\Exception $e) {
            $elapsed = (int)((microtime(true) - $startTime) * 1000);
            $this->recordAttempt($delivery, $attempt, null, $body, null, $elapsed, $e->getMessage());
            $this->scheduleRetry($delivery, $attempt, $endpoint, $e->getMessage());
        }
    }

    private function scheduleRetry(WebhookDelivery $delivery, int $attempt,
                                    WebhookEndpoint $endpoint, string $error): void
    {
        if ($attempt >= $endpoint->max_retries) {
            $delivery->update(['status' => 'failed', 'last_error_message' => $error]);
            return;
        }

        // Exponential backoff: 5s, 25s, 125s, 625s, 3125s
        $delaySeconds = 5 ** $attempt;
        $nextRetryAt  = now()->addSeconds($delaySeconds);

        $delivery->update([
            'status'        => 'retrying',
            'attempt_count' => $attempt,
            'next_retry_at' => $nextRetryAt,
            'last_error_message' => $error,
        ]);

        dispatch(new DeliverWebhookJob($delivery->id))->delay($nextRetryAt);
    }
}

Admin Dashboard API

// Get list of endpoints with aggregated statistics
public function endpoints(Request $request): JsonResponse
{
    $endpoints = WebhookEndpoint::withCount([
        'deliveries as pending_count'   => fn($q) => $q->where('status', 'pending'),
        'deliveries as failed_count'    => fn($q) => $q->where('status', 'failed'),
        'deliveries as delivered_count' => fn($q) => $q->where('status', 'delivered'),
    ])
    ->orderByDesc('created_at')
    ->paginate(20);

    return response()->json($endpoints);
}

// Detailed delivery log with filtering
public function deliveries(Request $request, int $endpointId): JsonResponse
{
    $deliveries = WebhookDelivery::where('endpoint_id', $endpointId)
        ->when($request->status, fn($q) => $q->where('status', $request->status))
        ->when($request->event_type, fn($q) => $q->where('event_type', $request->event_type))
        ->with('attempts')
        ->orderByDesc('created_at')
        ->paginate(50);

    return response()->json($deliveries);
}

// Manual retry of specific delivery
public function retry(int $deliveryId): JsonResponse
{
    $delivery = WebhookDelivery::findOrFail($deliveryId);
    $delivery->update(['status' => 'pending', 'next_retry_at' => null]);
    dispatch(new DeliverWebhookJob($deliveryId));

    return response()->json(['queued' => true]);
}

// Test webhook (ping)
public function ping(int $endpointId): JsonResponse
{
    $endpoint = WebhookEndpoint::findOrFail($endpointId);
    $this->webhookService->dispatch('webhook.ping', uniqid('ping_'), [
        'message' => 'Test webhook from dashboard',
        'timestamp' => now()->toIso8601String(),
    ]);
    return response()->json(['sent' => true]);
}

Signature Verification on Recipient Side

// Node.js — recipient verifies HMAC signature
const crypto = require('crypto');

function verifyWebhookSignature(rawBody, signature, secret) {
    const expected = 'sha256=' + crypto
        .createHmac('sha256', secret)
        .update(rawBody, 'utf8')
        .digest('hex');

    // Timing-safe comparison — protection from timing attacks
    return crypto.timingSafeEqual(
        Buffer.from(signature),
        Buffer.from(expected)
    );
}

app.post('/webhooks/yourapp', express.raw({type: 'application/json'}), (req, res) => {
    const signature = req.headers['x-webhook-signature-256'];
    if (!verifyWebhookSignature(req.body, signature, process.env.WEBHOOK_SECRET)) {
        return res.status(401).send('Invalid signature');
    }

    const event = JSON.parse(req.body);
    // Respond 200 immediately, process async
    res.status(200).send('OK');
    processEventAsync(event);
});

Timeline

Day 1 — Database schema, basic DeliveryService with HMAC signature, Job with retry logic.

Day 2 — REST API for Admin UI: CRUD endpoints, filtered delivery log, manual retry.

Day 3 — Frontend Admin UI (endpoints table, delivery log with drill-down to request/response bodies), ping test, monitoring failed/pending counts.