Any language

There’s no SDK to wait for. Sending events is one HTTP POST with a JSON body, so any language with an HTTP client works today.

Updated

On PHP or Laravel, the PHP SDK, and on Node.js the Node SDK, handle batching and retries for you. Everywhere else, this page has what you need.

The whole contract

  • POST https://app.clickclacks.io/api/v1/batch
  • Authorization: Bearer $CLICKCLACKS_SERVER_KEY, a server key, from your server only.
  • Content-Type: application/json and a body of { "items": [...] }, up to 500 items and 1 MiB.
  • Give every item an insert_id, and reuse it when you retry.
  • Retry network errors, 408, 429 and 5xx with backoff, honouring Retry-After. Don’t retry other 4xx.

The item fields, limits and every error code are in Send events.

Send an event

The shortest version, fine for a script or a first test:

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}}]}'

With safe retries

For production. Each loop builds the body once, so every retry carries the same insert_ids and nothing is counted twice. It backs off exponentially with jitter (up to 30 seconds), honours Retry-After (up to 5 minutes), gives up after 6 retries, and never retries a request the API refused on its merits.

send.py
import os, random, time, uuid, requests

def send(items, attempts=6):
    body = {"items": items}  # each item already has its insert_id
    for attempt in range(attempts + 1):
        try:
            resp = requests.post(
                "https://app.clickclacks.io/api/v1/batch",
                headers={"Authorization": f"Bearer {os.environ['CLICKCLACKS_SERVER_KEY']}"},
                json=body, timeout=10,
            )
        except requests.RequestException:
            resp = None
        if resp is not None and resp.status_code < 500 and resp.status_code not in (408, 429):
            resp.raise_for_status()   # other 4xx: fix the request, don't retry
            return resp.json()        # 202: check resp.json()["errors"] for refused items
        if attempt == attempts:
            raise RuntimeError("ClickClacks: gave up after retries")
        retry_after = resp.headers.get("Retry-After") if resp is not None else None
        delay = float(retry_after) if retry_after else random.uniform(0, min(30, 0.5 * 2 ** attempt))
        time.sleep(min(delay, 300))

send([{"event": "Invoice paid", "distinct_id": "user_8412",
       "insert_id": uuid.uuid4().hex, "properties": {"amount_cents": 4900}}])

A 202 can still list refused items in errors. Log them with their index and code; retrying them unchanged won’t help.

Try it without storing

Add ?validate=true and the API answers 200 with each item exactly as it would be stored, plus every error, and stores nothing. Handy while you write the integration, and in CI. ?strict=true refuses the whole batch if any item is invalid.

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

Large batches

Batch up to 500 items per request: the per-project rate limits count events in blocks of 100, so single-event requests run out far sooner. Compress big bodies with gzip; the 1 MiB limit applies after decompression.

terminal
gzip -c batch.json > batch.json.gz

curl https://app.clickclacks.io/api/v1/batch \
  -H "Authorization: Bearer $CLICKCLACKS_SERVER_KEY" \
  -H "Content-Type: application/json" \
  -H "Content-Encoding: gzip" \
  --data-binary @batch.json.gz

Next steps