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
| Event | Fires when | data payload |
|---|---|---|
link.created | A link or QR code is created (dashboard, API, extension, CLI, bot) | The created link object |
link.updated | A link’s destination, title, or settings change | The updated link object |
link.deleted | A 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
idfor idempotency andcreatedAtfor sequencing.