Windsor.ai

July 19, 2026 · View on GitHub

Windsor.ai is a marketing data aggregator with 325+ connectors (Meta Ads, Google Ads, GA4, TikTok Ads, LinkedIn, Microsoft Ads, organic social — Instagram / Facebook / TikTok / YouTube / LinkedIn —, Google Search Console, Klaviyo, Shopify, Stripe, and many more). It is an optional but high-leverage data source for the marketing-facing ops commands (/ops:marketing, /ops:socials, /ops:ecom, /ops:dash, /ops:go, /ops:next).

This doc is provider-agnostic and registry-driven: it describes the wiring pattern, not any specific account. All account identifiers below are placeholders — every user supplies their own via the project registry.

Two access modes

ModeUse forAuthCost
MCP (mcp__*Windsor*__*)interactive drill-down inside an ops command — per-campaign / per-day / arbitrary fieldsclaude.ai OAuth, no key neededruns in-session
REST (https://connectors.windsor.ai/<connector>)headless / cron pulls — feeding a cache, statusline, or dashboard with no model callsapi_key (one per Windsor account)free, no model quota

REST request shape:

https://connectors.windsor.ai/<connector>?api_key=<KEY>&date_from=YYYY-MM-DD&date_to=YYYY-MM-DD&fields=<comma,separated,field,ids>
  • Supported field ids per connector: https://connectors.windsor.ai/<connector>/fields?api_key=<KEY>
  • Omitting a date field aggregates over the whole range, one row per account.
  • A response of {"data":[...]} means the connector is connected (even if empty); {"error":"No <connector> account ..."} means it must be connected first in the Windsor dashboard.

Store the REST key in the OS secret store (e.g. macOS Keychain service windsor_api_key), never in a repo or in preferences.json in plaintext.

Registry-driven account mapping

Map each project's connector accounts in your project registry (registry.json.projects[].windsor) so every command pulls the right numbers per project. Connector slugs are lowercase (facebook = Meta Ads, google_ads, googleanalytics4 = GA4, tiktok = TikTok Ads, tiktok_organic, instagram, facebook_organic, youtube, searchconsole, …):

{
  "projects": [
    {
      "alias": "<your-project>",
      "windsor": {
        "facebook": "<meta_ad_account_id>",
        "google_ads": "<google_ads_account_id>",
        "googleanalytics4": "<ga4_property_id>",
        "instagram": "<ig_account_id>",
        "searchconsole": "sc-domain:<your-domain>",
      },
    },
  ],
}

Optionally register Windsor.ai in the partner registry so /ops:credentials and /ops:status can see it:

// preferences.json → partner_registry.windsor_ai
{
  "category": "marketing-analytics",
  "auth_type": "api-key+mcp",
  "mcp_server": "<windsor-mcp-server-id>",
  "credential_key": "windsor_api_key",
  "rest_base": "https://connectors.windsor.ai",
  "read_tools": ["get_connectors", "get_fields", "get_data", "get_options"],
  "write_tools": ["list_actions", "execute_action"],
}

What the marketing commands should produce

When a command has Windsor data available, return an analysis, not a table dump:

  1. Trend — today vs 7-day vs 30-day; what moved and by how much.
  2. Per channel — Meta vs Google (vs TikTok …) ROAS / CAC / CPC / CTR; where the return is and where budget leaks.
  3. Anomalies — spend / CPC / CTR / conversion spikes & drops; campaigns spending with no conversions (drill down per campaign via the MCP).
  4. Funnel — sessions → add-to-cart → checkout → purchase; where it drops.
  5. Organic ↔ paid — does organic growth contribute to blended CAC.
  6. Concrete actions with numbers — e.g. "pause campaign X (spend, 0 conv, 30d)", "shift budget from Meta to Google (ROAS a vs b)". Offer to execute via list_actions / execute_action (Meta + Google Ads support pause / enable / budget), only after the user confirms.

Caveats to apply before drawing conclusions

  • Blended over platform-reported ROAS. When server-side conversion APIs (Meta CAPI, GA4 server events, etc.) are partial or off, a platform under-reports its own attributed revenue. Prefer blended ROAS = analytics/store revenue ÷ total ad spend as the source of truth, and flag the attribution gap.
  • GA4 lag. GA4 typically trails several hours, so "today" revenue/orders may read 0. For realtime same-day revenue prefer the store source (Shopify, etc.); use GA4 for 7d/30d.
  • Connect-before-query. A connector returns error: No <connector> account until it is connected in the Windsor dashboard; some require a separate OAuth per ads vs organic (e.g. tiktok vs tiktok_organic).
  • Trial plan limits. Free/trial Windsor plans cap the number of data sources; adding a new connector may require freeing a slot or upgrading.

Data sanity / expired-plan detection

When a Windsor.ai subscription lapses, reads do not fail — they silently return only zeros / empty rows. Every downstream consumer (dashboards, blended-ROAS math, AI insight layers, cron caches) then reports zeros as if they were real data, potentially for weeks, with no error anywhere.

Detection pattern that works in practice:

  • If spend + impressions for facebook (Meta) AND google_ads, and instagram reach, are ALL exactly 0 over a 30-day window while the accounts are connected, it is almost certainly an expired plan or blocked reads — not a real quiet month.
  • Confirm via the Windsor MCP get_current_user where available: is_paid: false (and plan_name) is the smoking gun. Offer get_subscription_url so the user can renew.

There is a second failure mode: an expired/over-limit plan can also return HTTP 200 with a marketing text row inside the data instead of metrics, so a naive has("data") check "succeeds" while the payload is garbage. Example payload seen in production:

{"data":[{"source":"Uh-oh! You've reached your plan limit... Upgrade here... Free plan..."}]}

The sanity script detects this with a case-insensitive match on "upgrade here", "plan limit", "free plan", or "uh-oh" in any string value and prints warn: windsor quota/plan-limit message detected (plan expired?) (exit 1).

What to do on an all-zero result:

  1. Warn the user explicitly — e.g. "Windsor returns all zeros — plan likely expired, data unreliable" — instead of reporting the zeros.
  2. Never present the zeros as real metrics in any dashboard, analysis, or recommendation. Treat the source as unavailable and fall back to direct platform APIs or cached last-known-good data, clearly labeled as such.
  3. Offer get_subscription_url (MCP) or the Windsor dashboard to restore the plan, then re-verify with a fresh pull.

The reusable check lives in claude-ops/scripts/windsor-data-sanity.sh: it reads a Windsor JSON payload (file or stdin), sums a configurable list of jq paths (default: every spend, impressions, and reach field), and prints ok (exit 0), warn: windsor quota/plan-limit message detected (plan expired?) (exit 1, text-row pattern), or warn: windsor all-zero pattern (plan expired?) (exit 1). Wire it into any cron pull or cache refresh so stale-plan garbage is flagged before it reaches a dashboard:

curl -s "$WINDSOR_URL" | claude-ops/scripts/windsor-data-sanity.sh || echo "flag cache as unreliable"

Adding the integration

Run /ops:integrate windsor.ai (auth type api-key, base https://connectors.windsor.ai) to register it in the partner registry, then add the per-project windsor mapping shown above. The marketing commands pick it up automatically when the mapping is present.