Webhooks

Webhooks

Receive real-time link.created, link.updated, and link.deleted events at your own endpoint, signed with a per-endpoint secret.

Webhooks push link events from U2L to your server the moment they happen — no polling. Available on the Pro plan and above.

Endpoints per plan: Pro 3 · Growth 5 · Advanced 10 · Team 20 · Enterprise 50. Manage them via the Webhooks API.

Events

EventFires whendata payload
link.createdA link or QR code is created (dashboard, API, extension, CLI, bot)The created link object
link.updatedA link’s destination, title, or settings changeThe updated link object
link.deletedA link is deleted{ id, domain, slug, type }

Quick start

curl -X POST https://u2l.ai/api/v1/webhooks \
  -H "Authorization: Bearer u2l_live_your_api_key" \
  -H "Content-Type: application/json" \
  -d '{ "url": "https://example.com/u2l-webhook" }'

The 201 response contains your signing secret exactly once — store it. Every delivery is an HTTP POST like:

{
  "id": "evt_9c1d7d2f4a8b4a6c8f0e2b1d3c5a7e9f",
  "event": "link.created",
  "createdAt": "2026-07-25T09:00:00.000Z",
  "data": {
    "id": "d1a005ee-d491-451b-901f-0b6c0d6e9078",
    "shortLink": "https://u2l.ai/launch24",
    "domain": "u2l.ai",
    "slug": "launch24",
    "destination": "https://example.com/my-page",
    "type": "SL",
    "createdAt": "2026-07-25T09:00:00.000Z"
  }
}

with headers:

X-U2L-Event: link.created
X-U2L-Signature: t=1753434000,v1=5257a869e7ecebeda32affa62cdca3fa51cad7e77a0e56ff536d0ce8e108d8bd
User-Agent: U2L-Webhooks/1.0

Verifying signatures

The signature is HMAC-SHA256 over `${t}.${rawBody}` using your endpoint’s secret. Always verify before trusting a payload, and reject stale timestamps to block replays:

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

function verifyU2LSignature(rawBody, signatureHeader, secret, toleranceSeconds = 300) {
  const parts = Object.fromEntries(signatureHeader.split(",").map((p) => p.split("=")));
  const t = Number(parts.t);
  if (!t || Math.abs(Date.now() / 1000 - t) > toleranceSeconds) return false;
  const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`).digest("hex");
  return timingSafeEqual(Buffer.from(expected), Buffer.from(parts.v1 || ""));
}

// Express example — use the RAW body, not the parsed JSON
app.post("/u2l-webhook", express.raw({ type: "application/json" }), (req, res) => {
  if (!verifyU2LSignature(req.body.toString(), req.get("X-U2L-Signature"), process.env.U2L_WEBHOOK_SECRET)) {
    return res.status(401).end();
  }
  const event = JSON.parse(req.body);
  console.log(event.event, event.data.slug);
  res.status(200).end();
});
import hashlib, hmac, time

def verify_u2l_signature(raw_body: bytes, signature_header: str, secret: str, tolerance_seconds: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in signature_header.split(","))
    t = int(parts.get("t", 0))
    if not t or abs(time.time() - t) > tolerance_seconds:
        return False
    expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, parts.get("v1", ""))

Delivery and retries

  • Respond with any 2xx within 10 seconds to acknowledge. Redirects are treated as failures — point us at the final URL.
  • On 5xx, timeouts, or network errors we retry up to 5 times with exponential backoff (30s → 8min).
  • 4xx responses and exhausted retries count as failures. After 20 consecutive failures the endpoint is disabled automatically; re-enable it with PATCH /api/v1/webhooks/:id {"active": true} (this resets the counter).
  • Deliveries are at-least-once and not strictly ordered — use the event id for idempotency and createdAt for sequencing.

API reference

Create · List · Update · Delete