Skip to main content

Developer guide

Integrate ilync in minutes

From a first API call through QR and barcode generation, authentication, rate limits, link safety, link-in-bio pages, private links, analytics, avatar management, admin endpoints, custom domains, and account deletion.

Overview

ilync turns long URLs into short, shareable codes and redirects visitors to the original destination. Use it from the browser or programmatically over a small JSON API.

  • Instant shortening
  • Accounts & dashboard
  • Optional expiration
  • Click analytics
  • QR & barcode API
  • API keys & rate limits
  • Advanced safety screening
  • Free public URL checker
  • Link in bio pages
  • Private links
  • Passcode-protected links
  • Avatar upload & removal
  • Admin link management
  • Custom domains
  • Light & dark themes

Quick start

ilync is a hosted HTTP service, so there is nothing to install or deploy to start using it. Point your application at the API and you can create short links in minutes. Every endpoint accepts and returns JSON over HTTPS.

1. Get an API key (recommended)

Anonymous requests work out of the box with a lower rate limit. For higher limits, link ownership, and private links, sign in and create a key under Developer API, then send it as a bearer token on each request. Store it as a secret in your backend — never ship it in client-side code.

2. Create your first short link

Send a POST to /api/urls with the URL to shorten. The Authorization header is optional but recommended for production integrations.

cURL
curl -X POST https://ilync.dev/api/urls \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_your_key_here' \
  -d '{"url":"https://example.com/a/very/long/path"}'
JavaScript (fetch)
const res = await fetch("https://ilync.dev/api/urls", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    Authorization: "Bearer ilk_your_key_here", // optional
  },
  body: JSON.stringify({ url: "https://example.com/a/very/long/path" }),
});

if (!res.ok) throw new Error(`ilync error: ${res.status}`);

const { shortUrl } = await res.json();
console.log(shortUrl); // → https://ilync.dev/Ab3xK9
Python (requests)
import requests

res = requests.post(
    "https://ilync.dev/api/urls",
    json={"url": "https://example.com/a/very/long/path"},
    headers={"Authorization": "Bearer ilk_your_key_here"},  # optional
    timeout=10,
)
res.raise_for_status()
print(res.json()["shortUrl"])  # → https://ilync.dev/Ab3xK9

3. Use the response

A successful call returns 201 Created with the short link. Persist shortUrl (or shortCode) and hand it to your users — visiting it issues a 307 redirect to the original destination.

response — 201 Created
{
  "shortCode": "Ab3xK9",
  "shortUrl": "https://ilync.dev/Ab3xK9",
  "originalUrl": "https://example.com/a/very/long/path",
  "expiresAt": null,
  "createdAt": "2026-07-11T21:50:00.000Z",
  "isPrivate": false,
  "ownerId": null
}

That's the whole loop. From here, see Create a link for every request field, Authentication for keys, and Rate limits to size your usage.

Using the web UI

  1. Paste a long URL into the form on the home page.
  2. Optionally set a custom short code and/or pick an expiration.
  3. Signed in? Add an optional passcode — visitors must enter it before being redirected.
  4. Submit — the short link appears with Copy, a QR / barcode generator, and a link to stats.
  5. The Recently shortened list shows recent public links and their click counts; open a link's stats page at /{code}/stats.

Links created while signed out are public and appear in the landing-page feed. Links you create while signed inare unlisted — hidden from the public feed and tracked in your account's My links dashboard instead (they stay fully redirectable). Expired links are excluded from the public feed automatically.

To add or remove a passcode on an existing link, open it in your account's My links dashboard and use the Edit panel (see Passcodes).

Accounts & the dashboard

Accounts support Google and email/password sign-in. Visit /account to sign in or create one. Signing in is required to save links and to mint API keys. Email/password accounts receive a verification link on sign-up — click it to confirm your address (you can re-send it any time from the dashboard).

Dashboard areaWhat it does
ProfileAvatar (uploadable/removable), editable display name, sign-in method, and a copyable user id.
StatsTotal links, active links, and links expiring in the next 7 days.
Email verificationEmail/password accounts get a verification link on sign-up. The dashboard banner lets you re-send it or confirm once you've clicked it; your address shows a Verified badge.
Link in bioShortcut to manage your public /@username page — see Link in bio.
My linksEvery link you created while signed in — open, copy, view stats, or remove (removing only stops tracking; the short link keeps working). Paginated and filterable.
Developer APICreate, list, and revoke API keys. This is the easiest way to manage keys (see API keys below).
Custom domainsRegister up to 3 custom domains with DNS-based verification.
Danger zonePermanent account deletion — removes all links, API keys, access grants, and Firestore data.

Your saved links and profile are private to your account — only you can read or modify them.

Link in bio

Claim a public page at /@username and publish social profiles plus custom buttons — ideal for Instagram, TikTok, and other bios. Edit everything from /link (also linked from your account). Signing in is required to create or update a page.

Public URL

Visitors open /@username. Reserved names and usernames that clash with existing short codes cannot be claimed.

Editor

Set display name, tagline, avatar (uploadable), publish state, socials, and custom buttons on /link.

Social profiles

Instagram, X, TikTok, YouTube, LinkedIn, GitHub, Facebook, Threads, Pinterest, WhatsApp, Telegram, Stack Overflow, Slack, Medium, Discord, Reddit, Twitch, Spotify, email, website, and other — handles or full URLs are accepted.

Custom buttons

Stacked call-to-action links for shops, newsletters, portfolios, or anything else you want to highlight.

HTTP API

Signed-in clients can load and save the page with an ID token (same bearer auth used elsewhere for account actions).

GET/api/bioBearer required

Returns your page, or { "bio": null } if you have not created one yet.

get your link-in-bio page
curl https://ilync.dev/api/bio \
  -H 'Authorization: Bearer <id-token>'
response — 200 (no page yet)
{ "bio": null }
PUT/api/bioBearer required
FieldTypeRequiredNotes
usernamestringyes3–24 chars; letters, numbers, underscores; stored lowercase.
displayNamestringyes1–80 characters.
taglinestring | nullnoUp to 160 characters.
avatarUrlstring | nullnoChecked with the same destination safety rules as short links.
publishedbooleannoDefaults to true. Unpublished pages return 404 publicly.
linksarrayyesUp to 40 items. Each needs kind (social|custom), label, url; socials need platform.
example — save a link-in-bio page
curl -X PUT https://ilync.dev/api/bio \
  -H 'Authorization: Bearer <id-token>' \
  -H 'Content-Type: application/json' \
  -d '{
    "username": "you",
    "displayName": "Your name",
    "tagline": "Creator · Builder",
    "published": true,
    "links": [
      { "kind": "social", "platform": "instagram", "label": "Instagram", "url": "you" },
      { "kind": "custom", "label": "My shop", "url": "https://example.com/shop" }
    ]
  }'
GET/api/bio/analyticsBearer required

Returns visit analytics for your link-in-bio page, broken down by time period.

get link-in-bio analytics
curl https://ilync.dev/api/bio/analytics \
  -H 'Authorization: Bearer <id-token>'
response — 200
{
  "total": 1284,
  "periods": {
    "24h": 42,
    "3d": 118,
    "7d": 256,
    "30d": 890,
    "60d": 1203
  }
}

Core concepts

ConceptSummary
Short codeA short, unique code (e.g. Ab3xK9) generated for each link — collision-free and non-sequential.
Link in bioA signed-in user's public page at /@username, with socials and custom buttons. Managed at /link.
AccountA signed-in user. Required to save links to a dashboard, mint API keys, and manage a link-in-bio page.
Public linkAnyone with the code is redirected, and it may appear in the public feed. The default for anonymous shortening.
Unlisted linkRedirectable by anyone with the code, but hidden from the public feed. Links created while signed in are unlisted.
Private linkOnly the owner or granted users may follow it; everyone else gets 401. Requires an API key to create.
Passcode-protected linkAnyone may follow it, but visitors must first enter a passcode. The passcode is stored as a salted hash, never in plaintext, and requires an account to set. Applies to redirects in a browser; see Passcodes.
API keyAn opaque token (ilk_…) that authenticates requests, raises your rate limit, and owns the links it creates. Minted from your account and tied to it.
User idEvery API key maps to a numeric userId, used for ownership checks and access grants.
AdminA user whose email or user ID is listed in ADMIN_EMAILS / ADMIN_UIDS. Can manage all links via /admin.
Custom domainA user-registered domain for serving short links. Verified via DNS TXT record.

Create a short link

POST/api/urlsAuth optional
FieldTypeRequiredNotes
urlstringyesWith or without a scheme (defaults to https://). http/https only, must have a dotted domain. Unsafe or disallowed destinations are rejected.
expiresAtstring | numbernoISO 8601 string or epoch ms. Must be in the future.
customCodestringnoChoose your own short code (3–32 letters/numbers). Alias: shortCode. Requires a signed-in session or API key. Returns 401 if anonymous, 409 if taken. Omit to auto-generate.
privatebooleannotrue creates a private link. Requires an API key.
listedbooleannoDefaults to true. Set false to keep the link out of the public feed (the signed-in web form does this automatically). Does not affect redirects.
passcodestringnoOptional. Visitors must enter this passcode before being redirected. Requires a signed-in session or API key (returns 400 for anonymous callers). At least 4 characters. The passcode is stored only as a salted hash.
request — anonymous, public link
curl -X POST https://ilync.dev/api/urls \
  -H 'Content-Type: application/json' \
  -d '{"url":"https://example.com/a-very-long-url"}'
response — 201 Created
{
  "shortCode": "Ab3xK9",
  "shortUrl": "https://ilync.dev/Ab3xK9",
  "originalUrl": "https://example.com/a-very-long-url",
  "expiresAt": null,
  "createdAt": "2026-07-11T17:38:46.646Z",
  "isPrivate": false,
  "ownerId": null,
  "hasPasscode": false
}
request — custom short code (requires auth)
curl -X POST https://ilync.dev/api/urls \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_your_key_here' \
  -d '{"url":"https://example.com/launch","customCode":"launch"}'
request — authenticated, with expiry + private
curl -X POST https://ilync.dev/api/urls \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_your_key_here' \
  -d '{"url":"example.com/secret","expiresAt":"2026-12-31T23:59:59Z","private":true}'
request — passcode-protected (requires auth)
curl -X POST https://ilync.dev/api/urls \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_your_key_here' \
  -d '{"url":"https://example.com/members-only","passcode":"season-pass"}'
201created400invalid or blocked URL / bad JSON / expiry / code / passcode401auth required / invalid key409custom code taken429rate limited500server error

Follow a short link (redirect)

GET/{code}Private only

Returns 307 Temporary Redirect with a Locationheader pointing to the original URL. For private links, send the owner's or a granted user's key. Public redirects are rate-limited to reduce scraping and abuse — over-limit clients receive 429.

Links with a passcode return an HTML page asking for the passcode instead of an immediate redirect. Entering it sets a short-lived cookie and then redirects — see Passcodes.

curl -i https://ilync.dev/Ab3xK9 \
  -H 'Authorization: Bearer ilk_your_key_here'
307redirect404unknown code410expired401private, unauthorized429too many redirects

Delete a short link

DELETE/api/urls/{code}Owner
curl -X DELETE https://ilync.dev/api/urls/Ab3xK9 \
  -H 'Authorization: Bearer ilk_your_key_here'
204deleted401missing/invalid key403not the owner404unknown code

Grant access to a private link

POST/api/urls/{code}/accessOwner

Grants another user (by their userId) permission to follow a private link.

curl -X POST https://ilync.dev/api/urls/Ab3xK9/access \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_owner_key_here' \
  -d '{"userId":2}'
200granted400bad userId401missing/invalid key403not the owner404unknown code

Link analytics

GET/api/analytics/{code}Auth none

Returns click totals and breakdowns. Recording happens off the redirect hot path, so it never slows a redirect. A human-friendly page also lives at /{code}/stats.

curl https://ilync.dev/api/analytics/Ab3xK9
response — 200
{
  "shortCode": "Ab3xK9",
  "originalUrl": "https://example.com/…",
  "clicks": 4,
  "byPlatform": [
    { "platform": "iOS", "count": 1 },
    { "platform": "Windows", "count": 1 }
  ],
  "recent": [
    { "createdAt": "…", "referrer": "https://twitter.com/", "platform": "iOS" }
  ]
}
200ok404unknown code

QR codes & barcodes

Generate a QR code or 1D barcode from any value — a short link, a product SKU, a serial number. Auth is optional. CORS is open, so you can call this from a browser app or drop the GET URL into an <img>. A visual playground lives at /code.

GET/api/codesAuth optional
POST/api/codesAuth optional
FieldTypeRequiredNotes
valuestringyesText to encode. Max 2048 characters. Query param on GET, JSON field on POST.
formatstringnoQR (default), CODE128, CODE39, EAN13, UPC-A, or ITF14. Aliases like qrcode, c128, upca are accepted.
outputstringnopng (default, image/png), svg (image/svg+xml), or json (base64 PNG plus metadata).
sizeintegerno64–2048. Defaults to 256 on GET (for embeds) and 512 on POST.
foregroundstringnoHex color for modules/bars. Alias: fg. Default #18181b.
backgroundstringnoHex color for the quiet zone. Alias: bg. Default #ffffff.
errorCorrectionstringnoQR only: L, M (default), Q, or H. Alias: ec. Ignored for 1D barcodes.
downloadbooleannoIf true (or 1), sets Content-Disposition: attachment so the browser downloads the file.
logostringnoQR only. An image URL or a data:image base64 URI to place in the center of the code. 2 MB limit, rounded and padded to match the web UI. Best sent as JSON on POST (data URIs can get mangled in a GET query string).
FormatEncodes
QRAny text or URL
CODE128Nearly any ASCII text — URLs, SKUs, serials
CODE39Uppercase letters, digits, and - . $ / + % space
EAN1312–13 digits (retail)
UPC-A11–12 digits (North American retail)
ITF14Exactly 14 digits (shipping cartons)

Embed as an image

GET returns the image bytes. Use it as an src — no SDK, no key required for modest volume. Anonymous GET responses are cacheable.

HTML embed
<img
  src="https://ilync.dev/api/codes?value=https%3A%2F%2Filync.dev%2FAb3xK9&format=QR&size=256"
  alt="QR code for https://ilync.dev/Ab3xK9"
  width="256"
  height="256"
/>
request — GET PNG
curl "https://ilync.dev/api/codes?value=https%3A%2F%2Filync.dev%2FAb3xK9&format=QR&size=512" \
  -o qr.png
request — GET SVG, branded colors
curl "https://ilync.dev/api/codes?value=HELLO-42&format=CODE128&output=svg&fg=%2318181b&bg=%23ffffff" \
  -o barcode.svg

POST from an app

POST the same fields as JSON. Send an API key for the higher quota. output=json is the easiest path when you cannot handle raw image bytes.

request — POST PNG
curl -X POST https://ilync.dev/api/codes \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_your_key_here' \
  -d '{"value":"https://ilync.dev/Ab3xK9","format":"QR","size":512}' \
  -o qr.png
request — POST JSON (base64 PNG)
curl -X POST https://ilync.dev/api/codes \
  -H 'Content-Type: application/json' \
  -d '{"value":"5901234123457","format":"EAN13","output":"json"}'
response — 200 (output=json)
{
  "format": "EAN13",
  "value": "5901234123457",
  "output": "png",
  "mimeType": "image/png",
  "size": 512,
  "data": "iVBORw0KGgoAAAANSUhEUgAA..."
}
JavaScript — fetch a PNG blob
const res = await fetch("https://ilync.dev/api/codes", {
  method: "POST",
  headers: {
    "Content-Type": "application/json",
    "X-API-Key": "ilk_your_key_here",
  },
  body: JSON.stringify({ value: "https://ilync.dev/Ab3xK9", format: "QR" }),
});
const blob = await res.blob();
const url = URL.createObjectURL(blob);

Add a logo

Pass logo to sit a brand mark in the center of a QR code. It accepts a public image URL or a data:image/...;base64, URI, up to 2 MB. The result is rounded and given a quiet-zone backing, matching the /code playground. Use errorCorrection=H so the code still scans. Because + and / in a data URI can be mangled in a query string, send data URIs as JSON on POST.

request — POST QR with a logo (URL)
curl -X POST https://ilync.dev/api/codes \
  -H 'Content-Type: application/json' \
  -d '{"value":"https://ilync.dev/Ab3xK9","format":"QR","size":512,"output":"png","errorCorrection":"H","logo":"https://cdn.example.com/logo.png"}' \
  -o qr-with-logo.png
request — POST QR with a logo (data URI, PNG)
curl -X POST https://ilync.dev/api/codes \
  -H 'Content-Type: application/json' \
  -d '{"value":"https://ilync.dev/Ab3xK9","format":"QR","errorCorrection":"H","logo":"data:image/png;base64,iVBORw0KGgo..."}' \
  -o qr-with-logo.png
200image or JSON204OPTIONS preflight400missing/invalid value, format, or options401invalid key or session429rate limited

Mint an API key

POST/api/keysAccount

You must have an account. Create a key from the account dashboard (easiest), or call the endpoint directly with your account ID token as a bearer token. The plaintext key is returned once (only its hash is stored) plus your userId.

curl -X POST https://ilync.dev/api/keys \
  -H 'Authorization: Bearer <YOUR_ID_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"label":"my-app"}'
response — 201
{ "apiKey": "ilk_6I2kaePfLIiEUUvVHHYMaBDzPre4npf4", "label": "my-app", "userId": 1 }

List your keys with GET /api/keys:

list your API keys
curl https://ilync.dev/api/keys \
  -H 'Authorization: Bearer <YOUR_ID_TOKEN>'
response — 200
{
  "keys": [
    { "id": 1, "label": "my-app", "prefix": "ilk_6I2k", "createdAt": "2026-07-11T21:50:00.000Z" }
  ]
}

And revoke one with DELETE /api/keys/{id} (same bearer token):

revoke an API key
curl -X DELETE https://ilync.dev/api/keys/1 \
  -H 'Authorization: Bearer <YOUR_ID_TOKEN>'

Clean up expired links

POST/api/cleanupSecret / cron

Actively deletes every expired link (expired links are also removed lazily when someone tries to follow them, and are hidden from the public feed immediately). Pass CLEANUP_SECRETas a bearer token when set; when unset, it's reachable only outside production. Trigger it on a schedule.

curl -X POST https://ilync.dev/api/cleanup \
  -H 'Authorization: Bearer <CLEANUP_SECRET>'

Sign in (programmatic)

POST/api/auth/sign-inNone

Exchanges email + password for an ID token. Intended for headless / programmatic access — scripts, CLI, server-to-server. The web UI uses the client SDK directly.

request
curl -X POST https://ilync.dev/api/auth/sign-in \
  -H 'Content-Type: application/json' \
  -d '{"email":"user@example.com","password":"your-password"}'
response — 200
{ "idToken": "eyJ…", "expiresIn": "3600" }

Use the returned idToken as Authorization: Bearer <idToken> on any endpoint that accepts authentication (e.g. POST /api/keys, PUT /api/bio). The token expires after expiresIn seconds (typically 3600).

200token issued400bad/missing fields or invalid email401incorrect credentials429too many attempts

Avatar management

Upload, serve, and remove profile photos. Avatars are stored in Vercel Blob under avatars/{uid}/ and served through a proxy endpoint with caching.

POST/api/upload/avatarAccount

Upload a profile photo as multipart/form-data with a file field. Max 4 MB; allowed types: JPEG, PNG, WebP, GIF.

request
curl -X POST https://ilync.dev/api/upload/avatar \
  -H 'Authorization: Bearer <ID_TOKEN>' \
  -F 'file=@photo.jpg'
response — 200
{
  "url": "/api/avatar?pathname=avatars%2Fabc123%2F1700000000.jpg",
  "blobUrl": "https://..."
}
GET/api/avatar?pathname=avatars/{uid}/{file}None

Serves an avatar image with the correct Content-Type header and a 24-hour cache directive. No authentication required.

fetch an avatar
curl "https://ilync.dev/api/avatar?pathname=avatars%2Fabc123%2F1700000000.jpg" \
  -o avatar.jpg
DELETE/api/avatar?pathname=avatars/{uid}/{file}Account

Removes an avatar from blob storage. The pathname must belong to the requesting user.

request
curl -X DELETE "https://ilync.dev/api/avatar?pathname=avatars%2Fabc123%2F1700000000.jpg" \
  -H 'Authorization: Bearer <ID_TOKEN>'
200success400invalid pathname401not signed in403pathname does not belong to you404blob not found

Custom domains

Register custom domains for serving short links. Each account can register up to 3 domains. Verification is done via a DNS TXT record.

GET/api/domainsAccount

List your registered custom domains.

curl https://ilync.dev/api/domains -H 'Authorization: Bearer <ID_TOKEN>'
response — 200
{
  "domains": [
    {
      "id": 1,
      "domain": "go.example.com",
      "verified": false,
      "verificationToken": "abc123...",
      "createdAt": "2026-07-20T10:00:00.000Z"
    }
  ]
}
POST/api/domainsAccount

Register a new custom domain.

request
curl -X POST https://ilync.dev/api/domains \
  -H 'Authorization: Bearer <ID_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"domain":"go.example.com"}'
response — 201
{
  "domain": {
    "id": 1,
    "domain": "go.example.com",
    "verified": false,
    "verificationToken": "abc123...",
    "instructions": "Add a TXT record: _ilync.go.example.com with value \"abc123...\""
  }
}
200listed201created400invalid domain or limit reached401not signed in409domain already verified by another account

Account deletion

Permanently delete your account and all associated data. This action is irreversible and removes all links, API keys, access grants, and Firestore data.

DELETE/api/accountAccount

The confirm field must be exactly "DELETE_MY_ACCOUNT". The web UI additionally requires typing the word DELETE in a prompt before calling this endpoint.

request
curl -X DELETE https://ilync.dev/api/account \
  -H 'Authorization: Bearer <ID_TOKEN>' \
  -H 'Content-Type: application/json' \
  -d '{"confirm":"DELETE_MY_ACCOUNT"}'
response — 200
{ "deleted": true }
200deleted400missing/incorrect confirmation401not signed in

Authentication & API keys

  • Sign in first. Minting keys requires an account — create one from the account dashboard, then generate keys there. For programmatic sign-in, use POST /api/auth/sign-in.
  • Send a key using either Authorization: Bearer ilk_… or X-API-Key: ilk_…:
    via Authorization header
    curl -X POST https://ilync.dev/api/urls \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer ilk_your_key_here' \
      -d '{"url":"https://example.com/long"}'
    same request via X-API-Key header
    curl -X POST https://ilync.dev/api/urls \
      -H 'Content-Type: application/json' \
      -H 'X-API-Key: ilk_your_key_here' \
      -d '{"url":"https://example.com/long"}'
  • A valid key raises your rate limit, marks you as the owner of links you create, and enables private links.
  • Keys are stored only as SHA-256 hashes — the plaintext is shown once at creation, so save it.

Rate limits

Applied to POST /api/urls. Every response carries X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset; a 429 also includes Retry-After.

CallerLimitWindow
Anonymous (per IP)10 requests60 seconds
Authenticated (per key)100 requests60 seconds

GET and POST /api/codes use a separate quota so generating barcodes does not eat your shortening budget:

CallerLimitWindow
Anonymous (per IP)30 requests60 seconds
Authenticated (per key)120 requests60 seconds

Public redirects (/{code}) are also capped to limit scrapers and redirect abuse: 120 / IP, 60 / IP+code, and 300 / code per 60 seconds. Over-limit redirects return 429.

Link safety

When a short link is created, ilync checks the destination before it is saved. Failed checks return 400 with a clear error — nothing extra is required on the request.

Public http(s) only

Destinations need a real domain. Other schemes are rejected.

No embedded credentials

URLs like user:pass@host are blocked — a common phishing trick.

No internal networks

Private, local, and reserved destinations never get shortened.

Unsafe destinations refused

Known-dangerous or disallowed URLs return 400 with a clear message.

Operator denylist

Extra hostnames can be blocked site-wide without code changes.

Automatic on create

No extra request fields — every POST /api/urls is checked.

Phishing detection

Every destination is scored for phishing signals before the link is saved.

Screened for life

Saved links are re-checked over time — destinations that turn hostile later get quarantined or disabled automatically.

example — blocked destination
{ "error": "That destination is not allowed (private or reserved host)." }

Safety doesn't stop at creation

Destinations are re-evaluated over a link's lifetime. If a site turns hostile after the link was saved, it stops redirecting silently: visitors see a warning interstitial describing the risk before choosing to continue, and severely dangerous destinations are disabled outright.

Score any URL without shortening it

The same pipeline backs a public endpoint — no account or key required. Send a URL to POST /api/safety-check (rate-limited to 10 requests per minute per IP) and receive a 0–100 score, a safe / suspicious / dangerous verdict, and every signal that ran:

check a URL
curl -s https://ilync.dev/api/safety-check \
  -H "Content-Type: application/json" \
  -d '{"url":"https://example.com"}'
response
{
  "url": "https://example.com/",
  "host": "example.com",
  "score": 96,
  "rating": "safe",
  "signals": [
    { "id": "dns", "status": "pass", "detail": "Resolved to a public address." },
    { "id": "ml-phishing", "status": "pass", "detail": "No phishing signals detected." }
  ]
}

Private URLs & access control

  1. Owner signs in and mints a key → gets apiKey and userId (say 1).
  2. Owner creates a link with "private": true using their key.
  3. Following the link works only with the owner's key; anyone else gets 401.
  4. The other user signs in and mints their own key to obtain their userId (say 2).
  5. Owner grants access: POST /api/urls/{code}/access with { "userId": 2 }.
  6. User 2 can now follow the link with their key.

Because access is enforced via an API-key header, private links are API-oriented — not a click-through-in-a-browser flow unless the client sends the key.

put it together — create private, then grant access
# 1. Owner creates a private link with their key
curl -X POST https://ilync.dev/api/urls \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_owner_key' \
  -d '{"url":"https://example.com/secret","private":true}'
# → { "shortCode": "Ab3xK9", "isPrivate": true, ... }

# 2. Owner grants user 2 access
curl -X POST https://ilync.dev/api/urls/Ab3xK9/access \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer ilk_owner_key' \
  -d '{"userId":2}'

# 3. User 2 follows the link with their own key (owner can too)
curl -i https://ilync.dev/Ab3xK9 \
  -H 'Authorization: Bearer ilk_user2_key'
# → 307 redirect to https://example.com/secret

# 4. Anonymous / non-granted users are refused
curl -i https://ilync.dev/Ab3xK9   # → 401

Passcode-protected links

A passcode is a shared secret that visitors must enter before a short link redirects. It is different from private links: anyone who knows the code can reach the passcode page, but only people with the passcode get redirected.

How it works

  • Only signed-in users (or API-key holders) can protect a link. Anonymous callers cannot set a passcode.
  • The passcode is never stored or returned in plaintext — only a salted hash is kept. The API reports a boolean hasPasscode instead.
  • Passcode-protected links are hidden from the public feed and are rate-limited so passcodes can't be brute-forced.

Visitor flow

  1. Opening /{code} shows a passcode prompt.
  2. A correct passcode sets a signed, short-lived (30 min), HttpOnly cookie and redirects to the destination.
  3. Incorrect entries return an error and are rate-limited per IP.

Setting or clearing a passcode

Set one when creating a link via POST /api/urls with a passcode field (requires auth), or from the Edit panel in your account dashboard.

PATCH/api/urls/{code}/passcodeOwner

Change or remove the passcode on an existing link you own. A non-empty passcode sets a new one; { "passcode": "" } or { "passcode": null } clears it.

request — set a passcode
curl -X PATCH https://ilync.dev/api/urls/Ab3xK9/passcode \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <firebase-id-token>' \
  -d '{"passcode":"season-pass"}'
request — clear the passcode
curl -X PATCH https://ilync.dev/api/urls/Ab3xK9/passcode \
  -H 'Content-Type: application/json' \
  -H 'Authorization: Bearer <firebase-id-token>' \
  -d '{"passcode":""}'
200updated400invalid JSON / passcode too short (min 4)401signed-in session required403not the owner404unknown code

Expiration

  • Set expiresAt on creation as an ISO 8601 string or epoch milliseconds. It must be in the future.
    create an expiring link
    curl -X POST https://ilync.dev/api/urls \
      -H 'Content-Type: application/json' \
      -H 'Authorization: Bearer ilk_your_key_here' \
      -d '{"url":"https://example.com/promo","expiresAt":"2026-12-31T23:59:59Z"}'
  • Expired links return 410 Gone and are removed on access (passive cleanup).
    following an expired link
    curl -i https://ilync.dev/Ab3xK9   # → 410 Gone
  • Run POST /api/cleanup on a schedule for active cleanup.
    trigger active cleanup
    curl -X POST https://ilync.dev/api/cleanup \
      -H 'Authorization: Bearer <CLEANUP_SECRET>'

Privacy & cookies

ilync uses cookies and similar technologies for analytics, preferences, and authentication. You can review the full breakdown in the privacy policy. First-time visitors are shown a consent banner that lets them accept or decline analytics cookies.

Errors

Errors use standard HTTP status codes and return a JSON body with a human-readable error message. Always check the status code before parsing a success response.

error response — 400 Bad Request
{ "error": "Invalid URL: must be a valid http(s) address." }
StatusMeaning
400Malformed or blocked request — bad URL, unsafe destination, JSON, or expiry.
401Missing or invalid credentials.
403Authenticated, but not allowed (e.g. not the owner).
404Unknown short code.
410The link has expired.
429Rate limit exceeded on create or redirect — retry after the window resets.