Skip to Content
Retargeting Pixels on every Link 🎉
Link Squeeze LogoLink Squeeze Docs
FeaturesToolsPricingBlog
CTRL K
CTRL K
    • How to Add Custom Domains to Link Squeeze
    • How to Shorten Links Using Link Squeeze
    • How to Shorten Shopify URLs Using Link Squeeze
    • How to Add Google Ads Tag to Link Squeeze
    • How to Add Meta Pixel to Link Squeeze
    • How to Add Retargeting Pixels to links in Link Squeeze
    • How to Create a Custom Audience on Facebook and Instagram
    • Authentication
    • Links
    • Click Analytics
    • Domains
    • Pixels
    • Webhooks
    • Errors & Rate Limits
  • Features
  • Tools
  • Pricing
  • Blog
    • How to Add Custom Domains to Link Squeeze
    • How to Shorten Links Using Link Squeeze
    • How to Shorten Shopify URLs Using Link Squeeze
    • How to Add Google Ads Tag to Link Squeeze
    • How to Add Meta Pixel to Link Squeeze
    • How to Add Retargeting Pixels to links in Link Squeeze
    • How to Create a Custom Audience on Facebook and Instagram
    • Authentication
    • Links
    • Click Analytics
    • Domains
    • Pixels
    • Webhooks
    • Errors & Rate Limits

On This Page

  • Events
  • The webhook object
  • Create a subscription
  • URL requirements
  • List subscriptions
  • Delete a subscription
  • The delivery payload
  • Headers
  • link.created
  • link.clicked
  • pixel.fired
  • Verifying a delivery
  • Read the raw body
  • Recompute the HMAC
  • Compare in constant time
  • Check the timestamp
  • Retries and failures
  • Building a reliable consumer
Question? Give us feedback
API ReferenceWebhooks

Webhooks

Webhooks push events to your server as they happen, so you do not have to poll for new clicks. Subscribe an HTTPS endpoint to one or more event types, and Link Squeeze POSTs a signed JSON payload to it every time one of those events fires.

MethodPathScope
GET/v1/webhookswebhooks:manage
POST/v1/webhookswebhooks:manage
DELETE/v1/webhooks/{id}webhooks:manage

There is no update endpoint. To change a subscription’s URL or event types, delete it and create a new one — which also issues a new signing secret.

Events

EventFires whendata payload
link.createdA short link is created, via the API or the dashboardThe link object
link.clickedA short link is visitedThe click object
pixel.firedA retargeting pixel fires on a link’s redirect pageA pixel-firing object (below)

The webhook object

{ "id": "b8e3f0a2-5d61-4c93-8a47-6f2e1b9d0c54", "url": "https://acme.com/hooks/linksqueeze", "event_types": ["link.created", "link.clicked"], "created_at": "2026-08-28T10:14:02.000000Z", "updated_at": "2026-08-28T10:14:02.000000Z" }
FieldTypeDescription
iduuidThe subscription’s identifier
urlstringWhere deliveries are sent
event_typesarray of stringsEvents this subscription receives
created_attimestampWhen the subscription was created
updated_attimestampWhen the subscription was last modified

Create a subscription

POST /v1/webhooks

Scope: webhooks:manage

FieldTypeRequiredDescription
urlstringYesAn HTTPS URL on a publicly reachable host, maximum 2048 characters.
event_typesarray of stringsYesAt least one of link.created, link.clicked, pixel.fired.

Returns 201 Created. The response is the only place the signing secret ever appears:

{ "data": { "id": "b8e3f0a2-5d61-4c93-8a47-6f2e1b9d0c54", "url": "https://acme.com/hooks/linksqueeze", "event_types": ["link.created", "link.clicked"], "created_at": "2026-08-28T10:14:02.000000Z", "updated_at": "2026-08-28T10:14:02.000000Z", "secret": "yGq2v9NfC3xJ8bT1sR6wE4dP7hK0mZaLuVnQiOe5" } }
curl -X POST https://app.linksqueeze.io/api/v1/webhooks \ -H "Authorization: Bearer $LINKSQUEEZE_API_KEY" \ -H "Content-Type: application/json" \ -H "Accept: application/json" \ -d '{ "url": "https://acme.com/hooks/linksqueeze", "event_types": ["link.created", "link.clicked"] }'

Store the secret now. It is returned once, on creation, and never again — not by GET /v1/webhooks, not by anything in the dashboard. Without it you cannot verify deliveries, and the only recovery is to delete the subscription and create a new one.

URL requirements

The URL must use https and must resolve to a publicly routable address. URLs pointing at loopback, private, or link-local ranges are rejected with 422 — a webhook that could reach internal infrastructure would be a server-side request forgery vector.

This is enforced again at delivery time, against the address the request actually connects to, so a hostname that is re-pointed at a private address after registration is blocked rather than followed. Redirects are never followed for the same reason.

List subscriptions

GET /v1/webhooks

Scope: webhooks:manage

Returns your subscriptions, newest first, in the standard paginated shape. Supports page and per_page (default 15, maximum 100). Secrets are not included.

curl https://app.linksqueeze.io/api/v1/webhooks \ -H "Authorization: Bearer $LINKSQUEEZE_API_KEY" \ -H "Accept: application/json"

Delete a subscription

DELETE /v1/webhooks/{id}

Scope: webhooks:manage

Returns 204 No Content. Deliveries stop immediately.

curl -X DELETE https://app.linksqueeze.io/api/v1/webhooks/b8e3f0a2-5d61-4c93-8a47-6f2e1b9d0c54 \ -H "Authorization: Bearer $LINKSQUEEZE_API_KEY" \ -H "Accept: application/json"

The delivery payload

Every delivery is a POST with a JSON body in this envelope:

{ "id": "e7d41c98-2a06-4f35-9b8e-3c5a7d1f6024", "api_version": "v1", "event": "link.created", "created_at": "2026-08-28T10:14:02+00:00", "data": { } }
FieldTypeDescription
iduuidUnique id for this delivery. Use it to deduplicate — retries reuse the same id.
api_versionstringEnvelope version, currently v1.
eventstringThe event type that fired.
created_attimestampWhen the event was recorded.
dataobjectThe event payload, described below.

Headers

HeaderExampleDescription
X-Signaturet=1756377242,v1=3a1f...Timestamped HMAC signature. See Verifying.
X-Webhook-Timestamp1756377242Unix timestamp the delivery was signed at.
X-Delivery-Ide7d41c98-2a06-4f35-9b8e-...Same value as id in the body.
X-Event-Typelink.createdSame value as event in the body.
Content-Typeapplication/json

The data object is the same shape the REST API returns for that resource. A link.created payload is byte-for-byte the link object you would get from GET /v1/links/{id}, so you can parse both with one model. Internal columns and numeric primary keys are never included — id is always the public UUID.

link.created

data is the link object.

{ "id": "e7d41c98-2a06-4f35-9b8e-3c5a7d1f6024", "api_version": "v1", "event": "link.created", "created_at": "2026-08-28T10:14:02+00:00", "data": { "id": "9b1f4e2a-6c3d-4a17-9f2e-7c8d5a1b3e40", "title": "Summer Sale", "destination_url": "https://example.com/summer-sale", "short_url": "go.acme.com/summer", "slug": "summer", "hostname": "go.acme.com", "utm_params": [], "tags": [], "expires_at": null, "pixel_ids": [], "clicks": 0, "status": true, "created_at": "2026-08-28T10:14:02.000000Z", "updated_at": "2026-08-28T10:14:02.000000Z" } }

Fires for links created through the API and through the dashboard.

link.clicked

data is the click object, with an additional link_id naming the link that was clicked:

{ "id": "a3c7b219-8e04-4d61-95f2-7b1c6a0d3e58", "api_version": "v1", "event": "link.clicked", "created_at": "2026-08-28T10:16:31+00:00", "data": { "id": "c41d8b30-9e57-4a22-b6f8-1d0e7a94c2b5", "link_id": "9b1f4e2a-6c3d-4a17-9f2e-7c8d5a1b3e40", "ip": "203.0.113.42", "country": "US", "visitor_id": "visitor-9f2c1b", "referrer": "https://twitter.com/", "device": "mobile", "browser": "Safari", "platform": "iOS", "created_at": "2026-08-28T10:16:31.000000Z" } }

link_id is the link’s public UUID, so you can correlate the click back to a link you fetched from GET /v1/links.

pixel.fired

Fires once per pixel on a link that has retargeting pixels attached. A link with two pixels produces two deliveries per click.

{ "id": "f2b90d47-6c15-4e83-a071-9d3e2c5b8a16", "api_version": "v1", "event": "pixel.fired", "created_at": "2026-08-28T10:16:31+00:00", "data": { "pixel_id": "3f9c8a71-2b4d-4e60-8a15-9d7c6b3e21f8", "platform": "facebook", "link_id": "9b1f4e2a-6c3d-4a17-9f2e-7c8d5a1b3e40", "visit_id": "c41d8b30-9e57-4a22-b6f8-1d0e7a94c2b5" } }
FieldTypeDescription
pixel_iduuidThe Link Squeeze pixel that fired — not the platform’s own pixel identifier
platformstringThe pixel’s platform
link_iduuidThe link that was clicked
visit_iduuid | nullThe click that triggered it, matching id on the corresponding link.clicked

Join visit_id against a link.clicked delivery’s data.id to tie a pixel firing to its click.

Verifying a delivery

Every delivery is signed with the subscription’s secret. Verify the signature before trusting a payload — anyone can POST to your endpoint.

The X-Signature header carries two comma-separated fields:

X-Signature: t=1756377242,v1=3a1f9c...e42b
  • t — the Unix timestamp the delivery was signed at
  • v1 — a hex-encoded HMAC-SHA256 of {t}.{raw request body}, keyed with your webhook’s secret

The timestamp is inside the signed string, not merely alongside it, so a captured request cannot be replayed indefinitely.

Read the raw body

Capture the request body as raw bytes, before any JSON parsing. Re-serialising parsed JSON produces different bytes and the signature will not match.

Recompute the HMAC

Concatenate the timestamp, a literal ., and the raw body, then HMAC-SHA256 it with your secret.

Compare in constant time

Compare your result to the v1 field using a timing-safe comparison.

Check the timestamp

Reject deliveries whose t is more than 5 minutes (300 seconds) away from your current time, to bound the replay window.

Node.js

import crypto from "node:crypto"; const TOLERANCE_SECONDS = 300; // rawBody must be a Buffer or string of the unparsed request body. export function verifyLinkSqueezeWebhook(rawBody, signatureHeader, secret) { const parts = Object.fromEntries( signatureHeader.split(",").map((p) => p.split("=")), ); const timestamp = Number(parts.t); if (!timestamp || Math.abs(Date.now() / 1000 - timestamp) > TOLERANCE_SECONDS) { return false; } const expected = crypto .createHmac("sha256", secret) .update(`${parts.t}.${rawBody}`) .digest("hex"); const received = Buffer.from(parts.v1 ?? "", "utf8"); const computed = Buffer.from(expected, "utf8"); return ( received.length === computed.length && crypto.timingSafeEqual(received, computed) ); }

With Express, get the raw body using express.raw({ type: "application/json" }) on the webhook route rather than express.json().

PHP

<?php const TOLERANCE_SECONDS = 300; function verifyLinkSqueezeWebhook(string $rawBody, string $signatureHeader, string $secret): bool { parse_str(str_replace(',', '&', $signatureHeader), $parts); $timestamp = (int) ($parts['t'] ?? 0); if (! $timestamp || abs(time() - $timestamp) > TOLERANCE_SECONDS) { return false; } $expected = hash_hmac('sha256', "{$timestamp}.{$rawBody}", $secret); return hash_equals($expected, $parts['v1'] ?? ''); }

In Laravel, read the raw body with $request->getContent(), and exempt the route from CSRF verification.

Python

import hashlib import hmac import time TOLERANCE_SECONDS = 300 def verify_linksqueeze_webhook(raw_body: bytes, signature_header: str, secret: str) -> bool: parts = dict( part.split("=", 1) for part in signature_header.split(",") if "=" in part ) try: timestamp = int(parts["t"]) except (KeyError, ValueError): return False if abs(time.time() - timestamp) > TOLERANCE_SECONDS: return False signed_payload = f"{timestamp}.".encode() + raw_body expected = hmac.new( secret.encode(), signed_payload, hashlib.sha256 ).hexdigest() return hmac.compare_digest(expected, parts.get("v1", ""))

Retries and failures

BehaviourValue
Success conditionAny 2xx response
Request timeout10 seconds
AttemptsUp to 5
Backoff between tries10s, 30s, 60s, 5m, 15m
RedirectsNot followed — a 3xx counts as a failure

A delivery that returns a non-2xx status or times out is retried on the schedule above. After the fifth attempt it is marked failed and no further attempts are made.

A delivery whose URL resolves into private address space at send time is marked failed immediately, without consuming any attempts — retrying cannot make a private address public.

Building a reliable consumer

  • Respond fast. Acknowledge with 2xx as soon as you have stored the payload, then process it out of band. Anything slower than 10 seconds is treated as a failure and retried.
  • Deduplicate on id. Retries reuse the same delivery id, and a slow-but-successful handler can be retried after it has already done the work. Treat handlers as idempotent.
  • Do not rely on ordering. Retries and backoff mean a link.clicked delivery can arrive after a later one. Order by the payload’s created_at, not arrival time.
  • Verify before parsing. Signature-check the raw body first, and reject anything that fails, before acting on its contents.
Last updated on August 28, 2026
PixelsErrors & Rate Limits
Link Squeeze LogoLink Squeeze

Branded short links with built-in retargeting pixels for performance marketers.

Product
  • Pricing
  • Sign up
  • Log in
  • Use cases
  • Docs
  • API
  • Blog
Free tools
  • Link shortener
  • UTM builder
  • Link Preview Generator
  • All tools
Legal
  • Privacy policy
  • Terms of service
  • Support
Compare
  • Bitly
  • Rebrandly
  • Short.io
  • Replug
  • Dub
© 2026 Link Squeeze. All rights reserved.Made for marketers who measure everything.