Skip to content
fy9.in

Developers

FY9 API

Create short links and QR codes from your own systems and read their analytics. JSON over HTTPS, one key per integration.

Quick start

  1. API access is included in the Business and Agency plans (and in legacy plans that had it). Create a key under Developers in your dashboard and choose its scopes.
  2. Send it as a bearer token. The key is shown once; FY9 stores only a hash.
  3. Create a link:
curl
curl -X POST https://fy9.in/api/v1/links \
  -H "Authorization: Bearer fy9_YOUR_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: order-1042-link" \
  -d '{"destination":"https://example.com/offer","alias":"offer-1042","kind":"dynamic","tags":["email"]}'
Response 200
{
  "data": {
    "id": 98213,
    "code": "offer-1042",
    "shortUrl": "https://fy9.in/offer-1042",
    "kind": "dynamic",
    "status": "active",
    "destination": "https://example.com/offer",
    "tags": ["email"],
    "visitCount": 0,
    "createdAt": "2026-10-18T09:12:44Z"
  }
}
JavaScript (fetch)
const res = await fetch("https://fy9.in/api/v1/links", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.FY9_API_KEY}`, "Content-Type": "application/json" },
  body: JSON.stringify({ destination: "https://example.com/offer" }),
});
if (!res.ok) throw new Error((await res.json()).detail);
const { data } = await res.json();
console.log(data.shortUrl);

Authentication

Every request needs Authorization: Bearer fy9_…. Keys belong to one workspace and carry scopes:

  • links:read — list and read links
  • links:write — create and change links
  • qr:read, qr:write — read and create QR codes
  • analytics:read — read link analytics

Rotating a key issues a new secret and keeps the old one working for 24 hours. Revoked or expired keys get 401 invalid_api_key. A key without the needed scope gets 403 insufficient_scope. Keep keys on your server; never put them in web pages or apps.

Credits and limits

Operations that create or change something use one credit each: POST /links, PATCH /links/{id}, POST /qr-codes and the legacy /api/GenerateUrl. Reading data is free. Redirect visits never use credits.

  • Your plan includes a monthly allowance (Business 5,000; Agency 25,000), reset on the 1st of each month in India time.
  • Credit packs (10,000, 100,000 or 1,000,000 operations) are used after the allowance, earliest expiry first, and last 365 days.
  • A request that fails (validation error, conflict, server error) gives its credit back. A retry with the same Idempotency-Key is never charged twice.
  • When nothing is left you get 402 credits_exhausted.
  • Rate limit: 120 requests a minute per key. Over the limit you get 429 with a Retry-After header.

Create — POST /api/v1/links

Body fields: destination (required, http/https), alias, kind (static or dynamic), title, description, tags, expiresAt, activatesAt, maxVisits, password, utm ({source, medium, campaign, term, content}, merged into the destination), rules (smart routing) and reuseExisting (return an identical existing static link instead of creating a duplicate). Features your plan doesn't include return 402 plan_limit with the plan that includes them.

List — GET /api/v1/links

Query: search, tag, status, kind, sort (created, clicks, title), descending, page, pageSize (max 100). Responses carry meta.total, meta.page and meta.pageSize.

Read and change — GET / PATCH /api/v1/links/{id}

PATCH accepts the fields to change, plus status (active, disabled, archived). Changing the destination of a static link returns 409 static_link: static links never change by design. Dynamic link changes are recorded in the link's history.

QR codes

POST /api/v1/qr-codes with name, kind (static or dynamic), template (url, menu, review, whatsapp, payment, event, vcard), content and an optional style (foreground, background, errorCorrection). Dynamic codes encode https://fy9.in/q/{code} so their visits are reported as scans. Download the vector file from GET /api/v1/qr-codes/{id}.svg. Styles that wouldn't scan reliably (low contrast, light-on-dark) are rejected with an explanation.

Analytics

GET /api/v1/links/{id}/analytics?from=2026-10-01&to=2026-10-18 returns totals, a daily series and breakdowns by country, device, browser, OS, referrer and language. Ranges are limited to your plan's retention (rangeClamped tells you when that happened). Unique visitors are an estimate; the response's uniqueMethod explains how it's calculated. Values FY9 couldn't determine appear as Unknown.

Usage

GET /api/v1/usage returns operations used this month, credits remaining, each credit lot with its expiry, and the list of metered operations.

Errors

Errors are problem details with a stable code, a human-readable detail and sometimes details (for example the field at fault):

Response 400
{ "status": 400, "title": "Invalid request", "code": "validation",
  "detail": "Links to private or local network addresses can't be shortened.",
  "details": { "field": "destination", "problem": "PrivateAddress" } }
  • validation (400), invalid_api_key (401), plan_limit or credits_exhausted (402), insufficient_scope or forbidden (403), not_found (404), alias_taken, static_link (409), idempotency_conflict (422), rate_limited (429), server_error (500, with a traceId to quote to support).

Idempotency

Send Idempotency-Key: <your id> (up to 100 characters) on POST requests. Repeating the same request with the same key within 24 hours returns the original response with Idempotent-Replayed: true and isn't charged. Reusing a key with a different body returns 422 idempotency_conflict.

Legacy API

Integrations built for the previous fy9.in API keep working unchanged: POST /api/GenerateUrl (fields longurl, apikey, urlexpiry in minutes, issingleclick), POST /api/GenerateShortUrl?WebUrl=…, GET /api/ClickCountUrl?Url=… and GET /api/GetPackageList. They answer in the same format as before ({ message: "success" | "failure", … }). Legacy keys stay valid for as long as the package they were issued with. New integrations should use /api/v1.

OpenAPI

The machine-readable description generated from the running API is at /api/openapi/v1.json. Import it into Postman, Insomnia or a code generator.