Building with the Flyn API: A Developer's Guide
A practical walkthrough of the Flyn API for engineers: authentication, link CRUD, click analytics, webhooks, rate limits, and the TypeScript SDK, with the patterns and pitfalls you'll hit shipping it to production.

Getting Started with the Flyn API
If you've ever had to bulk-create 5,000 redirect URLs for a campaign, build a customer-portal feature that generates branded short links on the fly, or auto-shorten every blog URL on publish, you already know hand-crafting short links in a dashboard doesn't scale. The Flyn API is the answer: a small, well-typed REST surface that lets you do everything the dashboard does, plus a handful of things the dashboard intentionally doesn't.
This guide walks through the API end-to-end: authentication, the nine core endpoints, the TypeScript SDK, webhooks, rate limits, and the error-handling patterns we've seen production teams converge on. If you're evaluating Flyn against Bitly, Rebrandly, or Short.io specifically for API workloads, this is the document to read.
What you need before you start
Three things: a Flyn account with a Pro plan (API access is a Pro feature), an API key from Settings > API Keys, and any HTTP client. The base URL for every request is https://www.flyn.to. Endpoints live under /api/*. Responses are JSON; requests with a body must set Content-Type: application/json.
Creating an API key
Open the dashboard, navigate to Settings > API Keys, click New key, and label it descriptively (e.g. ci-deploy-bot, marketing-platform-prod). The full key is shown exactly once on creation, copy it immediately into a secrets manager. Flyn stores only the SHA-256 hash server-side, so if you lose the plaintext, you regenerate.
API keys are scoped to a single workspace. If your team uses multiple workspaces (e.g. marketing + product), generate one key per workspace and store them under separate environment variables.
Authentication and the Endpoint Map
Every request includes an Authorization: Bearer <your-key> header. There's no signed-request flow, no OAuth dance, keep it simple, store the key as a secret. See the full authentication doc for examples in cURL, Node, Python, and Go.
The nine endpoints you'll actually use
Most integrations only touch a handful of endpoints. Here's the cheat sheet:
| Method & Path | Purpose | Docs |
|---|---|---|
POST /api/links | Create a short link | Shorten |
GET /api/links | List links (paginated, filterable) | Links |
GET /api/links/:id | Fetch a single link | Links |
PATCH /api/links/:id | Update destination, slug, tags, password, expiry | Links |
DELETE /api/links/:id | Archive (soft-delete) a link | Links |
GET /api/links/:id/clicks | Paginated click events with geo/device/referrer | Clicks |
POST /api/domains | Add a custom branded domain | Domains |
POST /api/webhooks | Subscribe to event deliveries | Webhooks |
GET /api/analytics/summary | Aggregated metrics for dashboards | Docs index |
For deep-link routing toggles, the deep_link boolean is set on POST /api/links, see the deep links doc for the routing matrix.
A minimal first call
Hit POST /api/links with a JSON body of {"destination": "https://example.com/long-url"} and you get back a JSON object with a short URL on flyn.to (or your branded domain if you specified one). That's the whole MVP. Everything else, custom slugs, tags, UTM parameters, expiry dates, passwords, is optional fields on the same payload.
Installing the TypeScript SDK
You can call the API with raw fetch, axios, got, or whatever your runtime ships with. But the official TypeScript SDK saves real engineering time: full type coverage for every request and response shape, automatic retries on 429 and 5xx with Retry-After-aware backoff, cursor and offset pagination helpers, and edge-runtime compatibility out of the box.
Install and initialize
Run npm install flyn-sdk (or your equivalent pnpm/yarn command). Initialize once at module scope:
- Node.js / Next.js API routes:
const flyn = new Flyn({ apiKey: process.env.FLYN_API_KEY }) - Cloudflare Workers: same constructor, pass
env.FLYN_API_KEYfrom the request context - Deno: import via
npm:flyn-sdkspecifier, fully ESM, nonode:shims required - Vercel Edge Functions: works on the edge runtime; the SDK uses native
fetchonly
The shape of an SDK call
Every method maps 1:1 to a REST endpoint. flyn.links.create({ destination, slug }) wraps POST /api/links. flyn.links.clicks(linkId, { from, to }) wraps GET /api/links/:id/clicks. Method signatures are typed via the public FlynApi namespace, your editor will autocomplete every field, including which ones are optional. No more squinting at JSON schemas at 2am.
Pin the SDK to a minor version in package.json ("flyn-sdk": "~2.4.0") so you get patch updates automatically but never a breaking major. The SDK follows semver strictly and the changelog calls out every breaking change.
Creating Links Programmatically
The POST /api/links endpoint takes a single JSON body and returns the created link. The only required field is destination, the long URL to redirect to.
Optional fields you'll reach for
slug, your custom path (e.g.q2-launch). If omitted, Flyn generates a 7-character random slug.domain, your branded domain (e.g.links.yoursite.com). Defaults toflyn.to.tags, array of tag names for filtering in the dashboard. New tags are auto-created.expires_at, ISO 8601 timestamp. After this moment, the link stops redirecting.password, string. Visitors see a password prompt before redirect. Hashed server-side.utm_source,utm_medium,utm_campaign,utm_term,utm_content, appended to the destination URL on every redirect.deep_link, boolean. Enables native-app routing for the 16+ platforms listed in the deep links guide.
Idempotency for safe retries
Pass an Idempotency-Key header (any unique string, e.g. a UUID) on creation. If the request fails halfway through (network blip, timeout, your function gets killed mid-flight), retrying with the same key returns the same link instead of creating a duplicate. The key is valid for 24 hours after the original request.
Bulk creation patterns
The API doesn't expose a bulk-create endpoint by design, every link is its own operation, billed and rate-limited individually. For high-volume work (importing 10,000 legacy URLs), throttle to ~9 requests/min and use exponential backoff on 429. For one-time imports, use the dashboard's CSV import tool, see how to migrate from Bitly to Flyn for the recommended workflow.
For UTM-heavy programmatic generation (e.g. campaign sequencing in a CMS), generate UTM values with the UTM Builder spec first, validate them with the redirect checker, then commit the cleaned URL to the API. Garbage in, garbage out.
Fetching Click Analytics
Click data is the reason most teams pay for a link-shortener API in the first place. The GET /api/links/:id/clicks endpoint returns paginated click events with the full attribution payload Flyn captures at redirect time. See the full clicks doc for every field.
What's in a click event
- Geo:
country(ISO-3166 alpha-2),region,city - Device:
device_type(mobile/desktop/tablet),os,browser,browser_version - Network:
asn,connection_type(mobile/wifi/wired) - Referrer: full referrer URL when available, plus a parsed
referrer_hostfor grouping - UTM: any UTM values present on the redirect, surfaced as top-level fields
- Bot signal:
is_botboolean, Flyn classifies and excludes bot clicks from default counts (read more about click fraud prevention) - Timestamp:
created_atin ISO 8601 UTC
Filtering and pagination
Use from and to query parameters (ISO 8601) to constrain by date range. Pagination is offset-based: page (1-indexed, default 1) and limit (default 50, max 100). The response's meta object contains total, page, limit, and total_pages.
For very large queries (a year of clicks on a hot link), use the SDK's flyn.links.clicks.paginate() generator, it iterates pages lazily, respecting rate limits, and yields one event at a time so you can pipe into a database without buffering everything in memory.
Aggregates vs raw events
If you only need totals (e.g. for a customer-facing dashboard), prefer GET /api/analytics/summary, it returns pre-aggregated counts by country, device, browser, and time bucket without the per-event payload. Much cheaper than fetching raw events and aggregating client-side.
Webhooks for Real-Time Events
Polling the API every minute to detect new clicks is wasteful and laggy. Use webhooks instead, Flyn pushes a JSON payload to your endpoint within ~200ms of every subscribed event. Full spec is in the webhooks doc.
Subscribable events
| Event | Fires when |
|---|---|
link.create | A new link is created (via dashboard or API) |
link.update | Any field on an existing link is modified |
link.delete | A link is archived |
link.click | A real (non-bot) human clicks a tracked link |
link.expire | A link's expires_at timestamp passes |
Verifying the signature
Every delivery includes an X-Flyn-Signature header containing an HMAC-SHA256 of the raw request body, signed with the webhook secret shown when you create the subscription. Always verify it. Compute the same HMAC on your server using the raw (un-parsed) body and compare to the header value with a constant-time comparison (Node's crypto.timingSafeEqual, Go's hmac.Equal).
Anyone who knows your webhook URL can hit it. Without signature verification, a malicious actor could forge link.click events to skew your analytics or trigger downstream side-effects (Slack notifications, CRM updates). The SDK's flyn.webhooks.verify(rawBody, signatureHeader) helper handles this in one call. Don't roll your own unless you know what constant-time comparison is.
Retries and dead-letter behavior
If your endpoint returns a non-2xx status (or doesn't respond within 10 seconds), Flyn retries with exponential backoff: 30s, 2m, 10m, 30m, 2h, 6h, 12h. After 7 failed attempts, the event lands in your dashboard's dead-letter queue, viewable under Webhooks > Failed Deliveries. You can replay any failed delivery manually from there.
Error Handling and HTTP Status Codes
The Flyn API uses standard HTTP status codes alongside a structured JSON error body. Every non-2xx response includes a code, a human-readable message, and an errors array of field-level issues. See the errors doc for the full table.
The status codes you'll see most
| Status | Meaning | Common cause |
|---|---|---|
200 | OK | Successful read |
201 | Created | Successful POST |
204 | No Content | Successful DELETE |
400 | Bad Request | Validation failed, inspect errors[] |
401 | Unauthorized | Missing or invalid API key |
403 | Forbidden | Valid key but wrong workspace |
404 | Not Found | Link/domain/webhook doesn't exist |
409 | Conflict | Slug already exists on this domain |
422 | Unprocessable Entity | Body parsed but semantics invalid (e.g. expiry in the past) |
429 | Too Many Requests | Rate limit exceeded, read Retry-After |
500+ | Server error | Retry with backoff. Status page at status.flyn.to. |
Typed errors in the SDK
The TypeScript SDK throws FlynApiError for every non-2xx response. The instance carries code, message, statusCode, and a typed field property where applicable. You can narrow on error.code === 'slug_conflict' instead of regex-matching error strings.
Handling 409 slug conflicts gracefully
If your integration lets users pick a custom slug, a 409 means someone else already took it on that domain. The UX move is to show "that slug is taken, try another?" inline, not to throw an opaque error. The SDK exposes flyn.links.checkSlug({ slug, domain }) to pre-validate availability before submission.
Rate Limits and Pagination Patterns
Flyn enforces per-key rate limits to keep the platform fair: 10 mutations per minute (any POST, PATCH, or DELETE) and 60 reads per minute (any GET). Higher limits are available on the Pro and Enterprise plans and via the contact form for unusual workloads. Full breakdown in the rate-limits doc.
Reading the rate-limit headers
Every response carries three headers you should pay attention to:
X-RateLimit-Limit, the bucket size (10 or 60)X-RateLimit-Remaining, requests left in the current windowX-RateLimit-Reset, Unix timestamp when the window resets
On 429, also read Retry-After (seconds) and back off at least that long. The SDK does this automatically.
The pagination pattern that scales
For the GET /api/links and GET /api/links/:id/clicks endpoints, use offset pagination for arbitrary page-jumping (e.g. "go to page 5") and cursor pagination (via the cursor query parameter) for full-table iteration. Cursor pagination is consistent across writes that happen mid-iteration, which offset pagination isn't.
The teams that build smooth Flyn integrations have one thing in common: they treat the API as their infrastructure, not a black box. Pin SDK versions, store rate-limit metadata, log every4xx, alert on429spikes. The API is well-typed and the docs are short, the wins come from how you wrap it.
If you're hitting the rate limits frequently, that's usually a signal to batch differently, not to request a limit increase. Pre-compute UTM-tagged URLs server-side and create them all in one cron pass instead of generating on every page load. Read the rate-limits doc for the recommended client-side queue pattern.
Real-World Patterns: CI/CD, Dashboards, and Beyond
The API surface is small enough that the interesting question is what teams build with it. Here are four patterns we see repeatedly in production. Inspiration, not prescription.
1. CI/CD preview-link automation
On every preview deployment (Vercel, Netlify, Cloudflare Pages), call POST /api/links with the preview URL as the destination and a slug like pr-{number}. Post the resulting flyn.to/pr-1234 to Slack as a deployment notification. The short link is readable in chat, click analytics show you who's reviewing your PR, and reviewers don't fumble with long preview URLs. Less than 30 lines of code in any CI platform.
2. Customer-facing link management feature
If you're a CMS, email tool, or social-media manager, your customers want branded short links for the content they create. Use the API to mint links on their behalf, pass the destination on save, store the returned link ID, surface analytics in your UI by calling GET /api/links/:id/clicks. Multi-tenant support is built in via workspaces; one workspace per customer keeps data isolated.
3. Internal link-management dashboard
For larger teams, a thin internal dashboard on top of the Flyn API gives you exactly the filters and reports your org needs. Front it with React + SWR (or your favorite data-fetching lib), use the SDK's SWR-compatible methods, layer in role-based permissions backed by your existing SSO. We see teams build this in a week and replace three half-broken Notion docs with it.
4. Automated webhook-driven workflows
Subscribe to link.click and pipe events into your CRM. When a high-value prospect clicks a proposal link, fire a Slack alert to the AE. When a customer clicks an in-app upgrade link, mark them for outreach. The webhook payload includes the full click context, geo, device, UTM, so you can branch on attribution. Read click fraud prevention first to filter bots before you act on events.
Where to go next
Read the docs index for the full API reference. If your project also needs in-tab link creation for your team, install the Flyn Chrome extension. If you're building anything that touches mobile, the deep links guide explains the routing behavior. And if your workload involves passwords, expiration, or compliance, see the link security guide for the API fields that map to those Pro features.
To see the API in the context of a specific marketing stack, the integration guides pair Flyn with GA4, Mailchimp, SendGrid, HubSpot, and ten more platforms, including when to reach for the API instead of the dashboard.
Frequently Asked Questions
Is there an official TypeScript SDK for the Flyn API?
npm install flyn-sdk. The SDK provides full TypeScript types for every request and response, automatic retry with Retry-After-aware exponential backoff on 429 and 5xx, cursor- and offset-based pagination helpers, and edge-runtime compatibility (it uses native fetch only, so it runs on Cloudflare Workers, Vercel Edge, Deno, Bun, and Node.js without polyfills). See the full docs.What are the Flyn API rate limits?
POST, PATCH, DELETE) and 60 read requests per minute (GET). Every response includes X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset headers. On 429, the Retry-After header tells you how long to back off. Higher limits are available on Enterprise, see pricing or the rate-limits doc.How do I create links in bulk via the Flyn API?
429. The SDK's built-in retry handles this automatically. For one-time imports of legacy URLs (e.g. moving from Bitly or Rebrandly), use the dashboard's CSV import, see the migration guide.How do I verify Flyn webhook payloads in my server code?
X-Flyn-Signature header containing an HMAC-SHA256 hash of the raw request body, signed with the webhook secret shown on subscription creation. Compute the same HMAC server-side and compare with a constant-time comparison function (crypto.timingSafeEqual in Node). Reject any request with a mismatched signature. The SDK provides flyn.webhooks.verify(rawBody, signatureHeader) to handle this in one call. Don't use string equality, it leaks timing information.Can I fetch click analytics for a specific link via the API?
GET /api/links/:id/clicks returns paginated click events with country, region, city, device type, OS, browser, ASN, referrer, UTM values, and a bot-detection flag. Use the from and to ISO-8601 query parameters to filter by date range. The SDK wraps this as flyn.links.clicks(linkId, { limit, page, from, to }) and provides a .paginate() generator for streaming large result sets. For aggregated metrics (counts by country, device, etc.), prefer GET /api/analytics/summary.What does a <code>409</code> response from <code>POST /api/links</code> mean?
409 Conflict means the slug you tried to create already exists on the specified domain. Slugs are unique per domain, flyn.to/sale and links.yoursite.com/sale can coexist, but two flyn.to/sale entries cannot. The response body's code field will be slug_conflict. To avoid the round-trip, call flyn.links.checkSlug({ slug, domain }) first to pre-validate availability, useful for forms where the user picks the slug.Does the Flyn API support idempotent requests?
POST /api/links. Send an Idempotency-Key header (any unique string up to 64 characters, typically a UUID per logical operation). If the request fails partway through and you retry with the same key within 24 hours, Flyn returns the original link instead of creating a duplicate. Use this in CI/CD pipelines, webhook handlers, and any code where retry-on-failure could otherwise create duplicate short links.Which Flyn API endpoints require a Pro plan?
POST /api/domains), password-protected links (the password field on link creation), and link expiration (expires_at). See pricing for the full plan matrix, and the link security guide for the security-related fields specifically.Free tools for this
Three Flyn tools that pair well with the strategy in this article, all free, no signup needed.
UTM Builder
Build campaign-tracked URLs in seconds.
Broken Link Checker
Scan any page for dead links and 404s.
Open Graph Checker
Preview how URLs unfurl on social.
Keep reading
Three related deep-dives from the Flyn blog.

How to Track Which Posts Drive Sales, Not Just Clicks
19 min read
Postback URL Tracking: How S2S Conversion Tracking Works
17 min read
Email Newsletter Click Tracking: Measure Your Real CTR
12 min read
Ready to try Flyn?
Free plan includes 25 links/month, full analytics, and access to all 30+ free tools above. No credit card required.
Already a member? Log in

Karan Bhakuni is the founder of Flyn. He writes about branded links, click analytics, and the link-management tooling growth teams and creators actually need, drawn from building Flyn and reading a lot of user feedback.