Skip to content

Tip: Start by listing your pages with GET /api/public/pages, grab the slug you need, then fetch stats with GET /api/public/stats/{slug}. A machine-readable spec of everything on this page is served at /api/public/openapi.json.

The full API surface is described as an OpenAPI 3.1 document:

GET https://onlynk.me/api/public/openapi.json

Feed it to your API client, code generator or AI agent. It is the machine-readable twin of this page.

The public API is subject to rate limiting. If you exceed the limit, you receive a 429 Too Many Requests response.

Best practices:

  • Cache results on your end, stats don’t update in real time
  • Avoid polling more frequently than once every few minutes
  • Use the startDate/endDate range to request only the data you need

Lists all pages accessible to your API key: both personal pages and pages from organizations where your account has API access.

URL

GET https://onlynk.me/api/public/pages

Headers

Authorization: Bearer YOUR_API_KEY

Response

{
"personal": [
{
"id": "page_123",
"slug": "my_page",
"title": "My Page",
"published": true,
"note": null,
"createdAt": "2026-05-01T10:00:00.000Z",
"updatedAt": "2026-06-20T08:30:00.000Z"
}
],
"organizations": [
{
"organization": { "id": "org_123", "name": "My Team" },
"pages": [
{
"id": "page_456",
"slug": "team_page",
"title": "Team Page",
"published": true,
"note": "Q3 campaign",
"createdAt": "2026-05-12T09:00:00.000Z",
"updatedAt": "2026-06-18T16:45:00.000Z"
}
]
}
]
}

Response fields

FieldTypeDescription
personalarrayPages owned by your personal account
organizationsarrayGroups of pages by organization
organizations[].organization.idstringOrganization ID
organizations[].organization.namestringOrganization display name
pages[].idstringInternal page ID
pages[].slugstringPublic URL handle, use this for the stats endpoint
pages[].titlestring | nullPage display title
pages[].publishedbooleanWhether the page is publicly accessible
pages[].notestring | nullPrivate note attached to the page
pages[].createdAt / pages[].updatedAtISO date-timeCreation / last update timestamps

Legacy keys: the response also contains personalPages, organizationPages and summary. They are deprecated aliases kept for backward compatibility; prefer personal and organizations.

Example

Terminal window
curl -sS \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://onlynk.me/api/public/pages"

Creates a published page on the given handle.

URL

POST https://onlynk.me/api/public/pages

Headers

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Body

FieldTypeRequiredDescription
slugstringPublic handle, 3-32 chars, letters/numbers/_/., cannot start or end with a period. Stored lowercased.
titlestring-Display title (max 128 chars)
organizationIdstring-Create the page inside an organization you have API access to
linksarray-Initial links, max 50. Each: { title, url, order?, icon?, iconUrl?, effect?, adult?, cloak? }, same fields as POST /pages/{slug}/links.
socialsarray-Initial social links, max 50. Each: { type, url, order? }, same fields as POST /pages/{slug}/socials.

Handles share a single namespace with smart links: a slug must be free across both. Check GET /api/public/slug-availability first to avoid 409 errors. Cards and sections don’t exist yet at creation time. Add them afterward with their own endpoints.

Response (201 Created)

{
"id": "page_789",
"slug": "my_new_page",
"title": "My New Page",
"published": true,
"note": null,
"createdAt": "2026-07-07T10:00:00.000Z",
"updatedAt": "2026-07-07T10:00:00.000Z",
"organization": null,
"links": [],
"url": "https://onlynk.me/my_new_page"
}

Errors

StatuscodeCause
402plan_limit_reachedThe workspace owner’s plan link limit is reached
403forbiddenYour plan doesn’t include API access, or no API access to that organization
409slug_reservedThe handle collides with a reserved word or system route
409slug_takenThe handle is already used by a page or smart link
422invalid_requestBody validation failed (the response includes Zod issues)

Example

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"slug": "my_new_page", "title": "My New Page"}' \
"https://onlynk.me/api/public/pages"

Fetches full detail for a page: metadata, appearance settings, and its content relations (links, cards, sections, socials) with their ids, which you need to call the content endpoints below.

URL

GET https://onlynk.me/api/public/pages/{slug}

Response

{
"id": "page_789",
"slug": "my_page",
"title": "My Page",
"description": null,
"published": true,
"note": null,
"backgroundColor": null,
"buttonShape": "PILL",
"buttonFill": "SOLID",
"style": "DEFAULT",
"directLink": null,
"online": false,
"metaPixelEnabled": false,
"createdAt": "2026-05-01T10:00:00.000Z",
"updatedAt": "2026-07-07T10:00:00.000Z",
"links": [
{ "id": "link_1", "title": "My site", "url": "https://example.com", "order": 0, "sectionId": null, "cloakUrl": null }
],
"cards": [],
"sections": [],
"socials": [],
"url": "https://onlynk.me/my_page"
}

The full field list (all appearance columns) is in the OpenAPI spec (PageDetail schema).

Errors: 404 not_found, 403 forbidden.


Partially updates a page’s metadata and appearance. Content (links/cards/sections/socials) has its own endpoints below; handle renames stay in the dashboard.

URL

PATCH https://onlynk.me/api/public/pages/{slug}

Headers

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Body (every field is optional but at least one is required; null clears a nullable field)

FieldTypeDescription
titlestring | nullDisplay title (max 128 chars)
notestring | nullPrivate dashboard note (max 32 chars)
publishedbooleanPublish or unpublish the page
descriptionstring | nullBio text (max 256 chars)
picture / bannerstring | nullProfile picture / banner image URL. Upload bytes first with POST /pages/{slug}/images.
backgroundColor / textColor / buttonsColor / buttonsTextColor / cardBackgroundColorstring | nullHex colors
backgroundGradientstring | nullCSS gradient
buttonShapePILL | ROUNDED | SQUAREButton shape
buttonFillSOLID | OUTLINE | SOFT_SHADOWButton fill style
fontFamilystring | nullFont family
stylestringPage theme preset
directLinkstring | nullSkip the landing page and redirect straight to this URL. Requires a plan with direct links.
online / city / cityText / responseDelay / promotion-”Fake” online/city presence widgets. Require advanced customization on your plan.
chatbotMessages / chatbotLinkarray / string | nullSimulated chatbot bubble content and its link
metaPixelId / metaPixelEnabledstring | null / booleanMeta Pixel tracking. Requires a plan with Meta Pixel.

Setting a plan-gated field without the required plan feature returns 403 forbidden, the same gating as the dashboard editor.

Response (200 OK): the updated page, same shape as GET /pages/{slug} minus the content relations.

Errors: 404 not_found, 403 forbidden (no access, plan lost API access, or a plan-gated field), 422 invalid_request (validation failed or empty body).

Example (unpublish a page)

Terminal window
curl -sS -X PATCH \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"published": false}' \
"https://onlynk.me/api/public/pages/my_page"

Uploads image bytes and hands back a hosted, optimized URL. This is a generic, two-step flow, the same as the dashboard’s own uploader: upload first, then use the returned URL wherever an image field is expected (picture/banner on the page, or image/iconUrl on a link or card).

The dashboard optimizes images client-side (FilePond: auto-orient, crop, resize, re-encode) before they ever reach the server. This endpoint has no browser to do that, so it does the equivalent server-side: auto-orients from EXIF, resizes/crops to a target profile, and re-encodes at the same quality, matching the targets FilePond uses per field.

URL

POST https://onlynk.me/api/public/pages/{slug}/images

Headers

Authorization: Bearer YOUR_API_KEY
Content-Type: multipart/form-data

Body

FieldTypeRequiredDescription
filefileThe image. Max 5MB. Accepted formats: jpeg, png, webp, heic, heif.
purposestring-Resize/crop profile: picture (800×800, cropped square), banner (2048×1024, cropped), card (1024 wide, fit), icon (256 wide, fit), generic (1600 wide, fit, the default). Images are never upscaled.

Response (201 Created)

{ "url": "https://media.onlynk.me/my_page/api/8f14e45f-....jpg", "width": 800, "height": 800 }

Errors: 404 not_found, 403 forbidden, 422 invalid_request (missing file, oversized, unsupported format, or invalid purpose).

HEIC/HEIF: re-encoded like any other format. If optimization ever fails for a given file (corrupt image, decoder edge case), the original bytes are stored as-is rather than rejecting the upload.

Example

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-F "file=@./avatar.jpg" \
-F "purpose=picture" \
"https://onlynk.me/api/public/pages/my_page/images"
Terminal window
# Then attach it to the page
curl -sS -X PATCH \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"picture": "https://media.onlynk.me/my_page/api/8f14e45f-....jpg"}' \
"https://onlynk.me/api/public/pages/my_page"

Section titled “Page content: links, cards, sections, socials”

A page’s content is managed through granular endpoints, one resource at a time: add, update or remove a single item without resending the rest. Each resource follows the same shape:

GET /api/public/pages/{slug}/{resource} list, in display order
POST /api/public/pages/{slug}/{resource} create one, appended at the end unless order is given
PATCH /api/public/pages/{slug}/{resource}/{id} update one (partial, at least one field)
DELETE /api/public/pages/{slug}/{resource}/{id} delete one

where {resource} is links, cards, sections or socials. All four require the same headers as other write endpoints and return 404 not_found if the page, or the item under that page, doesn’t exist.

A page can have up to 50 links.

Fields: title (required, max 128), url (required), order, icon (brand icon, e.g. INSTAGRAM), iconUrl (custom icon image URL; upload one with POST /pages/{slug}/images), effect (attention animation, e.g. PULSE), adult, cloak (hide the destination behind a /go/{id} redirect, requires a plan with cloaking), sectionId (group under a section, or null to ungroup).

{
"id": "link_1",
"title": "My site",
"url": "https://example.com",
"order": 0,
"icon": null,
"iconUrl": null,
"effect": null,
"adult": false,
"sectionId": null,
"cloak": null,
"cloakUrl": null
}
Terminal window
curl -sS -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "My site", "url": "https://example.com"}' \
"https://onlynk.me/api/public/pages/my_page/links"

Passing an unknown sectionId returns 422 invalid_request. Reaching the 50-link cap returns 422 content_limit_reached.

Image cards, same shape as links but with image instead of effect. A page can have up to 50 cards.

Fields: title (required), url (required), image (upload one with POST /pages/{slug}/images), order, icon, iconUrl, adult, cloak, sectionId.

Named categories that group links/cards (e.g. “My partners”). A page can have up to 20 sections.

Fields: title (required, 1-64 chars), order.

Deleting a section does not delete its links/cards; they’re kept, ungrouped (sectionId set back to null).

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "My partners"}' \
"https://onlynk.me/api/public/pages/my_page/sections"

Social network profile links. A page can have up to 50.

Fields: type (required, e.g. INSTAGRAM, TIKTOK, YOUTUBE; see the OpenAPI spec for the full enum), url (required), order.

Terminal window
curl -sS -X PATCH \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"url": "https://instagram.com/new_handle"}' \
"https://onlynk.me/api/public/pages/my_page/socials/social_1"

Fetches analytics for a specific page identified by its slug.

URL

GET https://onlynk.me/api/public/stats/{slug}

Headers

Authorization: Bearer YOUR_API_KEY

Query parameters

ParameterTypeRequiredDescription
startDateYYYY-MM-DD-Start of the date range (inclusive). Defaults to 30 days ago.
endDateYYYY-MM-DD-End of the date range (inclusive). Defaults to today.

Dates are interpreted as UTC. An unparseable date returns 400 with code invalid_date_range.

Response

{
"slug": "my_page",
"totalViews": 1234,
"totalClicks": 256,
"totalDirect": 89,
"totalChatbotClicks": 42,
"totalSafed": 15,
"totalBlocked": 7,
"topCountries": [
{ "country": "FR", "views": 420 },
{ "country": "US", "views": 210 }
],
"topLinks": [
{ "url": "https://instagram.com/myprofile", "clicks": 130 },
{ "url": "https://youtube.com/mychannel", "clicks": 90 }
],
"topReferrers": [
{ "referrer": "instagram.com", "views": 300 },
{ "referrer": "direct", "views": 180 }
],
"topDeviceTypes": [
{ "deviceType": "mobile", "views": 900 },
{ "deviceType": "desktop", "views": 334 }
],
"dailyStats": [
{ "date": "2026-03-01", "views": 45, "clicks": 8 },
{ "date": "2026-03-02", "views": 62, "clicks": 11 }
]
}

Response fields

FieldTypeDescription
totalViewsnumberTotal PAGE_VIEW events in the range
totalClicksnumberLINK_CLICK + CARD_CLICK events
totalDirectnumberREDIRECT events (direct link redirects)
totalChatbotClicksnumberCHATBOT_CLICK events
totalSafednumberSAFE events, bots served the landing page instead of being redirected
totalBlockednumberBLOCK events, visitors blocked by geo rules
topCountriesarrayTop 5 countries by views, { country: "ISO-2", views: N }
topLinksarrayTop 5 clicked link URLs, { url: string, clicks: N }
topReferrersarrayTop 5 traffic sources, { referrer: string, views: N }. "direct" means no referrer header.
topDeviceTypesarrayTop 5 device categories, { deviceType: string, views: N }
dailyStatsarrayPer-day breakdown { date: "YYYY-MM-DD", views: N, clicks: N }, sorted ascending

Example (custom date range)

Terminal window
curl -sS \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://onlynk.me/api/public/stats/your-slug?startDate=2026-03-01&endDate=2026-03-31"

Access rules

Your API key can query stats for:

  • Pages owned by your personal account
  • Pages owned by an org where your account is OWNER or ADMIN

If neither condition is true, the endpoint returns 403 Forbidden.


Lists all smart links accessible to your API key, grouped like the pages endpoint.

URL

GET https://onlynk.me/api/public/smart-links

Headers

Authorization: Bearer YOUR_API_KEY

Response

{
"personal": [
{
"id": "sl_123",
"slug": "promo",
"title": "Summer promo",
"enabled": true,
"fallbackUrl": "https://example.com",
"note": null,
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-07-01T18:00:00.000Z"
}
],
"organizations": [
{
"organization": { "id": "org_123", "name": "My Team" },
"smartLinks": []
}
]
}
FieldTypeDescription
personalarraySmart links owned by your personal account
organizationsarrayGroups of smart links by organization
smartLinks[].idstringInternal smart link ID
smartLinks[].slugstringPublic handle, the link resolves at https://onlynk.me/{slug}
smartLinks[].titlestringDisplay name (dashboard only)
smartLinks[].enabledbooleanDisabled links resolve to their fallback
smartLinks[].fallbackUrlstringDestination used when the flow can’t produce an action
smartLinks[].notestring | nullPrivate note attached to the link
smartLinks[].createdAt / smartLinks[].updatedAtISO date-timeCreation / last update timestamps

Creates an enabled smart link on the given handle. Without a flow, every visit redirects to fallbackUrl; conditional flows (geo, device, A/B, schedule…) are built in the dashboard’s visual editor.

URL

POST https://onlynk.me/api/public/smart-links

Headers

Authorization: Bearer YOUR_API_KEY
Content-Type: application/json

Body

FieldTypeRequiredDescription
titlestringDisplay name (1-120 chars, dashboard only)
slugstringPublic handle, same rules as page creation. Stored lowercased.
fallbackUrlstringhttp(s) URL served when no flow matches (or none exists)
organizationIdstring-Create the link inside an organization you have API access to
flowobject-Optional starting Smart Flow graph ({ nodes, edges }), advanced: usually omitted and built afterward in the dashboard’s visual editor or via PATCH /smart-links/{slug}.

Handles share a single namespace with pages: check GET /api/public/slug-availability first to avoid 409 errors.

Response (201 Created)

{
"id": "sl_456",
"slug": "promo",
"title": "Summer promo",
"enabled": true,
"fallbackUrl": "https://example.com/offer",
"note": null,
"createdAt": "2026-07-07T10:00:00.000Z",
"updatedAt": "2026-07-07T10:00:00.000Z",
"organization": null,
"url": "https://onlynk.me/promo"
}

Errors: same as page creation: 402 plan_limit_reached (smart links share the plan’s link budget with pages), 403 forbidden, 409 slug_reserved / slug_taken, 422 invalid_request.

Example

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"title": "Summer promo", "slug": "promo", "fallbackUrl": "https://example.com/offer"}' \
"https://onlynk.me/api/public/smart-links"

Fetches full detail for a smart link: metadata, the complete flow graph (nodes/edges), and workspace placement (folder, custom domain).

URL

GET https://onlynk.me/api/public/smart-links/{slug}

Response

{
"id": "sl_456",
"slug": "promo",
"title": "Summer promo",
"flow": null,
"flowVersion": 0,
"fallbackUrl": "https://example.com/offer",
"note": null,
"enabled": true,
"groupId": null,
"domainId": null,
"isCustomDomainHome": false,
"pageId": null,
"createdAt": "2026-06-01T10:00:00.000Z",
"updatedAt": "2026-07-01T18:00:00.000Z",
"url": "https://onlynk.me/promo"
}

Use this to fetch the current flow before modifying it with PATCH (rather than overwriting it blind), or to grab the ids (groupId, domainId) surfaced only here.

Errors: 404 not_found, 403 forbidden.


Partially updates a smart link. Use enabled to pause or resume the link without deleting it. flow replaces the conditional-redirect graph entirely.

URL

PATCH https://onlynk.me/api/public/smart-links/{slug}

Body (at least one field required)

FieldTypeDescription
titlestringDisplay name (1-120 chars)
fallbackUrlstringhttp(s) fallback destination
notestring | nullPrivate dashboard note (max 32 chars). null or "" clears it.
enabledbooleanfalse pauses the link (visits resolve to the fallback)
flowobject | nullSerialized Smart Flow graph ({ nodes, edges }), validated against the same schema as the dashboard editor. null clears it; the link then always resolves to fallbackUrl. Fetch the current one via GET first if you’re modifying rather than replacing wholesale.

Response (200 OK): the updated smart link, same shape as GET /smart-links/{slug}.

Errors: 404 not_found, 403 forbidden, 422 invalid_request (validation failed, including an invalid flow graph).

Example (pause a link)

Terminal window
curl -sS -X PATCH \
-H "Authorization: Bearer YOUR_API_KEY" \
-H "Content-Type: application/json" \
-d '{"enabled": false}' \
"https://onlynk.me/api/public/smart-links/promo"

Building a flow: the graph shape (predicate types: geo, referrer, random A/B, device, visit count, schedule, language…) is documented in the OpenAPI spec. In practice, the easiest path for an AI agent is to fetch an existing flow via GET and adapt it, rather than building one from scratch.


POST /api/public/smart-links/{slug}/duplicate

Section titled “POST /api/public/smart-links/{slug}/duplicate”

Duplicates a smart link: same fallbackUrl and flow, onto a new auto-generated slug ({slug}_copie, {slug}_copie_2, …). The duplicate is created disabled so it never serves before review, the same behavior as the dashboard’s duplicate action.

URL

POST https://onlynk.me/api/public/smart-links/{slug}/duplicate

Response (201 Created): the new smart link, same shape as GET /smart-links/{slug}.

Errors: 404 not_found, 403 forbidden, 402 plan_limit_reached (the duplicate consumes a link slot like any new link).

Example

Terminal window
curl -sS -X POST \
-H "Authorization: Bearer YOUR_API_KEY" \
"https://onlynk.me/api/public/smart-links/promo/duplicate"

Fetches cookieless visit analytics for a smart link.

URL

GET https://onlynk.me/api/public/smart-links/{slug}/stats

Query parameters: same startDate / endDate as page stats (default: last 30 days, UTC).

Response

{
"slug": "promo",
"title": "Summer promo",
"enabled": true,
"total": 980,
"uniqueVisitors": 640,
"daysActive": 22,
"growthVisits": 12.5,
"growthUnique": 8.1,
"byDay": [{ "date": "2026-07-01", "count": 44 }],
"byAction": [{ "key": "redirect", "count": 900 }],
"topDestinations": [{ "key": "https://example.com/offer", "count": 620 }],
"byCountry": [{ "key": "FR", "count": 410 }],
"byDevice": [{ "key": "mobile", "count": 720 }],
"byReferrer": [{ "key": "instagram.com", "count": 300 }]
}
FieldTypeDescription
totalnumberTotal visits in the range
uniqueVisitorsnumberDistinct visitors (IP-based, cookieless)
growthVisits / growthUniquenumber | nullPercent change vs the previous period of the same length
byActionarrayVisits by terminal action (redirect, landing, deeplink, …)
topDestinationsarrayMost frequent resolved destinations

Access rules: same as page stats (author, org owner, or org OWNER/ADMIN).


Checks whether a handle can be claimed. No authentication required.

URL

GET https://onlynk.me/api/public/slug-availability?slug=my_handle

Response

{ "available": false, "reason": "taken" }
reasonMeaning
invalidThe slug doesn’t match the handle format rules
reservedThe slug collides with a reserved word or system route
takenThe slug is already used by a page or a smart link

Availability spans the shared namespace (pages and smart links) and is a soft signal: the handle is only truly reserved once the page or smart link is created.


Every error body contains a human-readable error message and a stable machine-readable code:

{ "error": "Username already exists", "code": "slug_taken" }
StatuscodeCause
400invalid_date_rangestartDate/endDate is not a valid date
401api_key_requiredNo Authorization header
403invalid_api_keyKey doesn’t match any account
403forbiddenKey is valid but you don’t have access to this resource
404not_foundNo page, smart link, or content item (link/card/section/social) with that id exists
402plan_limit_reachedPlan link limit reached (page and smart link creation share one budget)
409slug_reserved / slug_takenHandle unavailable (page or smart link creation)
422invalid_requestBody validation failed
422content_limit_reachedPage content cap reached (50 links, 50 cards, 20 sections, or 50 socials)
429-Rate limit exceeded; back off and retry