# Endpoints reference

<div class="doc-callout doc-callout--info">
  <strong>Tip</strong>: Start by listing your pages with <code>GET /api/public/pages</code>, grab the <code>slug</code> you need, then fetch stats with <code>GET /api/public/stats/{"{slug}"}</code>.
</div>

## 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`/`endDate` range to request only the data you need

---

## `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/pages
```

**Headers**

```
Authorization: Bearer YOUR_API_KEY
```

**Response**

```json
{
  "personal": [
    {
      "id": "page_123",
      "slug": "my_page",
      "title": "My Page",
      "published": true
    }
  ],
  "organizations": [
    {
      "organization": { "id": "org_123", "name": "My Team" },
      "pages": [
        {
          "id": "page_456",
          "slug": "team_page",
          "title": "Team Page",
          "published": true
        }
      ]
    }
  ]
}
```

**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 | Page display title |
| `pages[].published` | boolean | Whether the page is publicly accessible |

**Example**

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

---

## `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_KEY
```

**Query 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**. If your use case requires local timezone alignment, shift the dates accordingly before passing them.

**Response**

```json
{
  "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 that were 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 (last 30 days, default)**

```bash
curl -sS \
  -H "Authorization: Bearer YOUR_API_KEY" \
  "https://onlynk.me/api/public/stats/your-slug"
```

**Example (custom date range)**

```bash
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`.

---

## Error responses

| Status | Body | Cause |
|---|---|---|
| `401` | `{ "error": "API key is required" }` | No `Authorization` header |
| `403` | `{ "error": "Invalid API key" }` | Key doesn't match any account |
| `403` | `{ "error": "Forbidden" }` | Key is valid but you don't have access to this page |
| `404` | `{ "error": "Page not found" }` | No page with that slug exists |
| `429` | `{ "error": "Rate limit exceeded" }` | Too many requests; back off and retry |

---

## Related

- **[API Authentication](https://docs.onlynk.me/api/authentication/)** — How to generate and use your API key
- **[Page analytics](https://docs.onlynk.me/page-analytics/)** — Understanding the same metrics in the dashboard
- **[Glossary](https://docs.onlynk.me/glossary/)** — Event type definitions (PAGE_VIEW, LINK_CLICK, REDIRECT, etc.)