Endpoints reference
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.
OpenAPI specification
Section titled “OpenAPI specification”The full API surface is described as an OpenAPI 3.1 document:
GET https://onlynk.me/api/public/openapi.jsonFeed it to your API client, code generator or AI agent. It is the machine-readable twin of this page.
Rate limits
Section titled “Rate limits”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/endDaterange to request only the data you need
GET /api/public/pages
Section titled “GET /api/public/pages”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/pagesHeaders
Authorization: Bearer YOUR_API_KEYResponse
{ "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
| Field | Type | Description |
|---|---|---|
personal | array | Pages owned by your personal account |
organizations | array | Groups of pages by organization |
organizations[].organization.id | string | Organization ID |
organizations[].organization.name | string | Organization display name |
pages[].id | string | Internal page ID |
pages[].slug | string | Public URL handle, use this for the stats endpoint |
pages[].title | string | null | Page display title |
pages[].published | boolean | Whether the page is publicly accessible |
pages[].note | string | null | Private note attached to the page |
pages[].createdAt / pages[].updatedAt | ISO date-time | Creation / 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
curl -sS \ -H "Authorization: Bearer YOUR_API_KEY" \ "https://onlynk.me/api/public/pages"POST /api/public/pages
Section titled “POST /api/public/pages”Creates a published page on the given handle.
URL
POST https://onlynk.me/api/public/pagesHeaders
Authorization: Bearer YOUR_API_KEYContent-Type: application/jsonBody
| Field | Type | Required | Description |
|---|---|---|---|
slug | string | ✓ | Public handle, 3-32 chars, letters/numbers/_/., cannot start or end with a period. Stored lowercased. |
title | string | - | Display title (max 128 chars) |
organizationId | string | - | Create the page inside an organization you have API access to |
links | array | - | Initial links, max 50. Each: { title, url, order?, icon?, iconUrl?, effect?, adult?, cloak? }, same fields as POST /pages/{slug}/links. |
socials | array | - | 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
| Status | code | Cause |
|---|---|---|
402 | plan_limit_reached | The workspace owner’s plan link limit is reached |
403 | forbidden | Your plan doesn’t include API access, or no API access to that organization |
409 | slug_reserved | The handle collides with a reserved word or system route |
409 | slug_taken | The handle is already used by a page or smart link |
422 | invalid_request | Body validation failed (the response includes Zod issues) |
Example
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"GET /api/public/pages/{slug}
Section titled “GET /api/public/pages/{slug}”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.
PATCH /api/public/pages/{slug}
Section titled “PATCH /api/public/pages/{slug}”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_KEYContent-Type: application/jsonBody (every field is optional but at least one is required; null clears a nullable field)
| Field | Type | Description |
|---|---|---|
title | string | null | Display title (max 128 chars) |
note | string | null | Private dashboard note (max 32 chars) |
published | boolean | Publish or unpublish the page |
description | string | null | Bio text (max 256 chars) |
picture / banner | string | null | Profile picture / banner image URL. Upload bytes first with POST /pages/{slug}/images. |
backgroundColor / textColor / buttonsColor / buttonsTextColor / cardBackgroundColor | string | null | Hex colors |
backgroundGradient | string | null | CSS gradient |
buttonShape | PILL | ROUNDED | SQUARE | Button shape |
buttonFill | SOLID | OUTLINE | SOFT_SHADOW | Button fill style |
fontFamily | string | null | Font family |
style | string | Page theme preset |
directLink | string | null | Skip 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 / chatbotLink | array / string | null | Simulated chatbot bubble content and its link |
metaPixelId / metaPixelEnabled | string | null / boolean | Meta 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)
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"POST /api/public/pages/{slug}/images
Section titled “POST /api/public/pages/{slug}/images”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}/imagesHeaders
Authorization: Bearer YOUR_API_KEYContent-Type: multipart/form-dataBody
| Field | Type | Required | Description |
|---|---|---|---|
file | file | ✓ | The image. Max 5MB. Accepted formats: jpeg, png, webp, heic, heif. |
purpose | string | - | 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
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"# Then attach it to the pagecurl -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"Page content: links, cards, sections, socials
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 orderPOST /api/public/pages/{slug}/{resource} create one, appended at the end unless order is givenPATCH /api/public/pages/{slug}/{resource}/{id} update one (partial, at least one field)DELETE /api/public/pages/{slug}/{resource}/{id} delete onewhere {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}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.
sections
Section titled “sections”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).
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"socials
Section titled “socials”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.
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"GET /api/public/stats/{slug}
Section titled “GET /api/public/stats/{slug}”Fetches analytics for a specific page identified by its slug.
URL
GET https://onlynk.me/api/public/stats/{slug}Headers
Authorization: Bearer YOUR_API_KEYQuery parameters
| Parameter | Type | Required | Description |
|---|---|---|---|
startDate | YYYY-MM-DD | - | Start of the date range (inclusive). Defaults to 30 days ago. |
endDate | YYYY-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
| Field | Type | Description |
|---|---|---|
totalViews | number | Total PAGE_VIEW events in the range |
totalClicks | number | LINK_CLICK + CARD_CLICK events |
totalDirect | number | REDIRECT events (direct link redirects) |
totalChatbotClicks | number | CHATBOT_CLICK events |
totalSafed | number | SAFE events, bots served the landing page instead of being redirected |
totalBlocked | number | BLOCK events, visitors blocked by geo rules |
topCountries | array | Top 5 countries by views, { country: "ISO-2", views: N } |
topLinks | array | Top 5 clicked link URLs, { url: string, clicks: N } |
topReferrers | array | Top 5 traffic sources, { referrer: string, views: N }. "direct" means no referrer header. |
topDeviceTypes | array | Top 5 device categories, { deviceType: string, views: N } |
dailyStats | array | Per-day breakdown { date: "YYYY-MM-DD", views: N, clicks: N }, sorted ascending |
Example (custom date range)
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
OWNERorADMIN
If neither condition is true, the endpoint returns 403 Forbidden.
GET /api/public/smart-links
Section titled “GET /api/public/smart-links”Lists all smart links accessible to your API key, grouped like the pages endpoint.
URL
GET https://onlynk.me/api/public/smart-linksHeaders
Authorization: Bearer YOUR_API_KEYResponse
{ "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": [] } ]}| Field | Type | Description |
|---|---|---|
personal | array | Smart links owned by your personal account |
organizations | array | Groups of smart links by organization |
smartLinks[].id | string | Internal smart link ID |
smartLinks[].slug | string | Public handle, the link resolves at https://onlynk.me/{slug} |
smartLinks[].title | string | Display name (dashboard only) |
smartLinks[].enabled | boolean | Disabled links resolve to their fallback |
smartLinks[].fallbackUrl | string | Destination used when the flow can’t produce an action |
smartLinks[].note | string | null | Private note attached to the link |
smartLinks[].createdAt / smartLinks[].updatedAt | ISO date-time | Creation / last update timestamps |
POST /api/public/smart-links
Section titled “POST /api/public/smart-links”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-linksHeaders
Authorization: Bearer YOUR_API_KEYContent-Type: application/jsonBody
| Field | Type | Required | Description |
|---|---|---|---|
title | string | ✓ | Display name (1-120 chars, dashboard only) |
slug | string | ✓ | Public handle, same rules as page creation. Stored lowercased. |
fallbackUrl | string | ✓ | http(s) URL served when no flow matches (or none exists) |
organizationId | string | - | Create the link inside an organization you have API access to |
flow | object | - | 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
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"GET /api/public/smart-links/{slug}
Section titled “GET /api/public/smart-links/{slug}”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.
PATCH /api/public/smart-links/{slug}
Section titled “PATCH /api/public/smart-links/{slug}”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)
| Field | Type | Description |
|---|---|---|
title | string | Display name (1-120 chars) |
fallbackUrl | string | http(s) fallback destination |
note | string | null | Private dashboard note (max 32 chars). null or "" clears it. |
enabled | boolean | false pauses the link (visits resolve to the fallback) |
flow | object | null | Serialized 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)
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}/duplicateResponse (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
curl -sS -X POST \ -H "Authorization: Bearer YOUR_API_KEY" \ "https://onlynk.me/api/public/smart-links/promo/duplicate"GET /api/public/smart-links/{slug}/stats
Section titled “GET /api/public/smart-links/{slug}/stats”Fetches cookieless visit analytics for a smart link.
URL
GET https://onlynk.me/api/public/smart-links/{slug}/statsQuery 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 }]}| Field | Type | Description |
|---|---|---|
total | number | Total visits in the range |
uniqueVisitors | number | Distinct visitors (IP-based, cookieless) |
growthVisits / growthUnique | number | null | Percent change vs the previous period of the same length |
byAction | array | Visits by terminal action (redirect, landing, deeplink, …) |
topDestinations | array | Most frequent resolved destinations |
Access rules: same as page stats (author, org owner, or org OWNER/ADMIN).
GET /api/public/slug-availability
Section titled “GET /api/public/slug-availability”Checks whether a handle can be claimed. No authentication required.
URL
GET https://onlynk.me/api/public/slug-availability?slug=my_handleResponse
{ "available": false, "reason": "taken" }reason | Meaning |
|---|---|
invalid | The slug doesn’t match the handle format rules |
reserved | The slug collides with a reserved word or system route |
taken | The 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.
Error responses
Section titled “Error responses”Every error body contains a human-readable error message and a stable machine-readable code:
{ "error": "Username already exists", "code": "slug_taken" }| Status | code | Cause |
|---|---|---|
400 | invalid_date_range | startDate/endDate is not a valid date |
401 | api_key_required | No Authorization header |
403 | invalid_api_key | Key doesn’t match any account |
403 | forbidden | Key is valid but you don’t have access to this resource |
404 | not_found | No page, smart link, or content item (link/card/section/social) with that id exists |
402 | plan_limit_reached | Plan link limit reached (page and smart link creation share one budget) |
409 | slug_reserved / slug_taken | Handle unavailable (page or smart link creation) |
422 | invalid_request | Body validation failed |
422 | content_limit_reached | Page content cap reached (50 links, 50 cards, 20 sections, or 50 socials) |
429 | - | Rate limit exceeded; back off and retry |
Related
Section titled “Related”- API Authentication: How to generate and use your API key
- Page analytics: Understanding the same metrics in the dashboard
- Smart Links: What smart links are and how flows work
- Glossary: Event type definitions (PAGE_VIEW, LINK_CLICK, REDIRECT, etc.)