Tinylytics Webhooks

Webhooks let Tinylytics send a JSON POST to your application when selected events happen. Use them to pipe downtime alerts, live hits, kudos, custom events, AI insights, and content issues into your own systems without polling the API.

Webhooks are available on all paid plans. Tinylytics only creates outbound webhook events when the site owner has an active subscription, the webhook is active, the event type is selected, and the underlying write is accepted.

1. Quick Start

  1. Open Account Settings.
  2. Choose Webhooks.
  3. Create a webhook with a public HTTP or HTTPS URL. HTTPS is recommended.
  4. Select the events you want to receive.
  5. Save the webhook and copy the signing secret.
  6. Use Send Test from the webhook edit screen to confirm your endpoint and signature verification.

Your endpoint should return a 2xx response as soon as it accepts the delivery. Do any slower processing in a background job on your side.

Treat webhook delivery as at-least-once. Store X-Tinylytics-Delivery, or the nested hit.id or kudo.uid, if duplicate processing would be a problem.

2. Delivery Contract

Tinylytics sends each delivery as an HTTP POST with a JSON body.

Property Value
Method POST
Content type application/json
Timeout 30 seconds
Open timeout 10 seconds
Success response Any 2xx status
Failed response Network errors, unsafe URLs, timeouts, 3xx, 4xx, or 5xx responses
Retry limit Up to 10 failed attempts

Each delivery includes these headers:

Header Description
Content-Type Always application/json.
X-Tinylytics-Event Event type, such as new_hit or monitor_down.
X-Tinylytics-Delivery Unique delivery id. This is the WebhookEvent id and can be used for idempotency.
X-Tinylytics-Timestamp Unix timestamp for the delivery attempt.
X-Signature HMAC SHA-256 signature in the format sha256=<hex digest>.

Retries use this schedule after failed attempts: 30 seconds, 1 minute, 5 minutes, 15 minutes, 30 minutes, 1 hour, 2 hours, 4 hours, 8 hours, and 12 hours.

Tinylytics stores recent deliveries under the webhook’s Events page. You can inspect the payload, response code, response body, error message, attempt count, and retry status there. Failed events can be retried manually from that page.

3. Event Directory

Event When it fires Payload key
monitor_down An uptime monitor reports the site as down. monitor, downtime
monitor_up An uptime monitor reports the site as recovered. monitor, downtime
new_hit Tinylytics accepts a live hit from the collector script, tracking pixel, API create endpoint, or API batch create endpoint. Batch creates emit one webhook event per accepted hit. hit
new_kudo Tinylytics accepts a browser kudo, API kudo, or verified Webmention like. kudo
new_event Tinylytics accepts a custom analytics event from the collector script or API create/batch endpoints. Batch creates emit one webhook event per accepted event. analytics_event
new_insight Tinylytics creates a new AI insight for the site. insight
content_issue Content monitoring records a new or changed broken_link or mixed_content issue. check

Existing webhooks only receive the event types you selected. Edit a webhook and select any new types you want.

Tinylytics does not emit hit, kudo, or custom-event webhooks for ignored paths, ignored visitors, spam-suppressed writes, rate-limited writes, imports, seeds, cron-generated data, reimports, manual model creates, or kudo deletes. Spelling issues do not emit content_issue.

4. Payload Reference

Every event uses the same top-level wrapper.

Field Type Description
event string The event type. Matches X-Tinylytics-Event.
timestamp string ISO 8601 timestamp for when Tinylytics created the webhook payload.
site object Site metadata.

The site object has this shape:

Field Type Description
id integer Tinylytics site id.
name string Display name for the site.
url string Site URL.
dashboard_url string Tinylytics dashboard URL for the site.

monitor_down

{
  "event": "monitor_down",
  "timestamp": "2026-05-14T12:00:00Z",
  "site": {
    "id": 123,
    "name": "Example Blog",
    "url": "https://example.com",
    "dashboard_url": "https://tinylytics.app/sites/123"
  },
  "monitor": {
    "id": 456,
    "status": "down"
  },
  "downtime": {
    "started_at": "2026-05-14T11:58:00Z",
    "started_at_formatted": "May 14, 2026 at 11:58",
    "ended_at": null,
    "ended_at_formatted": null,
    "duration_seconds": 120,
    "duration_formatted": "2 minutes",
    "error": "Connection timeout"
  }
}

monitor_up

{
  "event": "monitor_up",
  "timestamp": "2026-05-14T12:08:00Z",
  "site": {
    "id": 123,
    "name": "Example Blog",
    "url": "https://example.com",
    "dashboard_url": "https://tinylytics.app/sites/123"
  },
  "monitor": {
    "id": 456,
    "status": "up"
  },
  "downtime": {
    "started_at": "2026-05-14T11:58:00Z",
    "started_at_formatted": "May 14, 2026 at 11:58",
    "ended_at": "2026-05-14T12:08:00Z",
    "ended_at_formatted": "May 14, 2026 at 12:08",
    "duration_seconds": 600,
    "duration_formatted": "10 minutes",
    "error": "Connection timeout"
  }
}

new_hit

The hit object matches the existing create-hit API response shape.

{
  "event": "new_hit",
  "timestamp": "2026-05-14T12:15:00Z",
  "site": {
    "id": 123,
    "name": "Example Blog",
    "url": "https://example.com",
    "dashboard_url": "https://tinylytics.app/sites/123"
  },
  "hit": {
    "id": 789,
    "url": "https://example.com/articles/webhooks",
    "path": "/articles/webhooks",
    "referrer": "https://example.org",
    "country": "US",
    "browser_name": "Safari",
    "platform_name": "macOS",
    "is_mobile": false,
    "source": "newsletter",
    "unique_hash": "4cc1ed6d2f0d",
    "visitor_hash": "9e89d84f3e47",
    "created_at": "2026-05-14T12:15:00.000Z"
  }
}

referrer, country, browser_name, platform_name, source, unique_hash, and visitor_hash can be null depending on the request and privacy rules.

new_kudo

The kudo object matches the existing API kudo response shape.

{
  "event": "new_kudo",
  "timestamp": "2026-05-14T12:20:00Z",
  "site": {
    "id": 123,
    "name": "Example Blog",
    "url": "https://example.com",
    "dashboard_url": "https://tinylytics.app/sites/123"
  },
  "kudo": {
    "id": 987,
    "uid": "kudo_d4f96c",
    "path": "/articles/webhooks",
    "created_at": "2026-05-14T12:20:00.000Z"
  }
}

new_event

The nested object is named analytics_event so it does not collide with the top-level webhook event string. value comes from the event’s optional value property and may be null.

{
  "event": "new_event",
  "timestamp": "2026-05-14T12:25:00Z",
  "site": {
    "id": 123,
    "name": "Example Blog",
    "url": "https://example.com",
    "dashboard_url": "https://tinylytics.app/sites/123"
  },
  "analytics_event": {
    "id": 555,
    "event": "cta.click",
    "value": "hero",
    "url": "https://example.com/pricing",
    "path": "/pricing",
    "referrer": "https://example.org",
    "country": "US",
    "source": "newsletter",
    "unique_hash": "4cc1ed6d2f0d",
    "visitor_hash": "9e89d84f3e47",
    "created_at": "2026-05-14T12:25:00.000Z"
  }
}

new_insight

{
  "event": "new_insight",
  "timestamp": "2026-05-14T13:00:00Z",
  "site": {
    "id": 123,
    "name": "Example Blog",
    "url": "https://example.com",
    "dashboard_url": "https://tinylytics.app/sites/123"
  },
  "insight": {
    "id": 42,
    "insights_for_date": "2026-05-14",
    "formatted_insights_date": "May 14, 2026",
    "generated_at": "2026-05-14T13:00:00.000Z",
    "summary": "Traffic stayed steady with a small mid-week lift.",
    "signals": [],
    "context_signals_count": 0,
    "traffic_patterns": "Visits were evenly spread through the week.",
    "best_content": "/articles/webhooks led the week.",
    "recommendations": "Keep publishing on the topics that drew attention."
  }
}

content_issue

Fired for new or changed broken_link and mixed_content checks only.

{
  "event": "content_issue",
  "timestamp": "2026-05-14T13:05:00Z",
  "site": {
    "id": 123,
    "name": "Example Blog",
    "url": "https://example.com",
    "dashboard_url": "https://tinylytics.app/sites/123"
  },
  "check": {
    "id": 77,
    "url": "https://example.com/broken-link",
    "status_code": 404,
    "error_message": "HTTP 404: Not Found",
    "issue_type": "broken_link",
    "checked_at": "2026-05-14T13:05:00.000Z",
    "ignored": false
  }
}

Test deliveries from the webhook edit screen include a top-level "test": true flag and sample data. They use the same delivery job, headers, signature format, and retry behaviour as live deliveries.

5. Signature Verification

Tinylytics signs the exact JSON request body with the webhook signing secret:

X-Signature: sha256=<hex HMAC SHA-256 digest>

Verify the signature before parsing or trusting the JSON. Always compute the HMAC from the raw request body you received. If you enable the content wrapper or use Discord formatting, the signed body is the transformed body that Tinylytics sent.

Rails Example

class TinylyticsWebhooksController < ApplicationController
  skip_before_action :verify_authenticity_token

  def create
    return head :unauthorized unless valid_signature?

    payload = JSON.parse(request.raw_post)

    case request.headers["X-Tinylytics-Event"]
    when "monitor_down"
      # Notify your incident system.
    when "monitor_up"
      # Resolve your incident.
    when "new_hit"
      # Enqueue analytics processing.
    when "new_kudo"
      # Notify the author or update local counters.
    when "new_event"
      # Handle a custom analytics event.
    when "new_insight"
      # Store or forward the AI insight summary.
    when "content_issue"
      # Open a content-fix ticket.
    end

    head :ok
  end

  private

  def valid_signature?
    signature = request.headers["X-Signature"].to_s
    body = request.raw_post
    expected = "sha256=#{OpenSSL::HMAC.hexdigest("sha256", signing_secret, body)}"

    signature.bytesize == expected.bytesize &&
      ActiveSupport::SecurityUtils.secure_compare(signature, expected)
  end

  def signing_secret
    Rails.application.credentials.dig(:tinylytics, :webhook_signing_secret)
  end
end

Node.js Example

Mount this route before any JSON middleware that would consume the raw body.

import crypto from "node:crypto";
import express from "express";

const app = express();

app.post("/webhooks/tinylytics", express.raw({ type: "application/json" }), (req, res) => {
  const secret = process.env.TINYLYTICS_WEBHOOK_SIGNING_SECRET;
  const signature = req.get("X-Signature") || "";
  const expected = `sha256=${crypto.createHmac("sha256", secret).update(req.body).digest("hex")}`;

  const verified =
    signature.length === expected.length &&
    crypto.timingSafeEqual(Buffer.from(signature), Buffer.from(expected));

  if (!verified) {
    return res.sendStatus(401);
  }

  const eventType = req.get("X-Tinylytics-Event");
  const payload = JSON.parse(req.body.toString("utf8"));

  switch (eventType) {
    case "new_hit":
      // Enqueue analytics processing.
      break;
    case "new_kudo":
      // Notify the author or update local counters.
      break;
    case "new_event":
      // Handle a custom analytics event.
      break;
    case "new_insight":
      // Store or forward the AI insight summary.
      break;
    case "content_issue":
      // Open a content-fix ticket.
      break;
    case "monitor_down":
    case "monitor_up":
      // Route monitor alerts.
      break;
  }

  res.sendStatus(200);
});

6. Platform And Body Options

The default platform is Generic. It receives the JSON payloads shown above.

If Wrap content is enabled, Tinylytics wraps the event payload before delivery:

{
  "content": {
    "event": "new_hit",
    "timestamp": "2026-05-14T12:15:00Z",
    "site": {},
    "hit": {}
  }
}

Discord webhooks use custom embeds for monitor_down and monitor_up. Other event types use the generic Discord fallback unless Tinylytics adds a custom Discord format later.

7. Common Flows

n8n

To pipe webhook events into n8n (signature verification, routing, and sample workflow JSON), see Tinylytics + n8n.

Downtime Alerts

Select monitor_down and monitor_up. Verify the signature, route monitor_down to your alerting or incident tool, and mark the incident resolved when monitor_up arrives.

Live Hit Processing

Select new_hit. Tinylytics creates one webhook event after each accepted live hit from the collector script, tracking pixel, API create endpoint, or API batch endpoint. Use X-Tinylytics-Delivery for delivery idempotency and hit.id for hit idempotency.

Kudos Notifications

Select new_kudo. Tinylytics creates one webhook event after each accepted browser kudo, API kudo, or verified Webmention like. Use kudo.uid if you need a stable public identifier.

Custom Events

Select new_event. Tinylytics creates one webhook event after each accepted custom analytics event from the collector script or API. Use analytics_event.id for idempotency.

AI Insights

Select new_insight. Tinylytics creates one webhook event after each newly generated insight record.

Content Issues

Select content_issue. Tinylytics creates one webhook event when content monitoring first records a broken link or mixed content issue, or when that issue type changes. Unchanged re-checks do not re-fire.

API Writes And Webhooks

Successful API hit, kudo, and custom-event creates can emit webhook events. Ignored or rejected writes do not emit webhooks, even if the API request returns a handled response.

8. Troubleshooting

Problem What to check
No deliveries appear Confirm the site owner has an active subscription, the webhook is active, and the event type is selected.
Test works but live hits do not Confirm the hit is not ignored, spam-suppressed, imported, seeded, or manually created.
Signature mismatch Verify against the raw request body, not a parsed and re-serialized JSON object.
Repeated retries Return a 2xx status after accepting the event. Redirects, 4xx, and 5xx responses are treated as failures.
Endpoint rejected Use a public HTTP or HTTPS endpoint. Localhost, private network addresses, and unsafe URLs are blocked.
Discord output is generic Only monitor_down and monitor_up use custom Discord embeds today.
Tinylytics — privacy-first analytics & monitoring for small sites and indie projects.
Incredibly OK™ software by Vincent Ritter.
Terms Privacy Contact
© 2026 Tinylytics