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": null,
"shield_enabled": true,
"landing": {
"title": "Summer Drop",
"bio": "New content every week",
"avatar_url": "https://klickly.app/api/upload/abc123.jpg",
"theme": "midnight",
"elements": [
{
"id": "clx...",
"label": "Exclusive content",
"destination_url": "https://example.com/offer",
"type": "LINK",
"position": 0,
"active": true
}
]
},
"settings": {
"decoy_mode": true,
"webview_escape": false,
"adult_warning": true,
"geo_rules": 2,
"language_rules": 0,
"ga_measurement_id": null,
"meta_pixel_id": "123456789"
}
}`destination_url` is `null` in landing mode. A landing page has no single destination — the destinations are its buttons, in landing.elements, ordered as they render. In escape mode it is the one URL the link sends visitors to, and landing.elements is empty. avatar_url is an absolute, directly-fetchable URL.
settings mirrors the per-link switches in the editor: decoy_mode and webview_escape are the protections, adult_warning the age notice, and ga_measurement_id/meta_pixel_id your tracking pixels. Geo and language redirects are reported as counts only — the rules themselves hold destination URLs, which is precisely the data a Klickly landing keeps out of its own markup.
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": "A", "name": "yourbrand.com", "value": "216.198.79.1", "purpose": "routing" },
{ "type": "TXT", "name": "_klickly-verify.yourbrand.com", "value": "klickly-verify=...", "purpose": "verification" }
],
"primary_smart_link": { "id": "clx...", "slug": "tania", "url": "https://yourbrand.com" },
"created_at": "2026-07-07T09:00:00.000Z"
}
]
}ssl_status is one of unverified (not verified yet), awaiting_dns (verified, but the A record doesn't point at us yet), issuing (DNS correct, certificate provisioning), active (serving HTTPS) or failed.
A domain serves exactly one smart link at its bare root: primary_smart_link.url is the naked domain, no slug. Assign the link by setting domainId when creating/updating the link (or primary_smart_link_id on the domain via PATCH).
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): an A record (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 domain isn't active yet, this also re-checks its DNS routing live.
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": [
{
"id": "clx...",
"username": "tania",
"display_name": "Tania B.",
"profile_pic_url": "https://...",
"is_banned": false,
"is_restricted": false,
"is_private": false,
"is_verified": true,
"last_synced_at": "2026-08-14T06:00:00.000Z",
"metrics": {
"followers": 25715,
"following": 412,
"posts_count": 57,
"avg_likes": 127.83,
"avg_comments": 4.5,
"engagement_rate": 0.51,
"total_views": 1557890,
"captured_at": "2026-08-14T06:00:00.000Z"
}
}
],
"smart_link_count": 4,
"created_at": "2026-07-01T09:00:00.000Z",
"updated_at": "2026-07-08T10:00:00.000Z"
}
],
"next_cursor": null
}`metrics` is the most recent daily snapshot, not a live read — captured_at tells you when it was taken. Every field is null for a handle we have never managed to read.
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.
Instagram monitoring
What is_banned and is_restricted mean
These are not two degrees of the same problem, and confusing them will make you act on healthy accounts.
- `is_restricted: true` — the account is alive and working normally for logged-in users. Instagram simply hides it from logged-out visitors, almost always because of an age gate. It is not a penalty, not a ban, and never a reason to archive. We read these handles through an authenticated provider, so their metrics keep flowing. A handle can sit restricted for months and be perfectly healthy.
- `is_banned: true` — Instagram reports the handle as gone. We require two independent providers to agree, sustained over several days, before setting this, so it is not a transient read failure.
List a creator's handles
GET /api/v1/creators/{id}/instagram — scope creators:read
The tracked handles with their current figures, without the surrounding creator payload. Same profile object as the one nested inside a creator.
curl https://klickly.app/api/v1/creators/clx.../instagram \
-H "Authorization: Bearer klk_live_your_secret_key"Follower and engagement history
GET /api/v1/creators/{id}/instagram/{username}/history?days=30 — scope creators:read
One entry per day we successfully read the handle. days accepts 1–365 (default 30); outside that range you get a 400.
{
"username": "tania",
"period_days": 30,
"history": [
{
"date": "2026-08-13",
"followers": 25713,
"following": 412,
"posts_count": 54,
"avg_likes": 185.08,
"avg_comments": 4.2,
"engagement_rate": 0.74,
"total_views": 1543873
}
]
}Gaps are meaningful. This series is deliberately not zero-filled: a missing day means no successful sync happened that day (a provider outage, a throttled restricted handle, or the handle being added mid-window). Filling it with zeros would render as the account collapsing and recovering overnight.
Posts and reels
GET /api/v1/creators/{id}/instagram/{username}/posts?limit=12 — scope creators:read
Newest first, keyset-paginated: pass the returned next_cursor as ?cursor=. limit accepts 1–50 (default 12).
{
"username": "tania",
"data": [
{
"id": "clx...",
"post_id": "3401...",
"type": "REEL",
"caption": "nuevo",
"likes": 412,
"comments": 18,
"views": 24500,
"thumbnail_url": "https://...",
"permalink": "https://instagram.com/p/...",
"posted_at": "2026-08-13T18:22:00.000Z"
}
],
"next_cursor": "MjAyNi0w..."
}views is null, never 0, for post types Instagram doesn't report a play count for — an absent measurement rather than a measured zero.
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,
"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 in whole UTC days ending today, 1–90 (default 30). Outside that range, or not an integer, returns 400. |
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,
"unique_clickers": 33,
"unique_viewers": 61
}
],
"by_country": [{ "country": "ES", "events": 512 }],
"by_city": [{ "city": "Madrid", "country": "ES", "events": 188 }],
"by_device": [{ "device": "mobile", "events": 690 }],
"by_browser": [{ "browser": "Safari", "events": 402 }],
"by_source": [{ "source": "instagram", "events": 588 }],
"by_element": [
{
"element_id": "clx...",
"label": "Exclusive content",
"destination_url": "https://...",
"type": "LINK",
"clicks": 611,
"ctr": 50.75,
"share": 72.5
}
],
"shield": {
"events": 318,
"by_family": [{ "family": "Meta", "events": 266 }],
"timeline": [{ "date": "2026-07-08", "events": 12 }]
}
}How to read these numbers
`ctr` is visitor-level, so it will not equal `clicks ÷ views`. It is unique_clickers ÷ unique_viewers, capped at 100: one visitor who taps three buttons is one converted visitor, not three. Both operands ship in totals so you can verify it. On landing links clicks routinely exceeds views for exactly that reason.
`days=N` returns exactly N timeline points, oldest first, zero-filled — a day with no traffic is present with zeros rather than missing.
Breakdown `events` = views + clicks, not clicks alone. Use it for relative share within a dimension; don't sum it against totals.clicks.
`by_city` is partial. Only events whose city resolved from the IP are counted — in practice about half — so this breakdown will not add up to the totals. Country resolves far more reliably.
`by_element` is per-button performance for landing links, and is an empty array for escape links, which have no buttons. Each ctr is the share of unique page viewers who tapped that button, so the column can sum past 100%; share is that button's slice of all element clicks and sums to 100%.
`shield` reports the bot traffic Shield caught in the window. Crawlers are grouped into coarse families (Meta, Google, TikTok, SEO bots, Scripts, Other); we don't expose the exact crawler name or the rule that matched it, since that would double as a manual for evading the Shield.
`by_source` uses direct when the visitor arrived with no referrer, and internal for navigation within Klickly (a refresh, or one of your own links pointing at another). Every event lands in a bucket, so the breakdown reconciles against the totals.
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 in whole UTC days ending today, 1–90 (default 30). Outside that range returns 400. |
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,
"unique_clickers": 9012,
"unique_viewers": 19045,
"ctr": 47.32,
"links": 22,
"shield_hits": 318
},
"timeline": [
{ "date": "2026-07-08", "clicks": 612, "views": 980 }
],
"by_country": [{ "country": "ES", "events": 7201 }],
"by_city": [{ "city": "Madrid", "country": "ES", "events": 2410 }],
"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": {
"events": 266,
"by_family": [{ "family": "Meta", "events": 221 }],
"timeline": [{ "date": "2026-07-08", "events": 9 }]
}
}Everything from the per-link endpoint applies here too: visitor-level ctr with its operands alongside, exactly period_days timeline points, events = views + clicks, partial by_city, and Shield reported by family.
Watch the two Shield figures. totals.shield_hits is a lifetime counter carried on your links — it is not windowed, unlike everything else in this response. shield.events is the windowed count and is the one to compare across periods. Both are exposed because the lifetime figure predates the breakdown and existing integrations already read it.
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.