Docs / Connect your data
Stripe
Get an alert when payments spike, drop or stop. Stripe sends each payment to CountWatch; nothing runs on your side.
In short: make a webhook URL in the dashboard (Sources), add it in Stripe as a webhook for successful payments, and each payment counts one. If you like, bring in the last 89 days of payments so alerts start today.
The detailed steps below work for you or your coding agent. Your agent can do every step that needs no secret; anything secret, such as a webhook URL or a key, you make in the dashboard and paste yourself.
Counts successful Stripe charges per 5 minutes and alerts when payments spike, drop or stop. No code runs on your side: Stripe calls a webhook URL. Needs a Stripe secret key (STRIPE_SECRET_KEY, test or live) and a CountWatch account (the person signs up at https://staging.countwatch.com/login) and a webhook URL the person makes in the dashboard (Sources, https://staging.countwatch.com/dashboard/sources): it is a secret, so the agent never handles it.
1. The webhook URL and the watch
The person makes the webhook URL for stripe_payments in the dashboard (Sources, https://staging.countwatch.com/dashboard/sources) and keeps it: it is a secret for this one metric, so the agent never sees it. The agent creates the watch as a push watch first (a 5-minute bucket without hits is zero; a watch's source is fixed when it is created): over MCP, send_counts {"source": "push", "points": [{"metric": "stripe_payments", "add": 0}]}; with an API key:
curl -sX POST https://staging.countwatch.com/v1/ingest -H "Authorization: Bearer $COUNTWATCH_API_KEY" -H "Content-Type: application/json" \
-d '{"source":"push","points":[{"metric":"stripe_payments","add":0}]}'
Query parameters on the link become dimensions: the watch here must have the same ones as the link's hits (each dimension combination is a watch: mind the plan's watch limit).
An import first would make it an explicit watch, which ignores the link's hits.
2. Point a Stripe webhook at it
The person adds it in Stripe: Developers → Webhooks → Add endpoint, the webhook URL from the dashboard as the URL, the event charge.succeeded. Or the person runs this with their Stripe key:
# For a scheduler or a person at a terminal, never an agent's shell: it holds a source secret and calls CountWatch.
curl -s https://api.stripe.com/v1/webhook_endpoints -u "$STRIPE_SECRET_KEY:" \
-d url="<webhook url>" -d "enabled_events[]=charge.succeeded" -d description="CountWatch"
Each charge.succeeded event adds 1. Stripe retries a failed delivery for up to 3 days; the webhook URL counts each event once (it reads the event id), so retries do not inflate the count. Use payment_intent.succeeded or checkout.session.completed instead if that is what "a sale" means here; pick one event type per metric. The watch reads a 5-minute bucket without events as zero (source push), so a stop in payments is a drop.
3. Import 89 days of charges
Two steps, so the Stripe key and the CountWatch key never meet. First the person counts past charges per 5 minutes with their key (Node 18+, no packages): every 5 minutes, zeros included, in UTC (Stripe's created is Unix time). The script writes the counts to stripe-counts.json and sends nothing. An agent saves it as stripe-import.mjs and lists the person's step in countwatch/NEXT.md: in that folder, read -s STRIPE_SECRET_KEY && export STRIPE_SECRET_KEY && node stripe-import.mjs (the key is pasted at a hidden prompt, so it stays out of the shell history). Without it the watch learns from live charges (about 3 weeks).
// Run by the person: it reads STRIPE_SECRET_KEY, writes stripe-counts.json (counts only) and calls nothing but Stripe.
import { writeFileSync } from "node:fs";
const since = Math.floor(Date.now() / 1000) - 89 * 86400;
const slot = (t) => Math.floor(t / 300); // 5-minute buckets, from Unix seconds
const counts = new Map();
let after;
for (;;) {
const q = new URLSearchParams({ limit: "100", "created[gte]": String(since) });
if (after) q.set("starting_after", after);
const r = await fetch("https://api.stripe.com/v1/charges?" + q, { headers: { authorization: "Bearer " + process.env.STRIPE_SECRET_KEY } });
const page = await r.json();
if (!r.ok) throw new Error("Stripe " + r.status + ": " + (page.error?.message ?? "error")); // e.g. a key without charges:read
for (const c of page.data) if (c.status === "succeeded") counts.set(slot(c.created), (counts.get(slot(c.created)) ?? 0) + 1);
if (!page.has_more) break;
after = page.data.at(-1).id;
}
// Up to the last complete 5 minutes: the API stops the import at the webhook URL's first count, so it never overwrites
// live counts.
const first = slot(since) + 1, last = slot(Date.now() / 1000) - 1;
const body = { watches: [{ metric: "stripe_payments", resolution: "5m", start: first * 300000,
counts: Array.from({ length: last - first + 1 }, (_, i) => counts.get(first + i) ?? 0) }] };
writeFileSync("stripe-counts.json", JSON.stringify(body));
console.log("stripe-counts.json: " + body.watches[0].counts.length + " buckets");
Then the agent sends its counts with import_history over MCP; without MCP, the person sends the file from the same folder with a write key:
curl -sS --fail-with-body -X POST https://staging.countwatch.com/v1/import -H "Authorization: Bearer $COUNTWATCH_API_KEY" -H "Content-Type: application/json" --data-binary @stripe-counts.json -w '\n%{http_code}\n'
It answers 202 and an import_id (the last line is the status).
The watch has no dimensions, like the webhook URL's hits, so both fill the same watch.
Check it works
- Data arrives.
check_setup(orcurl -s https://staging.countwatch.com/v1/status -H "Authorization: Bearer $COUNTWATCH_API_KEY"): thestripe_paymentswatch showslast_request_at, andmissing_buckets_24hstays0(explicit watches). Do whatnext_actionssays. - History is in.
GET /v1/imports/{import_id}showsstatus: doneand the rules chosen forstripe_payments, with the alerts they would have raised. (Skipped the import? The watch learns for about 3 weeks; starter rules apply meanwhile.) - An alert reaches you. The person turns on e-mail alerts in the dashboard (https://staging.countwatch.com/dashboard), or you add a webhook (
manage add_destination, thenverify_destination; the endpoint answers the challenge, see https://staging.countwatch.com/agent). Thenmanage {"action": "send_test_alert"}: every active destination receives a test alert (test: true) within a few minutes.
For agents: this page as Markdown.