Skip to content
CountJar
Docs Pricing The counter API for developers A hit counter for any website A view counter badge for your GitHub README An embeddable like button A yes/no poll you can embed anywhere Five-star ratings for any page Start free

Code examples

Copy-paste examples for the most common jobs. Each one is complete and works as written once you swap in your workspace, counter and token.

Before you start

The examples use a workspace called acme with two counters:

  • visits, a public counter for page views, counted from the browser with no token;
  • signups, a private counter counted from a server with a write token.

Server examples read the token from an environment variable called COUNTJAR_TOKEN, so it never ends up in your code:

export COUNTJAR_TOKEN=cj_your_token

Count from your server

Count a sign-up, tag it with the plan, and print the new total. Extra query parameters like plan are saved with the event as custom properties.

curl -X POST "https://countjar.vercel.app/api/v2/acme/signups/up?plan=pro" \
  -H "Authorization: Bearer $COUNTJAR_TOKEN"
// Node.js 18 or later has fetch built in.
const res = await fetch("https://countjar.vercel.app/api/v2/acme/signups/up?plan=pro", {
  method: "POST",
  headers: { Authorization: `Bearer ${process.env.COUNTJAR_TOKEN}` },
});
const data = await res.json();
if (!res.ok) throw new Error(`CountJar ${data.error.code}: ${data.error.message}`);
console.log(`Sign-ups so far: ${data.count}`);
import os

import requests

resp = requests.post(
    "https://countjar.vercel.app/api/v2/acme/signups/up",
    params={"plan": "pro"},
    headers={"Authorization": f"Bearer {os.environ['COUNTJAR_TOKEN']}"},
    timeout=5,
)
data = resp.json()
if not resp.ok:
    raise RuntimeError(f"CountJar {data['error']['code']}: {data['error']['message']}")
print("Sign-ups so far:", data["count"])
package main

import (
	"encoding/json"
	"fmt"
	"log"
	"net/http"
	"os"
)

func main() {
	req, err := http.NewRequest("POST", "https://countjar.vercel.app/api/v2/acme/signups/up?plan=pro", nil)
	if err != nil {
		log.Fatal(err)
	}
	req.Header.Set("Authorization", "Bearer "+os.Getenv("COUNTJAR_TOKEN"))

	resp, err := http.DefaultClient.Do(req)
	if err != nil {
		log.Fatal(err)
	}
	defer resp.Body.Close()

	var body struct {
		Count int64 `json:"count"`
		Error struct {
			Code    string `json:"code"`
			Message string `json:"message"`
		} `json:"error"`
	}
	if err := json.NewDecoder(resp.Body).Decode(&body); err != nil {
		log.Fatal(err)
	}
	if resp.StatusCode >= 400 {
		log.Fatalf("CountJar %s: %s", body.Error.Code, body.Error.Message)
	}
	fmt.Println("Sign-ups so far:", body.Count)
}
<?php
$ch = curl_init("https://countjar.vercel.app/api/v2/acme/signups/up?plan=pro");
curl_setopt_array($ch, [
    CURLOPT_POST => true,
    CURLOPT_RETURNTRANSFER => true,
    CURLOPT_TIMEOUT => 5,
    CURLOPT_HTTPHEADER => ["Authorization: Bearer " . getenv("COUNTJAR_TOKEN")],
]);
$data = json_decode(curl_exec($ch), true);
$status = curl_getinfo($ch, CURLINFO_RESPONSE_CODE);
curl_close($ch);

if ($status >= 400) {
    throw new RuntimeException("CountJar {$data['error']['code']}: {$data['error']['message']}");
}
echo "Sign-ups so far: {$data['count']}\n";
Counting is quick, but it's still a network call. If a count isn't critical to the request, run it after you've responded or in a background job, and don't fail the user's action when CountJar is unreachable.

Count on a web page

Browser code can be read by anyone, so it must never contain a token. Use a public counter: anyone can count it up by one, and nobody can subtract, set or reset it without a token.

If you only want to show the number, the widget does it in two lines. To use the number in your own design:

<p>This page has been viewed <span id="views">…</span> times.</p>

<script>
  fetch("https://countjar.vercel.app/api/v2/acme/visits/up", { method: "POST" })
    .then((res) => res.json())
    .then((data) => {
      document.getElementById("views").textContent = data.count.toLocaleString();
    })
    .catch(() => {
      document.getElementById("views").textContent = "many";
    });
</script>
import { useEffect, useState } from "react";

export function ViewCount({ workspace = "acme", counter = "visits" }) {
  const [count, setCount] = useState(null);

  useEffect(() => {
    const ctrl = new AbortController();
    fetch(`https://countjar.vercel.app/api/v2/${workspace}/${counter}/up`, { method: "POST", signal: ctrl.signal })
      .then((res) => res.json())
      .then((data) => setCount(data.count))
      .catch(() => {});
    return () => ctrl.abort();
  }, [workspace, counter]);

  return <span>{count === null ? "…" : count.toLocaleString()} views</span>;
}
In development, React's Strict Mode runs effects twice, so you'll see two counts per page load. Production builds count once. Turning on unique counting also stops repeat counts from the same visitor.

To read a number without counting, call the counter's URL without /up.

Next.js

With the App Router you can count on the server, where your token is safe. This server action counts a sign-up after it's saved:

// app/signup/actions.ts
"use server";

export async function signUp(formData: FormData) {
  // ... create the user ...

  await fetch("https://countjar.vercel.app/api/v2/acme/signups/up?plan=" + encodeURIComponent(String(formData.get("plan"))), {
    method: "POST",
    headers: { Authorization: `Bearer ${process.env.COUNTJAR_TOKEN}` },
    cache: "no-store",
  }).catch(() => {}); // never block a sign-up on a counter
}

And this server component shows a public total, refreshed at most once a minute:

// app/components/download-count.tsx
export async function DownloadCount() {
  const res = await fetch("https://countjar.vercel.app/api/v2/acme/downloads", { next: { revalidate: 60 } });
  if (!res.ok) return null;
  const { count } = await res.json();
  return <p>Downloaded {count.toLocaleString()} times</p>;
}

Add COUNTJAR_TOKEN to your .env.local file and to your host's environment settings. Don't prefix it with NEXT_PUBLIC_, or Next.js will send it to the browser.

Read a number

Reading is a GET on the counter's URL. Add ?window=24h to also get recent activity:

import requests

data = requests.get("https://countjar.vercel.app/api/v2/acme/visits", params={"window": "24h"}, timeout=5).json()
print(f"{data['count']:,} views, {data['window']['count']:,} in the last day")
print(f"{data['window']['unique']:,} different visitors in the last day")

Private counters need a read token in the Authorization header, just like the server examples above. For a fuller summary use /stats, and for chart data use /history.

Handle errors and limits

Failed requests return a JSON error with a stable code you can check. When you're rate limited, the Retry-After header says how many seconds to wait:

async function countUp(path, token, attempts = 3) {
  for (let i = 1; i <= attempts; i++) {
    const res = await fetch(`https://countjar.vercel.app/api/v2/${path}/up`, {
      method: "POST",
      headers: token ? { Authorization: `Bearer ${token}` } : {},
    });
    if (res.status === 429 && i < attempts) {
      const wait = Number(res.headers.get("Retry-After") || 1);
      await new Promise((r) => setTimeout(r, wait * 1000));
      continue;
    }
    const data = await res.json();
    if (!res.ok) throw new Error(`${data.error.code}: ${data.error.message}`);
    return data;
  }
}

const { count } = await countUp("acme/signups", process.env.COUNTJAR_TOKEN);

Only retry 429 and network errors. Other errors, like 401 for a missing token or 404 for a misspelt counter, will fail the same way every time. Limits and errors lists every code.

Export data

History and events can come back as CSV, ready for a spreadsheet:

# Daily totals for the last 90 days
curl -H "Authorization: Bearer $COUNTJAR_TOKEN" \
  "https://countjar.vercel.app/api/v2/acme/visits/history?range=90d&format=csv" -o visits-90d.csv

# The last 1,000 events tagged with plan=pro
curl -H "Authorization: Bearer $COUNTJAR_TOKEN" \
  "https://countjar.vercel.app/api/v2/acme/signups/events?limit=1000&filter=plan=pro&format=csv" -o pro-signups.csv

You can also download both from the counter's page in the dashboard.