---
title: "A link was created"
method: POST
path: "link.create"
tags: ["Webhooks"]
---

# A link was created

`POST link.create` (webhook)

Sent to the acting user's webhooks when a link is created with POST /api/links (data is the full Link) or in the Flyn dashboard (data is `{id, slug, destinationUrl, domain}`). Links created with POST /api/shorten or POST /api/links/bulk-create do not send it.

Every delivery is an HTTP POST with a JSON body `{event, timestamp, data}`. `X-Flyn-Signature` is `t=<timestamp>,v1=<hex>`, where v1 is HMAC-SHA256 keyed with the full webhook secret (including the `whsec_` prefix) over `<t>.<raw request body>`. To verify: split the header on commas, recompute the HMAC over t, a dot and the raw body bytes (not a re-serialised body), compare in constant time, and reject a t (milliseconds) older than your tolerance. Flyn makes one delivery attempt per event with a 10 s timeout (redirects included). A 2xx answer counts as success; a non-2xx answer counts as a failure, and after 10 counted failures in a row the webhook is set inactive. A URL whose host resolves to a private address or fails DNS resolution is not sent to and counts as a failure. Timeouts, connection errors and redirects to a private host are not counted.

## Headers

- `X-Flyn-Signature` string, required
- `X-Flyn-Event` string, required
- `User-Agent` 'Flyn-Webhook/1.0', required

## Payload

- LinkCreateEvent
  - `event` 'link.create', required
  - `timestamp` integer, required: Unix time in milliseconds.
  - `data` union, required: The full Link for links created with POST /api/links; a short object for links created in the dashboard.
    - Link: A short link, as returned by every /api/links endpoint and sent in link.create, link.update and link.delete webhook deliveries. Every field is always present.
      - `id` string, uuid, required: Link id.
      - `slug` string, required: Back-half of the short URL.
      - `url` string, required: Destination URL.
      - `shortUrl` string, uri, required: Full short URL, `https://<domain>/<slug>`.
      - `domain` string, required: Short domain the link lives on. New links default to the current Flyn short domain.
      - `title` string, nullable, required: Display title.
      - `clicks` integer, required: Human clicks (bots and scanners excluded). This is the headline number.
      - `botClicks` integer, required: Clicks filtered out as bots or scanners.
      - `qrScans` integer, required: Clicks that arrived from a Flyn QR code; a subset of `clicks`.
      - `totalClicks` integer, required: `clicks` + `botClicks`.
      - `status` 'active' | 'expired' | 'archived', required: Stored status. A link past `expiresAt` keeps `active` here; the redirect checks the expiry itself.
      - `tags` string[], required
      - `createdAt` string, date-time, required
      - `expiresAt` string, nullable, required: Expiry as stored (ISO 8601), or null.
      - `folderId` string, uuid, nullable, required
      - `password` boolean, required: True when the link is password protected. The password itself is never returned.
      - `ogTitle` string, nullable, required
      - `ogDescription` string, nullable, required
      - `ogImage` string, nullable, required
      - `targeting` Targeting, required: Smart-routing rules as stored. `device` takes four keys, `ios`, `android`, `windows` and `macos`, each applying only to visitors on that operating system, and a device rule takes priority. There is no `desktop` key: Linux, ChromeOS and any other or unrecognised system match no device rule and fall through to the link's other rules. `windows` and `macos` must be http or https URLs, and the aliases `mac`, `osx`, `win`, `desktop` and `linux` are rejected with 400 `INVALID_TARGETING`. `geo` maps ISO 3166-1 alpha-2 country codes (upper case) to a destination and applies to every visitor whose country is known, desktop included. A visitor matching no rule falls through to the rotation, or the legacy A/B split, when one is set, else goes to the link url. A PATCH replaces the whole targeting object; nothing is merged into the stored rules. Safari on an iPad usually sends the same user agent as Safari on a Mac, so on a link with a `macos` destination, Safari on a Mac or an iPad (or any browser sending that same user agent) first loads a blank check page that tells a Mac from an iPad, then continues at once. A browser with JavaScript off continues as a Mac. The check page records no click; the click is recorded once, when the browser continues to the short link with the reserved query parameters `flyn_os` and `flyn_ref` (the original referrer), which are never passed to a destination. Chrome, Edge and Firefox on a Mac skip the check page, and a link without a `macos` rule never shows it. Device and country rules, including the Safari check page, do not apply to password-protected links, to a link that has reached its click limit, or to mailto, tel and sms links.
        - `device` object
          - `ios` string: Destination for iOS visitors, such as an iPhone. Safari on an iPad usually sends the same user agent as Safari on a Mac (see Targeting).
          - `android` string: Destination for Android visitors.
          - `windows` string: Destination for Windows visitors. Must be an http or https URL, for example a Microsoft Store or download page.
          - `macos` string: Destination for Mac visitors. Must be an http or https URL, for example a Mac App Store or download page. Setting it turns on the Safari check page described on Targeting.
        - `geo` object: Country code to destination, for example {"US": "https://example.com/us"}.
      - `cloaking` boolean, required
      - `noIndex` boolean, required: Stored value of the legacy per-link no-index flag. It has no effect on how the link is served: every short link is already served with a noindex robots header on every plan, whether or not this is set.
      - `notes` string, nullable, required: Private notes.
      - `abUrlB` string, nullable, required: Legacy two-way A/B view of `rotation`, kept for older integrations.
      - `abSplit` number, nullable, required: Legacy A/B split: percent kept on the primary url.
      - `rotation` RotationEntry[], nullable, required: Weighted destinations, or null when the link does not rotate.
        - `id` string, required: Stable destination id (8 characters when minted by Flyn). Reported as `variant` on link.click deliveries.
        - `url` string, uri, required: Destination URL.
        - `weight` number, required: Relative weight, not a percentage. Weights are normalised when a visitor is routed.
        - `label` string, nullable, required: Optional display label.
      - `deepLink` boolean, required: Native-app handoff mode on mobile.
      - `pixelIds` string[], required: Retargeting pixels fired on click.
      - `forwardParams` boolean, required: Query-string passthrough to the destination.
      - `clickLimit` integer, nullable, required: Maximum human clicks. Once reached, visitors go to `fallbackUrl`, or to the expired-link page when no http(s) `fallbackUrl` is set.
      - `fallbackUrl` string, nullable, required: Destination after the click limit is reached.
      - `conversionTracking` boolean, required: When true, each human click gets a `flyn_id` click id for conversion pings.
      - `conversions` integer, required: Recorded conversions, net of refunds (never below 0).
      - `conversionValueCents` integer, required: Summed conversion value in minor units.
      - `conversionCurrency` string, nullable, required: Currency of the summed value.
    - DashboardLinkCreateData: data of a link.create delivery for a link created in the Flyn dashboard.
      - `id` string, uuid, required
      - `slug` string, required
      - `destinationUrl` string, required
      - `domain` string, required

## Acknowledgement `200`

Return any 2xx to acknowledge the delivery.

---

[API](https://skmtc.dev/flyn/apis/flyn-api.md) · [All operations](https://skmtc.dev/flyn/apis/flyn-api/llms.txt) · [OpenAPI document](https://skmtc.dev/flyn/apis/flyn-api/revisions/977962a98ccb?raw)
