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 under Senders (Connect) and add it in Stripe for successful payments; each payment counts one. Import the last 89 days to start alerts today.
The steps below work for you or your coding agent. Your agent does every step that needs no secret. The key comes from npx countwatch init (you approve it); a Webhook URL you make under Senders (Connect), or approve for your agent.
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://countwatch.com/login) and a Webhook URL the person makes in the dashboard (Senders (Connect), https://countwatch.com/dashboard/connections?tab=senders): it is a secret for one metric, so the agent gets it only through MCP webhook_url, after the person approves, to paste into the tool itself.
1. The Webhook URL and the watch
The agent creates the watch as a push watch first (a 5-minute window without hits is zero; a watch's source is fixed by its first count; until then it waits for that count): over MCP, manage {"action": "create_watch", "body": {"metric": "payments", "source": "push", "recipe": "stripe"}}; with an API key:
curl -sX POST https://countwatch.com/v1/ingest -H "Authorization: Bearer $COUNTWATCH_API_KEY" -H "Content-Type: application/json" \
-d '{"source":"push","recipe":"stripe","points":[{"metric":"payments","add":0}]}'
The Webhook URL for payments is a secret for this one metric. The person makes it in the dashboard (Senders (Connect), https://countwatch.com/dashboard/connections?tab=senders) and pastes it into the tool. When the agent sets the webhook up in the tool itself (through the tool's own MCP server or API), it may get the URL instead with MCP webhook_url {"action": "make_webhook_url", "metric": "payments"}, which the person approves first, and paste it only into that tool: never into code, a repository, chat or logs. API keys and signing secrets stay the person's.
Query parameters on the link become tags (?repo=api is the tag repo:api): the watch here must have the same ones as the link's hits (each combination of tag values 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 (the dashboard's New watch shows the same steps):
- In Stripe, go to Developers → Webhooks → Add endpoint.
- Paste the Webhook URL as the endpoint URL.
- Choose the event
charge.succeeded, then press Add endpoint.
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 window 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.
// 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 = { recipe: "stripe", watches: [{ metric: "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://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 tags, like the Webhook URL's hits, so both fill the same watch.
Check it works
- Data arrives.
check_setup(orcurl -s https://countwatch.com/v1/status -H "Authorization: Bearer $COUNTWATCH_API_KEY"): thepaymentswatch showslast_request_at, andmissing_buckets_24hstays0(explicit watches). Do whatnext_actionssays. - History is in.
GET /v1/imports/{import_id}showsstatus: doneand the rules chosen forpayments, with the alerts they would have raised. (Skipped the import? The watch learns from live data; starter rules apply meanwhile.) - An alert reaches you. The person turns on e-mail alerts in the dashboard (Destinations (Connect), https://countwatch.com/dashboard/connections?tab=destinations), or you add a webhook (
manage create_destination, thenverify_destination; the endpoint answers the challenge, see https://countwatch.com/agent). A write key cannot read alerts: read them with MCPalerts, or withGET /v1/alertsand an admin key. Thenmanage {"action": "send_test_alert"}: every active destination receives a test alert (test: true) within a few minutes. Real alerts go only where the watch's group chose (Alerts go to, ormanage update_groupwithdestination_ids); until then they stay in the app.
For agents: this page as Markdown.