dlogify docs

Webhooks

Receive tickets and digests as signed HTTP requests, and verify them.

By the end of this page your own endpoint receives dlogify's tickets and digests, and checks that every request really comes from dlogify.

Create a webhook channel

In the dashboard, open your project, go to Channels and choose Add channel, then Webhook. Enter the URL of your endpoint. dlogify shows the signing secret once, right after the channel is created: store it with your other secrets.

The URL must:

  • use https, with no credentials (user:pass@) and no fragment (#…);
  • use port 443, or a port between 1024 and 65535;
  • point to a public host name that resolves only to public addresses.

Otherwise the API refuses it with invalid-webhook-url. dlogify checks the address again before every delivery and does not follow redirects. Webhooks are part of the paid plans; on a plan without them, creating one returns channel-type-not-allowed.

Request

Every delivery is a POST with a JSON body and these headers:

HeaderValue
Content-Typeapplication/json
User-AgentLogify-Webhook/1
Logify-Eventticket, digest or test
Logify-Delivery-Iddlv_… for tickets and digests, tst_… for tests. The same on every retry of a delivery: use it to ignore duplicates.
Logify-Signaturet=<unix seconds>,v1=<signature>, see Verify the signature

Ignore fields you do not know: new ones can be added at any time.

Ticket body

Sent at once for critical and high tickets. ticket is the same object as GET /v1/tickets/{ticket_id} returns, and ticket_url opens the ticket in the dashboard.

{
  "type": "ticket",
  "delivery_id": "dlv_01j…",
  "organization_id": "org_01j…",
  "project": { "id": "prj_01j…", "name": "checkout" },
  "ticket": {
    "id": "tkt_01j…",
    "severity": "critical",
    "title": "Payment provider timeout after 3 retries",
    "summary": "…",
    "facts": { "…": "…" },
    "…": "…"
  },
  "ticket_url": "https://dlogify.com/dashboard/projects/prj_01j…/tickets/tkt_01j…"
}

Digest body

Sent on the project's digest schedule (see Notification settings) when there is something to report. Each item of tickets is an object like those of GET /v1/projects/{project_id}/tickets, with a url to its page.

{
  "type": "digest",
  "delivery_id": "dlv_01j…",
  "organization_id": "org_01j…",
  "project": { "id": "prj_01j…", "name": "checkout" },
  "digest": {
    "id": "dig_01j…",
    "period": { "start": "2026-10-03T06:00:00.000Z", "end": "2026-10-03T12:00:00.000Z", "timezone": "Europe/Madrid" },
    "ticket_count": 73,
    "tickets": [{ "id": "tkt_01j…", "severity": "medium", "title": "…", "url": "https://dlogify.com/…", "…": "…" }],
    "info": {
      "event_count": 120345,
      "window_count": 24,
      "busiest_window": { "start": "…", "end": "…", "total_count": 9000 },
      "latest_summary": { "…": "…" },
      "summary_ids": ["ism_01j…"],
      "url": "https://dlogify.com/dashboard/projects/prj_01j…/summaries"
    }
  }
}
  • ticket_count counts the period's tickets still kept when the digest is sent; tickets holds at most 100 of them.
  • summary_ids holds at most 300 INFO summaries, newest first; latest_summary is the newest one.
  • info is null when the period has no INFO summaries.

Test body

Sent when you choose Send test on a channel, or call POST /v1/channels/{channel_id}/test. A channel can be tested once a minute (channel-test-rate-limited). Tests are not retried.

{
  "type": "test",
  "delivery_id": "tst_01j…",
  "organization_id": "org_01j…",
  "project": { "id": "prj_01j…", "name": "checkout" },
  "channel": { "id": "chn_01j…", "name": "Ops webhook" }
}

Verify the signature

Logify-Signature has the form t=<unix seconds>,v1=<signature>, where the signature is the hex-encoded HMAC-SHA256 of t + "." + body, keyed with the channel's secret. dlogify computes a fresh one for every attempt. To verify a request:

  1. Use the raw body exactly as received. Parsing the JSON and serializing it again changes the bytes, and the signature no longer matches.
  2. Compare signatures in constant time.
  3. Reject timestamps more than 5 minutes away from your clock, so a captured request cannot be replayed later.
verify-webhook.mjs
import { createHmac, timingSafeEqual } from "node:crypto";

// Verify the Logify-Signature header of a webhook request.
// rawBody must be the exact bytes received, before any JSON parsing.
export function verifyLogifySignature(rawBody, header, secret, { toleranceSeconds = 300, now = Date.now() / 1000 } = {}) {
  if (!header || !(typeof rawBody === "string" || Buffer.isBuffer(rawBody))) return false;
  const parts = Object.fromEntries(header.split(",").map((part) => part.trim().split("=", 2)));
  if (!/^[0-9]{1,12}$/.test(parts.t ?? "") || !/^[0-9a-f]{64}$/.test(parts.v1 ?? "")) return false;
  if (Math.abs(now - Number(parts.t)) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${parts.t}.`).update(rawBody).digest();
  return timingSafeEqual(expected, Buffer.from(parts.v1, "hex"));
}

With Express, read the body as raw bytes on the webhook route:

import express from "express";
import { verifyLogifySignature } from "./verify-webhook.mjs";

const app = express();

app.post("/hooks/dlogify", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyLogifySignature(req.body, req.get("Logify-Signature"), process.env.DLOGIFY_WEBHOOK_SECRET)) {
    return res.sendStatus(401);
  }
  res.sendStatus(204);
  const event = JSON.parse(req.body.toString("utf8"));
  // handle event.type: "ticket", "digest" or "test"
});
verify_webhook.py
import hashlib
import hmac
import re
import time


def verify_logify_signature(raw_body: bytes, header: str | None, secret: str, tolerance_seconds: int = 300, now: float | None = None) -> bool:
    """Verify the Logify-Signature header. raw_body must be the exact bytes received."""
    if not header:
        return False
    parts = dict(part.strip().split("=", 1) for part in header.split(",") if "=" in part)
    t, v1 = parts.get("t", ""), parts.get("v1", "")
    if not re.fullmatch(r"[0-9]{1,12}", t) or not re.fullmatch(r"[0-9a-f]{64}", v1):
        return False
    current = time.time() if now is None else now
    if abs(current - int(t)) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), t.encode() + b"." + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, v1)

With Flask, request.get_data() returns the raw body:

import json
import os

from flask import Flask, request

from verify_webhook import verify_logify_signature

app = Flask(__name__)


@app.post("/hooks/dlogify")
def dlogify_webhook():
    raw = request.get_data()
    if not verify_logify_signature(raw, request.headers.get("Logify-Signature"), os.environ["DLOGIFY_WEBHOOK_SECRET"]):
        return "", 401
    event = json.loads(raw)
    # handle event["type"]: "ticket", "digest" or "test"
    return "", 204

Respond

Answer with any 2xx status as soon as the signature checks out, and do the slow work afterwards. dlogify waits 5 seconds to connect and 10 seconds for the response; a slower answer counts as a failed attempt.

Retries

A delivery is attempted up to 8 times. After a failed attempt, the next one waits 1 minute, then 5 minutes, 15 minutes, 1 hour, 3 hours, 6 hours and 12 hours (with a little random jitter): about 22 hours in total.

Your endpoint answersdlogify
2xxMarks the delivery as delivered
A timeout, network or TLS error, 408, 429 or 5xxRetries later. On 429 and 503, a larger Retry-After (seconds or HTTP date) replaces the wait, up to 12 hours.
3xx or any other 4xxGives up at once. Redirects are not followed.

Deliveries are at least once: the same delivery can reach you twice, for example if your answer was lost. Use Logify-Delivery-Id to skip one you already handled.

Channel health

When three deliveries in a row fail for good, the channel is marked as failing. It keeps receiving deliveries, and the next delivered ticket, digest or test clears the mark. The dashboard shows the health of each channel, and email digests list the project's failing channels.

On this page