Разработка Webhook-менеджмента (дашборд для управления хуками)

Наша компания занимается разработкой, поддержкой и обслуживанием сайтов любой сложности. От простых одностраничных сайтов до масштабных кластерных систем построенных на микро сервисах. Опыт разработчиков подтвержден сертификатами от вендоров.

Разработка и обслуживание любых видов сайтов:

Информационные сайты или веб-приложения
Сайты визитки, landing page, корпоративные сайты, онлайн каталоги, квиз, промо-сайты, блоги, новостные ресурсы, информационные порталы, форумы, агрегаторы
Сайты или веб-приложения электронной коммерции
Интернет-магазины, B2B-порталы, маркетплейсы, онлайн-обменники, кэшбэк-сайты, биржи, дропшиппинг-платформы, парсеры товаров
Веб-приложения для управления бизнес-процессами
CRM-системы, ERP-системы, корпоративные порталы, системы управления производством, парсеры информации
Сайты или веб-приложения электронных услуг
Доски объявлений, онлайн-школы, онлайн-кинотеатры, конструкторы сайтов, порталы предоставления электронных услуг, видеохостинги, тематические порталы

Это лишь некоторые из технических типов сайтов, с которыми мы работаем, и каждый из них может иметь свои специфические особенности и функциональность, а также быть адаптированным под конкретные потребности и цели клиента

Предлагаемые услуги
Показано 1 из 1 услугВсе 2065 услуг
Разработка Webhook-менеджмента (дашборд для управления хуками)
Средняя
~3-5 рабочих дней
Часто задаваемые вопросы

Наши компетенции:

Этапы разработки

Последние работы

  • image_website-b2b-advance_0.png
    Разработка сайта компании B2B ADVANCE
    1262
  • image_web-applications_feedme_466_0.webp
    Разработка веб-приложения для компании FEEDME
    1171
  • image_websites_belfingroup_462_0.webp
    Разработка веб-сайта для компании БЕЛФИНГРУПП
    874
  • image_ecommerce_furnoro_435_0.webp
    Разработка интернет магазина для компании FURNORO
    1094
  • image_crm_enviok_479_0.webp
    Разработка веб-приложения для компании Enviok
    831
  • image_bitrix-bitrix-24-1c_fixper_448_0.png
    Разработка веб-сайта для компании ФИКСПЕР
    851

Разработка Webhook-менеджмента (дашборд для управления хуками)

Webhook без дашборда — это чёрный ящик. Вы не знаете, доставлено ли уведомление, сколько попыток было, что ответил получатель. Дашборд для управления хуками даёт полную видимость: все отправки, статусы, тела запросов и ответов, ретраи.

Схема БД

CREATE TABLE webhook_endpoints (
    id           SERIAL PRIMARY KEY,
    name         VARCHAR(255) NOT NULL,
    url          TEXT NOT NULL,
    secret       VARCHAR(64) NOT NULL,     -- для HMAC-подписи
    events       TEXT[] NOT NULL,          -- ['order.created', 'order.shipped']
    is_active    BOOLEAN DEFAULT TRUE,
    created_at   TIMESTAMPTZ DEFAULT NOW(),
    updated_at   TIMESTAMPTZ DEFAULT NOW(),
    -- Настройки доставки
    timeout_ms   INTEGER DEFAULT 10000,
    max_retries  SMALLINT DEFAULT 5,
    -- Статистика (денормализованная для быстрого отображения)
    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,   -- идемпотентный ключ события
    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-детали последней попытки
    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;

Сервис доставки

class WebhookDeliveryService
{
    public function dispatch(string $eventType, string $eventId, array $payload): void
    {
        // Находим активные endpoints, подписанные на это событие
        $endpoints = $this->endpointRepo->findActiveForEvent($eventType);

        foreach ($endpoints as $endpoint) {
            // Идемпотентность: не дублируем если уже создали запись для этого события
            $delivery = $this->deliveryRepo->findOrCreate(
                $endpoint->id,
                $eventId,
                [
                    'event_type' => $eventType,
                    'payload'    => $payload,
                    'status'     => 'pending',
                ]
            );

            // Ставим в очередь на асинхронную отправку
            dispatch(new DeliverWebhookJob($delivery->id));
        }
    }
}

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

    public int $tries = 1; // retry-логика управляется вручную
    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 = '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

// Получение списка endpoints с агрегированной статистикой
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);
}

// Детальный лог доставок с фильтрацией
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);
}

// Ручной ретрай конкретной доставки
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]);
}

// Тестовый 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]);
}

Верификация подписи на стороне получателя

// Node.js — получатель верифицирует HMAC-подпись
const crypto = require('crypto');

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

    // Timing-safe сравнение — защита от 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);
    // Отвечаем 200 сразу, обработку делаем асинхронно
    res.status(200).send('OK');
    processEventAsync(event);
});

Таймлайн

День 1 — схема БД, базовый DeliveryService с HMAC-подписью, Job с retry логикой.

День 2 — REST API для Admin UI: CRUD endpoints, фильтрованный лог доставок, ручной ретрай.

День 3 — фронтенд Admin UI (таблица endpoints, лог доставок с drill-down до тела запроса/ответа), тест ping, мониторинг failed/pending counts.