---
title: "Quick shorten a URL"
method: POST
path: "/api/shorten"
tags: ["Quick Shorten"]
---

# Quick shorten a URL

`POST /api/shorten`

## Request

- Base URL: `https://www.flyn.to`
- URL: `https://www.flyn.to/api/shorten`
- Auth: one of:
  - none
  - HTTP bearer

Minimal shortener that works with or without authentication. Returns a slim object, not a Link. No link.create webhook is sent.

**Auth:** optional. With an `Authorization` header it must be a valid API key or MCP OAuth access token (an invalid one is a 401, never an anonymous fallback). With no header, a signed-in dashboard session cookie counts as authenticated. With neither, the request is anonymous: the link is created on Flyn's anonymous short domain and expires after 24 hours.

**Bot check (anonymous and session callers):** requests without an Authorization header must send an `Accept` header and a `User-Agent` of at least 10 characters that does not look like a bot, crawler or HTTP library (curl, wget, python-requests, Postman and similar), else 403.

**Plan:** no Pro-field gating here. The plan is the effective plan for an API key or OAuth token (a member of a paying owner's workspace has `team`), but the member's own plan for a session-cookie caller, whose links are always personal, never team links. On Free the cap is 25 links per UTC month, counted per user (429 `MONTHLY_LIMIT`); when a Free key or token acts in a team, the database also counts the team's links this month and fails the create with 500 once the team has 25. Pro, Lifetime and `team` are uncapped. Anonymous callers get 5 requests per IP per 24-hour fixed window that starts at their first request (in memory per server instance). Every anonymous request that passes the bot check and the per-minute limit counts, including ones that then fail validation (429 `DAILY_LIMIT`).

**Rate limit:** 5 requests per 60 s per IP anonymous, 30 authenticated, in one shared bucket per IP (fixed window, in memory per server instance). Each per-minute 429 also blocks the IP for 60 s; requests during the block get 429 with `Retry-After` set to the seconds left. (Only overlapping requests that carry a dashboard session cookie can stretch a block to 300 s or more.) X-RateLimit-* headers are sent only on the 200 and on the per-minute 429.

## Headers

- `User-Agent` string

## Request body

- ShortenRequest
  - `url` string, required: Destination (raw length at most 2048). Scheme rules as POST /api/links. For http(s): the host needs a dot, must not be a Flyn host, passes a category blocklist (422), must resolve publicly (else 400 "Invalid URL"), then a safety scan (422).
  - `slug` string: Custom back-half, allowed for anonymous callers too. Default: 6 random characters. 409 on clash.
  - `domain` string: Anonymous callers always get the anonymous short domain; this field is ignored. Authenticated callers default to the current Flyn short domain (the legacy value `www.flyn.to` is treated as a request for that default); any other value must be a custom domain owned in scope, else 403.
  - `tags` string[]: Stored as sent for every caller. Default [].

## Response `200`

Created. Anonymous responses also set cookies that let the visitor claim the link after signing up.

- ShortenResponse
  - `id` string, uuid, required
  - `slug` string, required
  - `shortUrl` string, uri, required
  - `expiresAt` string, nullable, required: Anonymous links expire 24 hours after creation; authenticated links have no expiry (null).
  - `authenticated` boolean, required: True when the link belongs to an account (API key or session).

## Other responses

- `400`: Validation failed. Messages: "URL is required", "URL is too long (max 2048 characters)", "Unsupported URL protocol", "Invalid URL", "Cannot shorten Flyn URLs", "Slug can only contain letters, numbers, hyphens, and underscores", "Slug must be 100 characters or fewer", "This slug is reserved".
- `401`: An Authorization header was sent but is not a valid key or token.
- `403`: Bot fingerprint without an Authorization header ("Request blocked. Please use a browser."), custom domain not owned, or a suspended account.
- `409`: The slug is already taken on that domain.
- `422`: Category blocklist or safety scan.
- `429`: Penalty cooldown ("You've been temporarily blocked due to excessive requests. Please try again later.", with Retry-After), per-minute limit ("Too many requests. Please slow down and try again.", with Retry-After and X-RateLimit-*), anonymous daily cap (code `DAILY_LIMIT`, no Retry-After), or the Free monthly cap (code `MONTHLY_LIMIT`, no Retry-After).
- `500`: Invalid JSON body ("Server error") or a database error ("Failed to create link"), which includes the database's Free monthly cap on a team's links.

---

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