API documentation

Create your links, sort them into categories and read your statistics from your own tools.

Getting started

The base address of the API is:

https://qlynk.eu/api/v1

Three steps are enough:

  1. Sign in and open API keys in your account, then create a key. It is shown only once, so copy it right away.
  2. Send the key in the Authorization header of every request.
  3. Requests with a body (create, update) send JSON, with the Content-Type: application/json header.
curl https://qlynk.eu/api/v1/urls \
  -H "Authorization: Bearer VOTRE_CLE_API"

Authentication

The API uses a personal key sent as a Bearer token. Keep it secret: it gives access to all your links. You can revoke it at any time from your account, and the number of active keys per account is limited.

Authorization: Bearer VOTRE_CLE_API

With no key, a malformed or revoked key, or a key from a disabled account, the API replies 401.

Quotas

The number of requests per day depends on your plan. The counter resets at midnight (Paris time).

PlanRequests per day
Free50
Pro10,000

Every response shows where you stand:

  • X-RateLimit-Limit: your daily quota
  • X-RateLimit-Remaining: requests left today
  • X-RateLimit-Reset: when the quota renews (Unix timestamp)

When the quota is reached, the API replies 429 with the Retry-After header (in seconds). Rejected requests do not count. Plan limits also apply to the API: for example, creating links follows the Free plan cap (active links at one time), and choosing the end of the link (custom_code) and setting an expiry date are reserved for the Pro plan (reply 403).

Response format

All responses are JSON. On success:

{
  "success": true,
  "data": { ... }
}

On error:

{
  "success": false,
  "error": "Link not found"
}

Error codes

CodeMeaning
200, 201Success (201 for a creation)
400Invalid request: malformed JSON, or a missing or incorrect field (address not allowed, link ending already taken, unknown period…). The message says what to fix.
401Key missing, invalid or revoked, or account disabled
403The link or category does not belong to you, or the feature is not in your plan (Free plan link cap, custom link ending, expiry date)
404Link or category not found
405Method not supported on this address
429Daily quota reached (see Retry-After)
500Error on our side: try again, then write to us
503API temporarily disabled

Links

Create a link

POST/urls
FieldDescription
url (required)Destination address (http or https)
custom_codeLink ending you want (Pro plan; on Free, reply 403). Otherwise, Qlynk generates one.
title, descriptionFree text (255 and 1,000 characters at most)
is_indexedBoolean, 1 by default
category_idCategory of the link (category or subcategory)
expires_atExpiry date in YYYY-MM-DD format, in the future (Pro plan). null for none.
curl -X POST https://qlynk.eu/api/v1/urls \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://www.exemple.com/evenements/salon-2026",
    "custom_code": "salon-26",
    "title": "Salon des entrepreneurs",
    "category_id": 12
  }'
{
  "success": true,
  "data": {
    "short_code": "salon-26",
    "short_url": "https://qlynk.eu/salon-26",
    "original_url": "https://www.exemple.com/evenements/salon-2026",
    "title": "Salon des entrepreneurs",
    "description": "",
    "is_indexed": 1,
    "category_id": 12,
    "status": "active",
    "paused_at": null,
    "expires_at": null,
    "app": null,
    "created_at": "2026-10-17 14:32:08"
  }
}

The app field gives the name of the app the link opens on a phone (for example “YouTube”), or null.

The status field is active or paused: a paused link (Free plan beyond the limit) no longer redirects and does not count toward active links.

List your links

GET/urls
curl https://qlynk.eu/api/v1/urls \
  -H "Authorization: Bearer VOTRE_CLE_API"

The response contains total and the urls array, from newest to oldest.

To filter by category, add ?category_id=12: you get the links of the category and of its subcategories. Add &include_children=0 for the category alone, or use ?category_id=none for uncategorized links.

curl "https://qlynk.eu/api/v1/urls?category_id=3" \
  -H "Authorization: Bearer VOTRE_CLE_API"

Read a link

GET/urls/{code}
curl https://qlynk.eu/api/v1/urls/salon-26 \
  -H "Authorization: Bearer VOTRE_CLE_API"

Update a link

PUT/urls/{code}

Send only the fields to change (the same as for creation). The others stay the same. Changing custom_code changes the short address (Pro plan only).

curl -X PUT https://qlynk.eu/api/v1/urls/salon-26 \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{"title": "Salon 2026, programme", "expires_at": "2026-12-31"}'

Delete a link

DELETE/urls/{code}

Deletion is permanent: the short link stops working and its statistics are erased.

curl -X DELETE https://qlynk.eu/api/v1/urls/salon-26 \
  -H "Authorization: Bearer VOTRE_CLE_API"

Categories

A category can have a parent category through the parent_id field. There is only one level of subcategories: the parent must be a top-level category that belongs to you. parent_id is null for a top-level category.

List your categories

GET/categories
curl https://qlynk.eu/api/v1/categories \
  -H "Authorization: Bearer VOTRE_CLE_API"
{
  "success": true,
  "data": {
    "total": 2,
    "categories": [
      { "id": 3, "parent_id": null, "name": "Paperasse Zéro", "color": "#E4572E",
        "icon": "folder", "position": 0, "link_count": 1,
        "created_at": "2026-09-02 10:15:00", "updated_at": "2026-09-02 10:15:00" },
      { "id": 4, "parent_id": 3, "name": "Indy", "color": "#2E86AB",
        "icon": "folder", "position": 0, "link_count": 4,
        "created_at": "2026-09-02 10:16:00", "updated_at": "2026-09-02 10:16:00" }
    ]
  }
}

Read, create, update, delete

GET/categories/{id}
POST/categories
PUT/categories/{id}
DELETE/categories/{id}
FieldDescription
name (required on creation)Name of the category
parent_idID of the parent category, or null (top level). Left out on update: unchanged.
colorColor in #RRGGBB format
iconIcon name (lowercase letters, digits, hyphens)
positionDisplay order
curl -X POST https://qlynk.eu/api/v1/categories \
  -H "Authorization: Bearer VOTRE_CLE_API" \
  -H "Content-Type: application/json" \
  -d '{"name": "Tiime", "parent_id": 3, "color": "#2E86AB"}'

A 400 error explains the cases that are refused: parent not found, a category that would be its own parent, a subcategory that would contain another subcategory, a category that already has subcategories and that you want to move under another one.

When a category is deleted, its links are kept (they become “Uncategorized”) and its subcategories become top-level categories.

Stats

GET/stats/{code}

Optional period parameter:

  • 1, 7, 30, 365: the matching last days
  • all: the whole history (default value)
  • the older values day, week, month and year are still accepted

On the Free plan, history is limited to 30 days. If you ask for a longer period, the API brings it down to that limit and the history_limited field is true. The Pro plan has no limit.

curl "https://qlynk.eu/api/v1/stats/salon-26?period=30" \
  -H "Authorization: Bearer VOTRE_CLE_API"
{
  "success": true,
  "data": {
    "short_code": "salon-26",
    "period": "30",
    "period_days": 30,
    "history_limited": false,
    "timezone": "Europe/Paris",
    "total_clicks": 412,
    "unique_visitors": 351,
    "app_opens": 96,
    "qr_scans": 111,
    "filtered_hits": 38,
    "last_click": "2026-10-17T14:02:11+02:00",
    "clicks_by_date": [ { "date": "2026-10-17", "clicks": 52 } ],
    "devices": { "mobile": 280, "tablet": 14, "desktop": 118 },
    "top_countries": [ { "country": "France", "code": "FR", "clicks": 375 } ],
    "top_browsers": [ { "browser": "Safari", "clicks": 190 } ],
    "top_os": [ { "os": "iOS", "clicks": 201 } ],
    "top_referrers": [ { "domain": "linkedin.com", "clicks": 64 } ]
  }
}
FieldMeaning
period_daysNumber of days actually covered (null for the whole history)
history_limitedtrue if the requested period was shortened by your plan limit
total_clicks, unique_visitorsCounted clicks and unique visitors (without bots or duplicates)
app_opensClicks that opened an app directly
qr_scansVisits that came from a QR code
filtered_hitsVisits left out of the count (bots, messaging previews, duplicates…)
last_clickLast click, in ISO 8601 format with the Paris offset (timezone)
clicks_by_dateClicks per day (Paris time), from newest to oldest, days without clicks left out

IP addresses are never provided. See the privacy policy.