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 awritetoken.
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";
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>;
}
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.