# CountWatch recipes

One page per source. Each ends with the same three checks.

- [Stripe payments (webhook URL)](https://staging.countwatch.com/docs/recipes/stripe.md): Count successful charges with a Stripe webhook to a webhook URL; import 89 days of charges in 5-minute buckets.
- [Supabase orders (pg_cron + pg_net)](https://staging.countwatch.com/docs/recipes/supabase.md): A scheduled count query in the database sends 5-minute order counts every hour; the same count, sent from the database too, imports the last 89 days.
- [GitHub activity (webhook URL)](https://staging.countwatch.com/docs/recipes/github.md): Count pushes, pull requests or deployments with a repository webhook to a webhook URL.
- [Count in your app (Node or Python)](https://staging.countwatch.com/docs/recipes/app-counter.md): A copy-paste counter: count events in the process, send every 5-minute bucket with zeros once an hour.
- [n8n workflows (webhook URL)](https://staging.countwatch.com/docs/recipes/n8n.md): Count a workflow's items or runs with one HTTP Request node that calls a webhook URL.

## Not verified yet

These are not run end to end in CI yet; follow them with care.

- [Website traffic (Cloudflare bridge)](https://staging.countwatch.com/docs/recipes/cloudflare-traffic.md): A small Worker in your own Cloudflare account reads the zone's visitor traffic every hour and sends it; the first run imports 30 days.
- [Shopify orders (webhook URL)](https://staging.countwatch.com/docs/recipes/shopify.md): Count new orders with a Shopify webhook to a webhook URL; import past orders from the orders export.
- [Zapier and Make (webhook URL)](https://staging.countwatch.com/docs/recipes/zapier-make.md): Count a Zap's or scenario's runs with one HTTP step that calls a webhook URL.
- [Watch a Tinybird endpoint](https://staging.countwatch.com/docs/methods/tinybird.md): Publish the 5-minute count query as a Tinybird endpoint; a cron job reads it and sends it; a second endpoint imports 89 days.

## No recipe for your source

Use a recipe when one fits. Otherwise use one of these general methods: every source fits one, and none needs a new endpoint.

| Your source | Method |
|---|---|
| A count you can query: a SQL database (Postgres, MySQL, MariaDB), a warehouse or log store (ClickHouse, Tinybird), another service's API | **Scheduled query → ingest.** A cron job or a CI schedule runs every 60 minutes (Free) or every 5 minutes (Pro). It counts per 5-minute bucket over the last 2 hours, **every bucket, zeros included** (`generate_series` in SQL, `WITH FILL` in ClickHouse), up to the last complete bucket, and sends them with `set` to an **explicit** watch: a re-send replaces a bucket (never doubles it), late rows are picked up, and a stopped job raises the no-data alert. Written out and run in CI: [Watch a ClickHouse query](https://staging.countwatch.com/docs/methods/clickhouse.md). |
| Your own code sees each event (an app, a worker, a job) | **Client library:** `npm install countwatch` or `pip install countwatch`, then `cw.count("orders")`. It sums per 5 minutes, sends a 0 for each empty 5-minute bucket of every metric it has seen (an explicit watch: a stopped process raises the no-data alert), sends at the account's interval (`send_interval_seconds` in `GET /v1/status`, read again after each send) and retries without double counts. Declare metrics at start (`metrics`) to get zeros before their first event. Serverless (no timer): call `flush()` before returning; the watch is then a push watch. |
| A tool that sends webhooks or can call a URL (forms, no-code tools, CI) | **Webhook URL:** made in the dashboard (https://staging.countwatch.com/dashboard) and pasted into the tool; each hit adds 1 (or `?n=`); a delivery id (`?id=` or the sender's delivery header) counts a retry once. It makes a **push** watch: the link is called as things happen, so a bucket with no hits is zero. |
| A shell script or cron job that finishes something often (every few minutes or more) | **`npx countwatch send <metric> [n] --dim key=value`** at the end of the script (a push watch). A push watch never raises a no-data alert, so for a job that runs rarely, count with a scheduled query instead. |
| Past data: the scheduled query over the last 89 days, or an export file (CSV) | **Import once:** count per 5 minutes, every bucket (an empty one is a `0`), with UTC bucket starts, and send it with `POST /v1/import` (`resolution: "5m"`; the API takes up to 90 days back, and the examples take 89 days so the first bucket is still inside the 90 days when the request arrives), ending where the scheduled sends begin. The API stops an import at a watch's first live bucket, so it never overwrites live counts. `"1h"` or `"1d"` only for a source that keeps nothing finer (they are spread evenly over 5-minute buckets). |

Rules for any method:

- Create the watch first, with its final source (it cannot change): an explicit watch by its first `set` points, a push watch by one point `{"metric": "…", "add": 0}` with `"source": "push"`.
- One metric per thing counted (`orders`, `signups`); dimensions only for a few values each (every combination is a watch, within the plan's watch limit).
- Batch scheduled sends: one request carries up to 10,000 points.
- Keys and webhook URLs are secrets, made in the dashboard (https://staging.countwatch.com/dashboard) and pasted into the host's secrets: a write key (`COUNTWATCH_API_KEY`) goes into senders, a webhook URL (`COUNTWATCH_PING_URL`) into tools that call a URL. Code reads those names; an agent never handles the values.
- Then the same checks as every recipe: `GET /v1/status` (an explicit watch's `missing_buckets_24h` stays 0), the import's `status: done`, a test alert.

The scheduled query's request (`ts` is each bucket's start, ISO 8601 or Unix milliseconds, at most 6 hours back):

```
curl -sX POST https://staging.countwatch.com/v1/ingest -H "Authorization: Bearer $COUNTWATCH_API_KEY" -H "Content-Type: application/json" \
  -d '{"points":[{"metric":"orders","ts":"<bucket start>","set":7},{"metric":"orders","ts":"<next bucket start>","set":0}]}'
```
