Skip to content
LinkProfit

Webhooks

Signed event deliveries: available events, verifying X-LinkProfit-Signature in JavaScript and Python, retries and the failing state.

Updated August 13, 2026

Webhooks push events to your endpoint as JSON POST requests — no polling. Register endpoints in the dashboard (Settings → Webhooks) or via POST /v1/webhooks; the signing secret is shown once at creation.

Events

| Event | Fires when | Available to | |---|---|---| | link.created / link.updated / link.deleted | A link changes — via dashboard, API or bulk import | workspace and partner endpoints | | domain.pending_ssl | DNS verified, certificate being issued | workspace and partner | | domain.activated | The domain serves traffic | workspace and partner | | domain.error | Provisioning failed; details inside | workspace and partner | | click.threshold | 80% / 100% of the monthly tracked-clicks limit | workspace and partner | | conversion.created | A conversion was recorded: an order, signup or custom goal attributed to a click | workspace and partner | | workspace.subscribed | A client paid the first invoice of a subscription | partner only | | workspace.past_due | A client payment failed | partner only | | workspace.suspended | A client was suspended (dunning or manually) | partner only | | payment.succeeded | A client payment settled, with the fee breakdown | partner only | | payment.refunded | A payment was refunded | partner only |

The delivery

POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-LinkProfit-Event: link.created
X-LinkProfit-Delivery: dlv_8Yq2…
X-LinkProfit-Signature: t=1755081600,v1=5f8a2c…
{
  "id": "dlv_8Yq2…",
  "type": "link.created",
  "created_at": "2026-08-13T10:00:00.000Z",
  "data": { "id": "lnk_…", "domain": "go.your-brand.com", "slug": "x7f2q1z", "url": "https://example.com" }
}

Answer with any 2xx within 10 seconds. Anything else — including a timeout — counts as a failure and schedules a retry. Deliveries can arrive out of order and, rarely, twice: deduplicate by id.

Verifying the signature

The signature is HMAC-SHA256 over `${t}.${rawBody}` with your endpoint secret, hex-encoded. Always verify against the raw request body, before any JSON parsing, and reject timestamps older than 5 minutes.

import { createHmac, timingSafeEqual } from "node:crypto";

function verifySignature(secret, rawBody, header, toleranceSeconds = 300) {
  const parts = Object.fromEntries(
    header.split(",").map((piece) => piece.split("=", 2)),
  );
  if (!parts.t || !parts.v1) return false;
  if (Math.abs(Date.now() / 1000 - Number(parts.t)) > toleranceSeconds) return false;

  const expected = createHmac("sha256", secret)
    .update(`${parts.t}.${rawBody}`)
    .digest("hex");
  const a = Buffer.from(expected);
  const b = Buffer.from(parts.v1);
  return a.length === b.length && timingSafeEqual(a, b);
}
import hashlib
import hmac
import time


def verify_signature(secret: str, raw_body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(piece.split("=", 1) for piece in header.split(",") if "=" in piece)
    timestamp, signature = parts.get("t"), parts.get("v1")
    if not timestamp or not signature:
        return False
    if abs(time.time() - int(timestamp)) > tolerance:
        return False

    expected = hmac.new(
        secret.encode(), f"{timestamp}.".encode() + raw_body, hashlib.sha256
    ).hexdigest()
    return hmac.compare_digest(expected, signature)

Retries and the failing state

The first attempt is immediate. Failures retry on a fixed schedule: 1 min → 5 min → 30 min → 2 h → 12 h. After the fifth retry the endpoint is marked failing, stops receiving new events and the owner gets an email.

Fix the endpoint, then re-enable it in the dashboard or with PATCH /v1/webhooks/{id} and {"status": "active"}. Missed events are not replayed automatically — the delivery log (GET /v1/webhooks/{id}/deliveries or the dashboard) shows every payload so you can reconcile.

A successful delivery resets the failure streak.

Testing

The Send test button (or POST /v1/webhooks/{id}/test) delivers a real, signed event with "data": { "test": true } through the same pipeline as production events — the delivery log shows the response of your endpoint.