Back to Blog

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.

Karan Bhakuni
Karan Bhakuni
Founder, Flyn
DeveloperFeb 8, 202612 min readUpdated May 5, 2026
Building with the Flyn API: A Developer's Guide

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 & PathPurposeDocs
POST /api/linksCreate a short linkShorten
GET /api/linksList links (paginated, filterable)Links
GET /api/links/:idFetch a single linkLinks
PATCH /api/links/:idUpdate destination, slug, tags, password, expiryLinks
DELETE /api/links/:idArchive (soft-delete) a linkLinks
GET /api/links/:id/clicksPaginated click events with geo/device/referrerClicks
POST /api/domainsAdd a custom branded domainDomains
POST /api/webhooksSubscribe to event deliveriesWebhooks
GET /api/analytics/summaryAggregated metrics for dashboardsDocs 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_KEY from the request context
  • Deno: import via npm:flyn-sdk specifier, fully ESM, no node: shims required
  • Vercel Edge Functions: works on the edge runtime; the SDK uses native fetch only

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.

Pro tip

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.

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_host for grouping
  • UTM: any UTM values present on the redirect, surfaced as top-level fields
  • Bot signal: is_bot boolean, Flyn classifies and excludes bot clicks from default counts (read more about click fraud prevention)
  • Timestamp: created_at in 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

EventFires when
link.createA new link is created (via dashboard or API)
link.updateAny field on an existing link is modified
link.deleteA link is archived
link.clickA real (non-bot) human clicks a tracked link
link.expireA 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).

Watch out

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

StatusMeaningCommon cause
200OKSuccessful read
201CreatedSuccessful POST
204No ContentSuccessful DELETE
400Bad RequestValidation failed, inspect errors[]
401UnauthorizedMissing or invalid API key
403ForbiddenValid key but wrong workspace
404Not FoundLink/domain/webhook doesn't exist
409ConflictSlug already exists on this domain
422Unprocessable EntityBody parsed but semantics invalid (e.g. expiry in the past)
429Too Many RequestsRate limit exceeded, read Retry-After
500+Server errorRetry 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 window
  • X-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 every 4xx, alert on 429 spikes. The API is well-typed and the docs are short, the wins come from how you wrap it.
Note

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.

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.

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.

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?
Yes. Install with 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?
Free and Pro plans share the same defaults: 10 mutation requests per minute (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?
The API doesn't expose a single bulk-create endpoint by design, every link is an individual operation. For high-volume programmatic creation (thousands of links), throttle to ~9 requests/min and use exponential backoff on 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?
Every webhook delivery includes an 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?
Yes. 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?
A 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?
Yes, for 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?
API access is a Pro feature, so the core API surface, creating, reading, updating, and deleting links, fetching click analytics, and managing webhooks, requires a Pro plan. Additional Pro features that affect API behavior include custom branded domains (via 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.

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
Karan Bhakuni· Founder, Flyn

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.