API Reference
The Klickly API lets you read and manage your smart links and account programmatically over HTTPS. This reference covers v1.
All endpoints live under the base URL https://klickly.app/api/v1, return JSON, and require a secret API key sent as a Bearer token. API access is available on the Agency, Scale and Enterprise plans.
A machine-readable OpenAPI 3.1 description of this API is published at https://klickly.app/api/openapi.json — point Swagger UI, Postman, an SDK generator or an AI coding agent at it to explore every endpoint, parameter and response schema without reading this page by hand.
Getting started
1. Create an API key
- Open your Klickly dashboard and go to Settings → Developers.
- Click Create key, give it a name, choose the permissions (scopes) it needs, and optionally an expiry.
- Copy the secret immediately: it starts with
klk_live_and is shown only once. If you lose it, revoke the key and create a new one. - Only an organization admin on a plan with API access can create or revoke keys. Each organization can have up to 25 active keys.
2. Make your first request
Send the key in the Authorization header:
curl https://klickly.app/api/v1/links \
-H "Authorization: Bearer klk_live_your_secret_key"If the key is valid you get a JSON response. That's it — you're talking to the API.
Authentication
- Every request must include the header
Authorization: Bearer <key>. Keys are accepted only from this header — never from the query string or request body. - A key is the prefix
klk_live_followed by 43 characters, for exampleklk_live_Ab3xZ9q2…. - Klickly stores only a hash of your key, never the key itself. Treat it like a password: keep it server-side and never commit it to source control or expose it in a browser.
- Keys are verified live on every request. A revoked or expired key — or one whose organization is no longer on an API plan — stops working immediately, with no caching.
Rate limits
Requests are limited per key and per organization:
| Limit | Value |
|---|---|
| Per key | 120 requests / minute |
| Per organization | 600 requests / minute (across all its keys) |
When you exceed a limit you get a 429 response with a Retry-After header (in seconds). Your current budget is also returned by GET /account/usage.
Errors
Every error returns the same envelope, with a stable machine-readable code and a human-readable message:
{ "error": { "code": "not_found", "message": "Link not found" } }| HTTP | Code | Meaning |
|---|---|---|
| 400 | invalid_request | Malformed request or parameters. |
| 401 | unauthorized | Missing, malformed, unknown, revoked or expired key. |
| 403 | forbidden | The key is valid but lacks the required scope. |
| 403 | plan_required | Your plan does not include API access. |
| 403 | quota_exceeded | A plan limit was reached (e.g. active-link cap) or the action needs a feature your plan lacks. |
| 404 | not_found | The resource does not exist, or is not yours (we do not reveal which). |
| 409 | conflict | The request conflicts with the current state — e.g. a slug already in use, or deleting a link that is a custom domain's primary destination. |
| 429 | rate_limited | You hit a rate limit; retry after the delay. |
| 500 | internal_error | Something went wrong on our side. |
Pagination
List endpoints return a page of results plus a cursor:
{ "data": [ /* … */ ], "next_cursor": "MjAyNi0wNi0yOFQxMTo…" }- Pass
?limit=to set the page size — an integer from 1 to 100 (default 50). - To fetch the next page, pass the
next_cursorvalue back as?cursor=. Whennext_cursorisnull, you have reached the last page.
curl "https://klickly.app/api/v1/links?limit=25&cursor=MjAyNi0wNi0yOFQxMTo…" \
-H "Authorization: Bearer klk_live_your_secret_key"Idempotency
To retry a create (POST) safely — for example after a network timeout, when you don't know whether the first attempt reached us — send an Idempotency-Key header with a unique value you generate (a UUID works well):
curl -X POST https://klickly.app/api/v1/links \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Idempotency-Key: 8f3a1c9e-4b2d-4e6a-9c7f-1a2b3c4d5e6f" \
-H "Content-Type: application/json" \
-d '{"destinationUrl":"https://example.com/offer"}'- The first request with a given key runs normally and we store its response.
- Any retry with the same key returns that stored response verbatim (with an
Idempotent-Replayed: trueheader) instead of creating a second resource. - Reusing a key with a different request body returns
400— a key must identify one operation. - If the first request is still in flight, a retry returns
409; wait a moment and try again.
Keys are scoped to your organization, apply to POST requests only, and expire after 24 hours. Only successful and client-error (non-5xx) responses are stored — a server error frees the key so you can retry cleanly.
Endpoints
List links
GET /api/v1/links — scope links:read
Returns your organization's smart links, newest first.
| Parameter | Type | Description |
|---|---|---|
limit | integer | Page size, 1–100 (default 50). |
cursor | string | Pagination cursor from a previous response. |
curl https://klickly.app/api/v1/links \
-H "Authorization: Bearer klk_live_your_secret_key"{
"data": [
{
"id": "clx7k2p9a0001abcd1234efgh",
"slug": "summer-drop",
"url": "https://klck.link/summer-drop",
"title": "Summer Drop",
"mode": "landing",
"active": true,
"creator_id": "clx7k2p9a0002ijkl5678mnop",
"created_at": "2026-07-01T14:22:05.123Z",
"updated_at": "2026-07-06T09:10:44.000Z"
}
],
"next_cursor": "MjAyNi0wNy0wMVQxNDoyMjowNS4xMjNafGNseDdrMnA5YTAwMDE"
}Each item has: id, slug, url (the public klck.link URL), title (may be null), mode (landing or escape), active, creator_id (may be null), created_at and updated_at (ISO 8601).
Get a link
GET /api/v1/links/{id} — scope links:read
Returns a single smart link by its id, including its destination and landing-page details. If the link does not exist — or belongs to another organization — you get the same 404.
curl https://klickly.app/api/v1/links/clx7k2p9a0001abcd1234efgh \
-H "Authorization: Bearer klk_live_your_secret_key"{
"id": "clx7k2p9a0001abcd1234efgh",
"slug": "summer-drop",
"url": "https://klck.link/summer-drop",
"title": "Summer Drop",
"mode": "landing",
"active": true,
"creator_id": "clx7k2p9a0002ijkl5678mnop",
"created_at": "2026-07-01T14:22:05.123Z",
"updated_at": "2026-07-06T09:10:44.000Z",
"destination_url": "https://example.com/offer",
"shield_enabled": true,
"landing": {
"title": "Summer Drop",
"bio": "New content every week",
"avatar_url": "https://klickly.app/api/upload/abc123.jpg",
"theme": "midnight"
}
}In addition to the list fields, a link detail includes destination_url, shield_enabled, and a nested landing object (title, bio, avatar_url, theme — each may be null). avatar_url is an absolute, directly-fetchable URL.
Create a link
POST /api/v1/links — scope links:write
Creates a smart link in your organization. Send a JSON body; every field is optional except that a link needs a destinationUrl. Field names match those returned by the read endpoints' underlying model — the most useful are:
| Field | Type | Description |
|---|---|---|
destinationUrl | string | The URL your link points to (http/https). |
slug | string | Your custom short slug (3–50 chars, a–z 0–9 -). Omit to auto-generate. Must be unique and not a reserved word. |
title | string | Internal title for the link. |
creatorId | string | Attach the link to one of your creators. |
linkMode | string | LANDING (a bio-style page) or ESCAPE (a direct escape page). |
landingTitle, landingBio, landingTheme | string | Landing-page content. |
The response is the same object as Get a link (201 Created).
curl -X POST https://klickly.app/api/v1/links \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"destinationUrl":"https://example.com/offer","slug":"summer-drop","title":"Summer Drop"}'Notes:
- Tenancy is taken from your key — you cannot create a link in another organization, and unknown fields are rejected with
400. - Reaching your plan's link limit returns
403 quota_exceeded; a slug that is taken or reserved returns409 conflict. - To retry safely without creating duplicates, send your own
slug— the same slug can only exist once, so a retried create returns409rather than a second link.
Create links in bulk
POST /api/v1/links/bulk — scope links:write
Create up to 100 links in one request. Send {"links": [ … ]} where each item is the same body as Create a link. Each item is validated and created independently — one bad item does not fail the rest.
curl -X POST https://klickly.app/api/v1/links/bulk \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"links":[
{"destinationUrl":"https://example.com/a","slug":"drop-a"},
{"destinationUrl":"https://example.com/b","slug":"drop-b"}
]}'{
"total": 2,
"created_count": 2,
"failed_count": 0,
"skipped_count": 0,
"created": [
{ "index": 0, "id": "clx...", "slug": "drop-a", "url": "https://klck.link/drop-a" },
{ "index": 1, "id": "clx...", "slug": "drop-b", "url": "https://klck.link/drop-b" }
],
"failed": []
}Notes:
- The request returns
200even if some (or all) items fail — readfailedfor the per-item errors (each has anindex,codeandmessage). - If you hit your plan's link limit part-way through, the remaining items are reported in
skipped_count(not created) instead of repeating the same quota error. - Sending your own
slugon each item makes retries safe: an already-created slug comes back as aconflictinfailed, never a duplicate link.
Update a link
PATCH /api/v1/links/{id} — scope links:write
Updates one or more fields of an existing link, and/or activates/pauses it. Send only the fields you want to change. Include "active": true|false to toggle whether the link is live. At least one field is required.
# Rename and pause a link in one call
curl -X PATCH https://klickly.app/api/v1/links/clx7k2p9a0001abcd1234efgh \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"title":"Summer Drop (ended)","active":false}'Returns the updated link (same shape as Get a link). A missing or cross-tenant id returns 404. Activating a link beyond your plan's active-link cap returns 403 quota_exceeded; renaming onto a taken or reserved slug returns 409 conflict.
Delete a link
DELETE /api/v1/links/{id} — scope links:write
Soft-deletes a link. Returns 204 No Content on success.
curl -X DELETE https://klickly.app/api/v1/links/clx7k2p9a0001abcd1234efgh \
-H "Authorization: Bearer klk_live_your_secret_key"If the link is the primary destination for one or more custom domains, the delete is refused with 409 conflict and the affected domains are listed in error.details.primary_for — deleting it would fall those domains back to klickly.app. Acknowledge the impact and delete anyway by passing ?force=true:
curl -X DELETE "https://klickly.app/api/v1/links/clx7k2p9a0001abcd1234efgh?force=true" \
-H "Authorization: Bearer klk_live_your_secret_key"Custom domains
Bring your own domain so your links resolve on your own brand. Use your root domain (e.g. yourbrand.com) — all lowercase, no https://, no www., no path. The flow is: add the domain, create the DNS records we return, verify it, then optionally choose which of your links loads at the domain root.
Custom domains require a plan that includes them; without that feature, writes return 403 quota_exceeded. Everywhere below, a host you do not own returns 404 (never a hint that it exists elsewhere).
List domains
GET /api/v1/domains — scope domains:read
{
"data": [
{
"host": "yourbrand.com",
"verified": true,
"verified_at": "2026-07-08T10:00:00.000Z",
"ssl_status": "active",
"dns_records": [
{ "type": "CNAME", "name": "yourbrand.com", "value": "cd.klck.link", "purpose": "routing" },
{ "type": "TXT", "name": "_klickly-verify.yourbrand.com", "value": "klickly-verify=...", "purpose": "verification" }
],
"primary_smart_link": { "id": "clx...", "slug": "tania", "url": "https://klck.link/tania" },
"created_at": "2026-07-07T09:00:00.000Z"
}
]
}ssl_status is one of unverified (not verified yet), issuing (verified, certificate provisioning), active (serving HTTPS), failed or blocked.
Add a domain
POST /api/v1/domains — scope domains:write
curl -X POST https://klickly.app/api/v1/domains \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"hostname":"yourbrand.com"}'Returns 201 with the domain and the exact DNS records to create at your registrar (in dns_records): a CNAME (routing) and a TXT (verification). An invalid hostname returns 400; a domain already attached — to you or to another organization — returns 409 conflict.
Get a domain
GET /api/v1/domains/{host} — scope domains:read
Returns a single domain. While the certificate is still issuing, this also refreshes the live SSL status from our edge.
Verify a domain
POST /api/v1/domains/{host}/verify — scope domains:write
Checks your verification TXT record is live and, if it matches, marks the domain verified and starts SSL issuance. If the record is not visible yet (DNS still propagating) or does not match, you get 424 failed_dependency — wait a few minutes and retry.
curl -X POST https://klickly.app/api/v1/domains/yourbrand.com/verify \
-H "Authorization: Bearer klk_live_your_secret_key"Set the primary link
PATCH /api/v1/domains/{host} — scope domains:write
Choose which of your links loads at the domain root. Pass a primary_smart_link_id you own, or null to clear it. A link id you do not own returns 404.
curl -X PATCH https://klickly.app/api/v1/domains/yourbrand.com \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"primary_smart_link_id":"clx7k2p9a0001abcd1234efgh"}'Remove a domain
DELETE /api/v1/domains/{host} — scope domains:write
Detaches the custom domain. Returns 204 No Content.
Creators
Group your smart links under a creator and — on Instagram-monitoring plans — track that creator's Instagram accounts. A creator has a name and zero or more Instagram handles.
List creators
GET /api/v1/creators — scope creators:read
Paginated, newest first. Use ?limit= (1–100, default 50) and pass the returned next_cursor as ?cursor= for the next page.
{
"data": [
{
"id": "clx...",
"name": "Tania",
"instagram_profiles": [
{
"username": "tania",
"display_name": "Tania B.",
"profile_pic_url": "https://...",
"is_banned": false,
"is_restricted": false
}
],
"smart_link_count": 4,
"created_at": "2026-07-01T09:00:00.000Z",
"updated_at": "2026-07-08T10:00:00.000Z"
}
],
"next_cursor": null
}Create a creator
POST /api/v1/creators — scope creators:write
curl -X POST https://klickly.app/api/v1/creators \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"name":"Tania","instagram_usernames":["tania","tania.backup"]}'name is required (max 100 chars); instagram_usernames is optional (up to 20 handles). Returns 201 with the creator. Reaching your plan's creator or Instagram-profile limit returns 403 quota_exceeded.
Get a creator
GET /api/v1/creators/{id} — scope creators:read
Returns a single creator. A missing or cross-tenant id returns 404.
Update a creator
PATCH /api/v1/creators/{id} — scope creators:write
Renames the creator. Send {"name":"New name"}.
Attach Instagram handles
POST /api/v1/creators/{id}/instagram — scope creators:write
curl -X POST https://klickly.app/api/v1/creators/clx.../instagram \
-H "Authorization: Bearer klk_live_your_secret_key" \
-H "Content-Type: application/json" \
-d '{"usernames":["tania.vip"]}'Adds one or more handles (deduped; handles already attached are ignored). A handle already tracked under a different creator returns 409 conflict; exceeding your Instagram-profile limit returns 403 quota_exceeded. Returns the updated creator.
Delete a creator
DELETE /api/v1/creators/{id} — scope creators:write
Removes the creator and its Instagram profiles. Returns 204 No Content. By default the creator's smart links are detached (they keep working with no creator); pass ?force=true to also soft-delete those links.
curl -X DELETE "https://klickly.app/api/v1/creators/clx...?force=true" \
-H "Authorization: Bearer klk_live_your_secret_key"Account profile
GET /api/v1/account/profile — scope account:read
Returns your organization's plan, limits and enabled features.
curl https://klickly.app/api/v1/account/profile \
-H "Authorization: Bearer klk_live_your_secret_key"{
"organization": { "name": "Acme Creators" },
"plan": { "id": "AGENCY", "name": "Agency" },
"limits": {
"links": 20,
"ig_profiles": 20,
"team_members": 5
},
"features": {
"api_access": true,
"custom_domain": true,
"shield_protection": true,
"webview_escape": true,
"instagram_monitoring": true,
"geo_filtering": true,
"ab_testing": true,
"compare_analytics": true,
"analytics_retention_days": 365
}
}A numeric limit of "unlimited" (a string) means there is no cap on that resource for your plan.
Account usage
GET /api/v1/account/usage — scope account:read
Returns your current rate-limit budget and analytics retention window.
curl https://klickly.app/api/v1/account/usage \
-H "Authorization: Bearer klk_live_your_secret_key"{
"rate_limit": {
"per_key_per_minute": 120,
"per_org_per_minute": 600,
"window_seconds": 60
},
"analytics_retention_days": 365
}Link analytics
GET /api/v1/analytics/link — scope analytics:read
Click analytics for a single smart link. Only real (non-bot) traffic is counted.
| Parameter | Type | Description |
|---|---|---|
link_id | string | Required. The link's id. |
days | integer | Look-back window, 1–90 (default 30). |
curl "https://klickly.app/api/v1/analytics/link?link_id=clx7k2p9a0001abcd1234efgh&days=30" \
-H "Authorization: Bearer klk_live_your_secret_key"{
"link_id": "clx7k2p9a0001abcd1234efgh",
"slug": "summer-drop",
"mode": "landing",
"period_days": 30,
"totals": {
"clicks": 842,
"views": 1533,
"unique_clickers": 611,
"unique_viewers": 1204,
"ctr": 50.75
},
"timeline": [
{ "date": "2026-07-08", "clicks": 41, "views": 77 }
],
"by_country": [{ "country": "ES", "events": 512 }],
"by_device": [{ "device": "mobile", "events": 690 }],
"by_browser": [{ "browser": "Safari", "events": 402 }],
"by_source": [{ "source": "instagram", "events": 588 }]
}ctr is the share of unique visitors (deduplicated by IP) who clicked at least one element, as a percentage (0–100). For escape links (mode: "escape") every visitor clicks by definition, so views equals clicks, ctr is 100, and unique_clickers/unique_viewers are null (not measured). Note clicks can exceed views on landing links because a single visitor may tap several buttons — that is why ctr is computed from unique visitors, not the raw ratio.
timeline is by UTC day. The breakdowns (by_country, by_device, by_browser, by_source) report the top entries per dimension, and each entry's events is the total tracked interactions for that value — views plus clicks — not clicks alone. Don't sum events and compare it to totals.clicks; use it for relative share within a dimension. days includes the current (partial) UTC day, so a request for days=7 returns up to 8 daily buckets. If the link does not exist — or belongs to another organization — you get the same 404.
Account analytics
GET /api/v1/analytics/summary — scope analytics:read
Click analytics aggregated across all of your organization's links.
| Parameter | Type | Description |
|---|---|---|
days | integer | Look-back window, 1–90 (default 30). |
creator_id | string | Optional. Narrow to a single creator's links. |
curl "https://klickly.app/api/v1/analytics/summary?days=30" \
-H "Authorization: Bearer klk_live_your_secret_key"{
"period_days": 30,
"totals": {
"clicks": 12840,
"views": 20114,
"ctr": 47.32,
"links": 22,
"shield_hits": 318
},
"timeline": [
{ "date": "2026-07-08", "clicks": 612, "views": 980 }
],
"by_country": [{ "country": "ES", "events": 7201 }],
"by_device": [{ "device": "mobile", "events": 10233 }],
"by_browser": [{ "browser": "Safari", "events": 6120 }],
"by_os": [{ "os": "iOS", "events": 8890 }],
"by_source": [{ "source": "instagram", "events": 9004 }]
}shield_hits is the number of bot requests the Shield absorbed in the window. As with the per-link endpoint, each breakdown entry's events counts views plus clicks for that dimension (not clicks alone), and days includes today's partial UTC day. A missing creator_id covers the whole organization; a creator_id that does not exist — or belongs to another organization — returns 404.
Both analytics endpoints report on the last 90 days at most: older raw click data is compacted and no longer queryable per link.
Scopes
Each key carries one or more scopes. A key can only call an endpoint whose scope it holds; otherwise the request is refused with 403 forbidden. Grant a key the least it needs.
| Scope | Grants | Status |
|---|---|---|
links:read | Read smart links | Available |
links:write | Create, update, bulk-create and delete links | Available |
domains:read | List and view custom domains | Available |
domains:write | Add, verify, set primary and remove custom domains | Available |
creators:read | List and view creators | Available |
creators:write | Create, update, delete creators + attach IG handles | Available |
account:read | Read plan, limits and usage | Available |
analytics:read | Read click analytics for links and account | Available |
What's next
- Have a use case or need a scope we do not offer yet? Email us at support@klickly.app and tell us what you are building.