# Server-side tracking

> Why and when to send events from your backend to ClickClacks, how server events join the same people as browser events, and how server keys work: creating, storing, rolling and revoking them.

- Canonical URL: https://clickclacks.io/docs/server-side
- Section: Server-side
- Last updated: 2026-09-26

Send events from your own servers: subscriptions, invoices, background jobs. Things a visitor can’t fake and an ad blocker can’t hide, in the same reports and on the same people as your browser events.

The server API is available on every project. This page covers the ideas and the keys; [Send events](https://clickclacks.io/docs/api.md) is the full guide to the endpoint, and the [canonical reference](https://app.clickclacks.io/docs/api) in the app lists every error code.

## Why server-side {#why}

- **Nothing to block.** Ad blockers and privacy extensions can stop a script in a browser, not a request between two servers.
- **Nothing to fake.** A server key is secret and never works from a browser, so events sent with it only come from your code.
- **No page needed.** Renewals, webhooks, refunds, background jobs and exports happen with no browser open.
- **The source of truth.** Revenue and plan changes come from the system that knows, not from a thank-you page that might not load.

Keep the [script tag](https://clickclacks.io/docs/install.md) for pageviews, clicks and scroll depth. The two work together: a funnel can start on your pricing page and end at “Invoice paid”.

## How it fits {#how-it-fits}

- **A server source** sits in your project next to your website and web app sources. Its events go into the same reports.
- **People are joined by your user ID.** Send the same ID from your server as `distinct_id` that the browser passes to `identify`, and both sides land on one person. [Identity from the server](https://clickclacks.io/docs/server-identity.md) explains the rules.
- **Sessions stay honest.** A server event isn’t part of a browser session unless you pass one, so it never inflates session counts.
- **It counts like any event** toward your monthly total. Identify calls are free.

## Add a server source {#add-source}

1. In the app, open **Sources** and add a source. Choose **Server**: “A secret key and the Node SDK or HTTP API.” (New projects can pick it on the welcome screen too.)
2. Name it after the system that sends, such as “Billing backend”.
3. Create its first key, and copy it somewhere safe. The source’s page then shows how to send an event.

## Server keys {#keys}

A server key (`cks_live_…`) belongs to one server source and can only send events to it. Server keys live on that source’s page, under **Secret keys**. (Settings › API access is for the other direction: [read-only keys](https://clickclacks.io/docs/mcp.md#keys) that let Claude, Codex or your scripts read your analytics.)

- **Shown once.** When you create a key you see the whole secret one time. Afterwards the app shows its first characters, its last four and when it was last used; only a hash is stored.
- **Store it as a secret,** for example in an environment variable called `CLICKCLACKS_SERVER_KEY`, and send it as `Authorization: Bearer cks_live_…`.
- **Two working keys per source,** so you can roll without downtime. **Roll key** creates a new one and keeps the old one working for the time you choose: stop it now, keep it for 24 hours, or keep it for 7 days.
- **Revoke** takes effect on the next request.
- **Never from a browser.** A request carrying an `Origin`, `Sec-Fetch-Site` or `Sec-Fetch-Mode` header is refused with `403 browser_not_allowed`, and the API sends no CORS headers. The source’s health panel warns you if that happens and links to **Roll key**.
- Older `sk_live_…` keys keep working. Rolling one creates a `cks_live_` key and leaves the old one working until you revoke it.

> **Keep the key on your server**
>
> Don’t put a server key in client-side code, a mobile app, a repository, logs or screenshots. If one leaks, roll it on the source’s page and choose to stop the old key now.

## Send your first event {#first-event}

The same snippets the app shows. Each reads the key from `CLICKCLACKS_SERVER_KEY`.

**Node**

```ts title="server.ts"
// npm install @clickclacks/node
import { ClickClacks } from '@clickclacks/node'

const clickclacks = new ClickClacks({
  key: process.env.CLICKCLACKS_SERVER_KEY,
})

clickclacks.track({
  event: 'Subscription started',
  distinctId: 'user_8412',
  properties: { plan: 'pro' },
})

process.on('SIGTERM', () => clickclacks.shutdown())
```

**curl**

```bash title="terminal"
curl https://app.clickclacks.io/api/v1/batch \
  -H "Authorization: Bearer $CLICKCLACKS_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -d '{"items":[{"event":"Invoice paid","distinct_id":"user_8412","insert_id":"inv_2291","properties":{"amount_cents":4900}}]}'
```

**Python**

```python title="send.py"
import os, uuid, requests

requests.post(
    "https://app.clickclacks.io/api/v1/batch",
    headers={"Authorization": f"Bearer {os.environ['CLICKCLACKS_SERVER_KEY']}"},
    json={"items": [{"event": "Invoice paid", "distinct_id": "user_8412",
                     "insert_id": uuid.uuid4().hex,
                     "properties": {"amount_cents": 4900}}]},
    timeout=10,
).raise_for_status()
```

**Go**

```go title="send.go"
body, _ := json.Marshal(map[string]any{"items": []map[string]any{{
    "event": "Invoice paid", "distinct_id": "user_8412",
    "insert_id": uuid.NewString(),
    "properties": map[string]any{"amount_cents": 4900},
}}})
req, _ := http.NewRequest("POST", "https://app.clickclacks.io/api/v1/batch", bytes.NewReader(body))
req.Header.Set("Authorization", "Bearer "+os.Getenv("CLICKCLACKS_SERVER_KEY"))
req.Header.Set("Content-Type", "application/json")
resp, err := http.DefaultClient.Do(req) // retry on 429/5xx, honouring Retry-After
```

A `202` response means it was accepted. Open **Realtime** or **Events** and filter to the server source to see it.

## SDK or HTTP? {#choose}

- **Node.js, including serverless and Cloudflare Workers:** use [@clickclacks/node](https://clickclacks.io/docs/node.md). It batches, compresses, retries and adds an `insert_id` for you.
- **Anything else:** one `POST` with JSON. [Any language](https://clickclacks.io/docs/http.md) has curl, Python, Go and Ruby examples with retries.

## Next steps {#next}

- [Send events](https://clickclacks.io/docs/api.md): items, batching, idempotency, limits and errors.
- [Identity from the server](https://clickclacks.io/docs/server-identity.md): identify from your backend and join browser history.
- [Limits](https://clickclacks.io/docs/limits.md): every size and rate in one table.
