Getting started
The base address of the API is:
https://qlynk.eu/api/v1 Three steps are enough:
- Sign in and open API keys in your account, then create a key. It is shown only once, so copy it right away.
- Send the key in the
Authorizationheader of every request. - Requests with a body (create, update) send JSON, with the
Content-Type: application/jsonheader.
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).
| Plan | Requests per day |
|---|---|
| Free | 50 |
| Pro | 10,000 |
Every response shows where you stand:
X-RateLimit-Limit: your daily quotaX-RateLimit-Remaining: requests left todayX-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
| Code | Meaning |
|---|---|
| 200, 201 | Success (201 for a creation) |
| 400 | Invalid request: malformed JSON, or a missing or incorrect field (address not allowed, link ending already taken, unknown period…). The message says what to fix. |
| 401 | Key missing, invalid or revoked, or account disabled |
| 403 | The 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) |
| 404 | Link or category not found |
| 405 | Method not supported on this address |
| 429 | Daily quota reached (see Retry-After) |
| 500 | Error on our side: try again, then write to us |
| 503 | API temporarily disabled |
Links
Create a link
/urls| Field | Description |
|---|---|
url (required) | Destination address (http or https) |
custom_code | Link ending you want (Pro plan; on Free, reply 403). Otherwise, Qlynk generates one. |
title, description | Free text (255 and 1,000 characters at most) |
is_indexed | Boolean, 1 by default |
category_id | Category of the link (category or subcategory) |
expires_at | Expiry 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
/urlscurl 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
/urls/{code}curl https://qlynk.eu/api/v1/urls/salon-26 \
-H "Authorization: Bearer VOTRE_CLE_API"
Update a link
/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
/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
/categoriescurl 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
/categories/{id}/categories/categories/{id}/categories/{id}| Field | Description |
|---|---|
name (required on creation) | Name of the category |
parent_id | ID of the parent category, or null (top level). Left out on update: unchanged. |
color | Color in #RRGGBB format |
icon | Icon name (lowercase letters, digits, hyphens) |
position | Display 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
/stats/{code}Optional period parameter:
1,7,30,365: the matching last daysall: the whole history (default value)- the older values
day,week,monthandyearare 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 } ]
}
} | Field | Meaning |
|---|---|
period_days | Number of days actually covered (null for the whole history) |
history_limited | true if the requested period was shortened by your plan limit |
total_clicks, unique_visitors | Counted clicks and unique visitors (without bots or duplicates) |
app_opens | Clicks that opened an app directly |
qr_scans | Visits that came from a QR code |
filtered_hits | Visits left out of the count (bots, messaging previews, duplicates…) |
last_click | Last click, in ISO 8601 format with the Paris offset (timezone) |
clicks_by_date | Clicks per day (Paris time), from newest to oldest, days without clicks left out |
IP addresses are never provided. See the privacy policy.