---
title: "Update a link"
method: PATCH
path: "/api/links/{id}"
tags: ["Links"]
---

# Update a link

`PATCH /api/links/{id}`

## Request

- Base URL: `https://www.flyn.to`
- URL: `https://www.flyn.to/api/links/{id}`
- Auth: HTTP bearer

Partial update. Only recognised fields are written. Returns the updated Link and sends a link.update webhook to the calling user's webhooks.

**Auth:** API key (`Bearer flyn_sk_live_...`), an MCP OAuth access token (`Bearer flyn_mcp_at_...`), or the signed-in dashboard session cookie when neither is sent.

**Plan:** on Free, a custom domain or a Pro field set to a real value returns 403 `UPGRADE_REQUIRED`; there is no monthly cap on edits. Pro, Lifetime or Team (a member of a paying owner's workspace has the effective plan `team`) have no gate. **Team roles:** viewer gets 403; a member may edit only links they created; owner and admin may edit any link in the workspace.

**Rate limit:** 10 requests per 60 s per client IP, fixed window, in memory per server instance, checked before authentication.

An invalid JSON body returns 500.

## Request body

- LinkUpdateRequest: Send only the fields to change. At least one recognised field is required (else 400 "No fields to update"). Fields marked **Pro** return 403 `UPGRADE_REQUIRED` on a Free plan when set to a real value; false, [], null and "" pass that plan check so a downgraded account can switch features off, but null is refused (400) for domain, deepLink, forwardParams and pixelIds: send the default short domain, false or [] instead.
  - `url` string: New destination. Same rules as create, except the host does not need a dot. On a rotating link the first rotation destination is re-pointed too. null or an empty string returns 400.
  - `slug` string: New back-half; not reserved; 409 on clash. null or an empty string returns 400.
  - `domain` string: Moves the link to another short domain. The current Flyn short domain, or the legacy value `www.flyn.to`, moves it back to the default. Any other value must exactly match (case-sensitive) a custom domain owned by the workspace (the team in a team workspace, otherwise your own), else 403 "Domain not found or not owned by you"; that includes `flyn.to` and other customers' domains. A pending or paused domain is accepted, but the link does not serve until the domain is active. Sending the link's current domain changes nothing. **Pro** for a custom domain. null or an empty string returns 400.
  - `title` string
  - `status` 'active' | 'expired' | 'archived': Any other value returns 400, including an empty string or null.
  - `tags` string[]: **Pro** when non-empty. Replaces the array.
  - `password` string, nullable: **Pro** when non-empty. A non-empty string is hashed and set; null or "" removes the password.
  - `expiresAt` string, date-time, nullable: Must parse as a date; null clears it.
  - `folderId` string, uuid, nullable: Folder in the workspace (404 otherwise); null clears it.
  - `ogTitle` string
  - `ogDescription` string
  - `ogImage` string: http or https, public IP.
  - `targeting` Targeting: 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: **Pro** when true.
  - `noIndex` boolean: **Pro** when true. Accepted and stored for compatibility. 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
  - `rotation` RotationEntryInput[], nullable: **Pro** when it normalises to a rotation. Replaces the whole list (existing ids kept, missing ids minted). [], null or an all-invalid list clears the rotation and also clears abUrlB/abSplit unless abUrlB is sent in the same body. Destinations follow the create rules, including 400 "Cannot shorten Flyn URLs" for a Flyn host.
    - `id` string: Keep an existing destination id. Missing ids are minted (8 characters).
    - `url` string, required: Destination. `https://` is prepended when there is no scheme; must be http or https and resolve to a public IP (else 400); safety scanned (422). An entry with an empty or missing url is dropped silently, not rejected.
    - `weight` number, required: Relative weight. Non-finite or <= 0 becomes 0 (never served). If every weight is 0, no rotation is stored.
    - `label` string: Optional display label.
  - `abUrlB` string, nullable: **Pro** when non-empty. Returns 409 `ROTATION_ACTIVE` when the link has a stored rotation other than the legacy two-destination A/B pair (ids A and B) and `rotation` is not sent in the same request; update `rotation` instead. Otherwise the create rules apply, including 400 "Cannot shorten Flyn URLs" for a Flyn host.
  - `abSplit` number, nullable: **Pro** when non-zero. Clamped to 0..100; same 409 rule as abUrlB.
  - `deepLink` boolean: **Pro** when true. Send a JSON boolean; null returns 400.
  - `pixelIds` string[]: **Pro** when non-empty. Ids outside the workspace are dropped; [] clears; null returns 400. If any id is not a valid UUID the list is set to [].
  - `forwardParams` boolean: null returns 400.
  - `clickLimit` integer, nullable: **Pro** when set. null or "" clears it.
  - `fallbackUrl` string, nullable: **Pro** when set. null or "" clears it; otherwise the same rules as create.
  - `conversionTracking` boolean: **Pro** when true. Stored as `value === true`, so any other value (including the string "true") turns tracking off.

## Response `200`

The updated link.

- 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.

## Other responses

- `400`: Nothing recognised in the body, or a validation failure. Messages include "Request body must be valid JSON", "Request body must be a JSON object", "No fields to update", "url must be a non-empty string", "Cannot shorten Flyn URLs", "slug must be a non-empty string", "deepLink must be true or false", "forwardParams must be true or false", "pixelIds must be an array of pixel ids; send [] to clear", "domain must be a non-empty string. To move a link back to the default short domain, send \"<the default short domain>\".", "Invalid status. Use: active, expired, archived", "This slug is reserved", targeting errors with code `INVALID_TARGETING`, "A link can rotate between at most 12 destinations. You sent 13.", and the URL, date and click-limit messages of create.
- `401`: No valid API key and no dashboard session. Messages: "Invalid or revoked API key", "API key has expired", "Unauthorized. Sign in to the dashboard, or pass a Bearer API key in the Authorization header.", or "Unauthorized" when the session lookup itself fails. An MCP OAuth token lands here too.
- `403`: Viewer role, a member editing another member's link ("Only the person who created this link, or a workspace owner or admin, can edit it."), a Free custom domain or Pro field (code `UPGRADE_REQUIRED` with `premiumFields`), a domain the workspace does not own ("Domain not found or not owned by you"), or a suspended account.
- `404`: The link or the folderId is not in the workspace ("Link not found" or "Folder not found").
- `409`: Slug clash ("Slug already exists"), including when the back-half already exists on the domain the link is moving to, or abUrlB/abSplit sent without `rotation` for a link whose stored rotation is anything other than the legacy two-destination A/B pair with ids A and B (code `ROTATION_ACTIVE`; the message says "more than two destinations" even when there are fewer).
- `422`: A URL that would be served failed the safety scan.
- `429`: More than 10 requests in the window. Counted per client IP. Retry-After gives the seconds until the window resets.
- `500`: A database error.

---

[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)
