Kokli URL shortener API
Use the Kokli REST API to create and edit short links and fetch click analytics from your own app, website, Telegram bot or CRM. Responses are JSON and authentication uses an API key.
- REST & JSON
Standard JSON requests and responses over HTTPS; works with any programming language.
- Configurable access
Enable only the permissions each key needs (e.g. read-only).
- Key security
IP allowlists, expiry dates and instant revocation from the dashboard.
- Analytics & QR
Daily clicks, countries, devices and a QR image for every link via the API.
Quick start
- 1Get a plan with API access
API access is included in the plans marked “API access” on the pricing page.
- 2Create an API key
Go to “API keys” in your dashboard and choose the key's name and permissions. The key is shown only once.
- 3Send your first request
Send the key in the Authorization: Bearer header and create your first short link with POST /links.
Create a short link in the language of your choice — base URL: https://kok.li/api/v1
curl -X POST https://kok.li/api/v1/links \
-H "Authorization: Bearer kk_live_YOUR_KEY" \
-H "Content-Type: application/json" \
-d '{"longUrl": "https://example.com/very/long/url"}'Authentication
All requests (except the introduction and the OpenAPI file) require an API key. Send the key in the Authorization: Bearer kk_live_… or X-API-Key header. Keep the key on the server side only, and never put it in browser code or a mobile app.
- You can set an expiry date and a list of allowed IPs (single IPs or CIDR ranges) for each key.
- Revoked or expired keys are rejected immediately with a 401 error.
- The site admin may disable a key's or an account's access in case of abuse.
Permissions (scopes)
When creating a key, you choose which areas it can access. Follow the principle of least privilege; for example, a reporting dashboard key only needs links:read and analytics:read.
| Scope | Purpose | Endpoints |
|---|---|---|
links:read | Read links | GET /links, GET /links/{code} |
links:write | Create & edit links | POST /links, POST /links/bulk, PATCH /links/{code} |
links:delete | Delete links | DELETE /links/{code} |
analytics:read | View click analytics | GET /links/{code}/stats, GET /analytics |
qr:read | Get QR codes | GET /links/{code}/qr |
account:read | Account info & usage | GET /me |
API endpoints
All paths are relative to https://kok.li/api/v1. Responses are JSON and timestamps are ISO 8601 (UTC).
/api/v1/linksCreate a short link
Creates a new short link. The destination is security-checked before creation and counts towards your plan's monthly quota.
Required scope: links:write
longUrlrequired | string | Destination URL (http or https) |
alias | string | Custom alias, 6–32 characters (English letters, numbers, - and _) |
utmSource / utmMedium / utmCampaign / utmTerm / utmContent | string | Campaign tracking parameters appended to the destination |
expireIn | string | Validity: 1d, 3d, 7d, 14d, 30d, 90d or never |
expiresAt | string | Expiry date in ISO 8601 format |
password | string | Password required to open the link |
{
"longUrl": "https://example.com/products/summer-sale",
"alias": "summer-sale",
"utmSource": "telegram",
"expireIn": "30d"
}{
"code": "summer-sale",
"shortUrl": "https://kok.li/summer-sale",
"longUrl": "https://example.com/products/summer-sale?utm_source=telegram",
"clicks": 0,
"scanStatus": "clean",
"expiresAt": "2026-10-25T08:30:00.000Z",
"createdAt": "2026-09-25T08:30:00.000Z"
}/api/v1/links/bulkBulk create links
Creates up to 50 links in one request. Each item takes the same fields as “Create a short link” and gets its own result; one failed item doesn't stop the others.
Required scope: links:write
linksrequired | array | Array of 1–50 objects with the create-link fields |
{
"links": [
{ "longUrl": "https://example.com/a" },
{ "longUrl": "https://example.com/b", "alias": "promo-b" }
]
}{
"created": 1,
"failed": 1,
"results": [
{ "index": 0, "ok": true, "link": { "code": "x7Kp2q", "shortUrl": "https://kok.li/x7Kp2q" } },
{ "index": 1, "ok": false, "status": 409, "code": "alias_taken", "error": "This alias is already taken." }
]
}/api/v1/linksList links
Returns your account's links, newest first, with pagination.
Required scope: links:read
page | integer | Page number (default 1) |
limit | integer | Items per page, max 100 (default 50) |
q | string | Search destination URLs, codes and tags |
status | string | active | paused | archived | blocked |
tag | string | Only links with this tag |
folder | string | Only links in this folder |
{
"links": [
{
"id": "66f3…",
"code": "summer-sale",
"shortUrl": "https://kok.li/summer-sale",
"longUrl": "https://example.com/products/summer-sale",
"clicks": 128,
"status": "active",
"tags": ["campaign"],
"passwordProtected": false,
"utm": { "source": "telegram", "medium": null, "campaign": null, "term": null, "content": null },
"expiresAt": null,
"createdAt": "2026-09-25T08:30:00.000Z"
}
],
"pagination": { "page": 1, "limit": 50, "total": 1, "totalPages": 1 }
}/api/v1/links/{code}Get a link
Returns full details of a link (destination, status, clicks, tags, expiry…).
Required scope: links:read
/api/v1/links/{code}Update a link
Only the fields you send are changed. PUT is accepted with the same behaviour. Send password as null to remove it.
Required scope: links:write
longUrl | string | New destination URL (security-checked again) |
tags | string[] | Tags (max 20) |
folder | string | null | Folder |
maxClicks | integer | null | Click cap; the link is paused afterwards |
fallbackUrl | string | null | Fallback URL after expiry or pause |
password | string | null | New password, or null to remove it |
expireIn / expiresAt | string | Change the expiry |
deviceTargets | object | Separate destinations for ios, android and desktop |
{
"tags": ["campaign", "autumn"],
"maxClicks": 1000
}/api/v1/links/{code}Delete a link
Permanently deletes the link and all of its click data.
Required scope: links:delete
{ "ok": true }/api/v1/links/{code}/statsLink click analytics
Daily clicks and unique visitors, range totals and a comparison with the previous range. On plans with advanced analytics, the breakdown also includes countries, devices, browsers, operating systems and referrers.
Required scope: analytics:read
range | integer | 7 | 10 | 14 | 30 | 90 | 180 | 365 days (default 10) |
from / to | YYYY-MM-DD | Custom range in Iran time (instead of range) |
{
"link": { "code": "summer-sale", "shortUrl": "https://kok.li/summer-sale", "clicks": 128 },
"range": 10,
"from": "2026-09-16",
"to": "2026-09-25",
"daily": [{ "date": "2026-09-25", "label": "Sep 25", "clicks": 14, "uniques": 11 }],
"totals": { "clicks": 128, "uniques": 97 },
"totalsPrev": { "clicks": 90, "uniques": 70 },
"breakdown": null
}/api/v1/analyticsAccount analytics
Same output as link analytics, but for all links in your account combined. Accepts range or from/to.
Required scope: analytics:read
/api/v1/links/{code}/qrQR code image
Returns a PNG or SVG QR code for the short link (the response is an image, not JSON).
Required scope: qr:read
format | string | png (default) or svg |
size | integer | Image width, 64–1024 px (default 320) |
color / bgColor | hex | Foreground and background colours, e.g. 000000 and ffffff |
margin | integer | Margin 0–10 |
ecc | string | Error correction level: L, M, Q or H |
/api/v1/meAccount, key & usage
Returns your account and plan, the current key's permissions and rate limit, and this month's usage.
Required scope: account:read
{
"user": { "id": "66f1…", "name": "Alex" },
"plan": { "tier": "pro", "label": "Pro" },
"key": { "prefix": "kk_live_3fa9c1", "scopes": ["links:read", "links:write"], "rateLimitPerMinute": 60 },
"limits": { "maxKeys": 10, "maxLinks": -1, "monthlyLinkLimit": 1000 },
"usage": { "period": "2026-09", "monthlyLinksCreated": 42, "totalLinks": 318 }
}Rate limits & quotas
- Each key has a requests-per-minute limit (60 by default). The remaining allowance is returned in the
RateLimit-Limit,RateLimit-RemainingandRateLimit-Resetheaders. - Once you exceed the limit you'll get a 429 response with the code
rate_limited; wait a few seconds and try again. - Links created through the API count against your plan's overall limit and monthly quota.
- Bulk creation accepts up to 50 links per request.
Errors
Error responses always include a human-readable message (in the Accept-Language language: Persian, English or Arabic) in error and a stable English code in code; base your application logic on code.
{ "error": "This API key doesn't have the “links:write” scope.", "code": "insufficient_scope", "requiredScope": "links:write" }| Code | HTTP | Meaning |
|---|---|---|
missing_api_key | 401 | No key was sent in the Authorization or X-API-Key header. |
invalid_api_key | 401 | The key is invalid or has been revoked. |
key_expired | 401 | The key has expired; create a new one. |
key_disabled | 403 | The key has been temporarily disabled by the site admin. |
ip_not_allowed | 403 | Your IP isn't in this key's allowlist. |
api_access_denied | 403 | Your plan or account doesn't have API access (the reason field explains why). |
insufficient_scope | 403 | The key lacks the permission this endpoint needs (see requiredScope). |
validation_error | 400 | The request body is invalid (the field property names the problem field). |
not_found | 404 | Link or endpoint not found. |
alias_taken | 409 | The custom alias is already taken. |
url_rejected | 422 | The destination URL was rejected for security reasons. |
limit_reached | 403 | You've reached your plan's total link limit. |
quota_exceeded | 429 | Your plan's monthly link quota is used up. |
rate_limited | 429 | Requests per minute exceeded the key's limit. |
API FAQ
Short answers to common developer questions.
Is the Kokli API free?
API access is included in the plans marked “API access” on the pricing page. Links created through the API count towards your plan's quota.
Where do I create an API key, and what if it leaks?
Create keys in the “API keys” section of your dashboard. A key is shown only once and we store only its hash. If a key leaks, revoke it there and create a new one; revocation takes effect immediately.
What's the API rate limit?
Each key allows 60 requests per minute by default. Your key's exact limit is in the GET /me response and in the RateLimit-Limit and RateLimit-Remaining headers of every response. If you get rate_limited, wait a moment and retry.
How do I restrict what a key can do?
When creating or editing a key, select only the permissions it needs (e.g. just links:read for reporting). You can also restrict the key to your servers' IPs and set an expiry date.
Do you have an OpenAPI (Swagger) spec?
Yes. The OpenAPI 3 file is available at https://kok.li/api/v1/openapi.json and can be imported into Postman, Insomnia or Swagger UI.