// agents.sync_kpis

Sync KPIs

Four ways to land KPI period values in BoardHerald — webhook, Stripe forwarder, MCP cron, or CSV upload. Pick whichever matches the tool that already owns the data.

KPIs in BoardHerald are time-series — every period (monthly, quarterly, ad-hoc) is one row. The board sees the latest value, a sparkline of the last six periods, period-over-period delta, and an on-track / at-risk / off-track badge against the target window. Once the values are landing, the rest is automatic.

The five entry points below all upsert the same KpiPeriod row, so a value pushed via the built-in Stripe connector is indistinguishable from one entered by hand. Pick whichever fits the source system; mix and match freely.

1. Built-in Stripe connector (recommended for MRR)

The fastest path for revenue metrics. Connect once at Admin → Integrations → Connect Stripe (paste a restricted API key with Subscriptions: Read), then bind a KPI to a metric on the KPI's detail page. Three metrics ship today: active_subscription_mrr, active_subscriptions_count, and trialing_subscriptions_count. The daily KPI sync cron picks up the binding and refreshes the current period — no glue code on your side.

Use the "Sync now" button on the binding panel to verify the value before waiting for the cron tick.

2. Webhook (one period at a time)

The simplest entry point. POST a single value, identify the KPI either by kpiId or by (sourceSystem, externalId). Both fields can be set on the KPI from the "Link to external source" collapsible in the new-KPI form.

http
POST https://app.boardherald.com/api/kpis/webhook
Authorization: Bearer bh_REPLACE_WITH_YOUR_KEY
Content-Type: application/json

{
  "sourceSystem": "stripe",
  "externalId":   "metric_mrr",
  "periodStart":  "2026-04-01",
  "value":        42000,
  "note":         "April MRR snapshot"
}
http
200 OK
{
  "ok":          true,
  "kpiId":       "9b2e…",
  "periodId":    "f1a3…",
  "periodStart": "2026-04-01T00:00:00.000Z",
  "value":       "42000",
  "isNewLatest": true
}
Scope
The webhook key needs the kpis:webhook scope. Issue it from Profile → API Keys with only that scope ticked — narrower than financials:write, which would let the agent rewrite KPI metadata too.

Re-posting the same (kpi, periodStart) silently overwrites the value. That makes the endpoint idempotent for cron retries; for an audited correction (the value changed for a reason worth recording), use the kpis_restate MCP tool instead.

3. Custom Stripe forwarder (when the built-in metric isn't the one you want)

The built-in Stripe connector covers active subscription MRR + counts. If your definition of "MRR" is different (different currency conversion, plan exclusions, prorations) it's usually faster to fork the computation into your own webhook handler than to ask us for a config knob.

ts
// One-shot Node script you can wire to a Stripe webhook.
// Subscribe to invoice.paid + invoice.payment_failed in
// the Stripe dashboard, point them at your endpoint, and
// forward the running MRR total to BoardHerald.

import Stripe from "stripe";

export async function forwardMrrToBoardHerald(stripe: Stripe) {
  // Sum live subscriptions; this is the "today" snapshot.
  const subs = await stripe.subscriptions.list({
    status: "active",
    limit: 100,
  });
  const mrr = subs.data.reduce((sum, s) => {
    return sum + (s.items.data[0]?.price.unit_amount ?? 0);
  }, 0) / 100;

  await fetch("https://app.boardherald.com/api/kpis/webhook", {
    method: "POST",
    headers: {
      "Authorization": "Bearer " + process.env.BOARDHERALD_KEY,
      "Content-Type":  "application/json",
    },
    body: JSON.stringify({
      sourceSystem: "stripe",
      externalId:   "metric_mrr",
      periodStart:  new Date().toISOString().slice(0, 10) + "-01",
      value:        mrr,
    }),
  });
}

The KPI itself is created once in BoardHerald (Admin → KPIs → New KPI), with sourceSystem="stripe" and externalId="metric_mrr" set under "Link to external source". After that, every push lands without any manual mapping.

4. MCP cron job

For data that lives in another internal tool — a database, a Notion page, a custom dashboard — call the existing kpis_record_period MCP tool from a scheduled job. Same authentication path Claude Desktop uses; easier to reason about than a separate webhook because all KPI tools live under the same scope namespace.

bash
# Run this once a day (e.g. cron, GitHub Actions schedule,
# Railway scheduled job). Uses the MCP bridge to record a
# value via the same kpis_record_period tool admins use in
# the UI.

npx -y @boardherald/mcp-bridge \
  --api-key "$BOARDHERALD_KEY" \
  --url    "https://app.boardherald.com" \
  --tool   kpis_record_period \
  --json   '{
    "kpiId":       "9b2e…",
    "periodStart": "2026-04-01",
    "value":       42000,
    "note":        "Daily MRR roll-up"
  }'

Use a separate API key for the cron with only financials:write ticked. Rotating it doesn't touch your interactive Claude key.

5. CSV upload

For one-off backfills, period-over-period imports from a spreadsheet, or hand-curated history that predates the tooling — paste a CSV at Admin → KPIs → Import CSV. Same upsert semantics as the webhook (silent overwrite on collision); rows whose KPI name doesn't resolve are reported back per-row so you can fix and re-upload.

kpi-backfill.csvcsv
kpiName,periodStart,value,note
MRR,2026-01-01,42000,Q1 kickoff
MRR,2026-02-01,45500,
MRR,2026-03-01,48200,Pricing change shipped
Headcount,2026-01-01,18,Hired 2 engineers
Headcount,2026-02-01,19,
Runway months,2026-01-01,14,Updated cash forecast

The header row is required (kpiName, periodStart, value; optional note). KPI name must match exactly — the display-name override doesn't apply on import. Max 1,000 rows per batch.

Picking the right entry point

  • Built-in Stripe connector — default for MRR / subscription counts. Zero glue code; the cron does the work.
  • Webhook — when the source already emits webhooks (GitHub, Linear, Zapier). Cheapest plumbing for non-Stripe sources.
  • Custom Stripe forwarder — when the built-in metric definition doesn't match your board's convention.
  • MCP cron — when the source needs auth you don't want to surface (a private DB, an internal API). The MCP key + scoped role keep the blast radius small.
  • CSV upload — historic backfills, one-off corrections, or hand-curated metrics that don't have a system of record yet.

Next