Automation

Build Log: Real-Time KPI Dashboard via Webhook-Triggered Pipeline

A client was pulling from four data sources manually twice a day to update their reporting dashboard. Here's how I replaced that with a webhook-driven pipeline that updates the moment a conversion happens.
4 minutes to read2 months agoIgnasius Sevandri
June 21, 2026

The client ran a B2B lead generation operation — paid ads, outbound sequences, and affiliate partners — across four separate platforms. Every morning and every afternoon, their ops manager opened all four dashboards, pulled numbers into a spreadsheet, and manually updated the reporting Google Sheet that the leadership team relied on for decisions.

Two pulls a day. Four platforms. One spreadsheet. One person spending 45 minutes on it, twice, every working day.

The brief: make it automatic and make it real-time.

Data Sources

  • Facebook Ads Manager — campaign spend and conversion events
  • GoHighLevel — contact creation, deal stage changes, and call logs
  • Google Ads — keyword performance and conversion tracking
  • Stripe — actual revenue events (completed payments)

The challenge: these four platforms have different APIs, different authentication patterns, different rate limits, and different data shapes. Unifying them into a single reporting schema is the core of the problem.

Architecture Decision: Push vs. Pull

The original manual process was a pull model — someone goes to each platform and retrieves data. The obvious automated equivalent is a scheduled pull: an n8n workflow that runs every hour and pulls from all four APIs.

I went with a hybrid instead:

  • Stripe and GoHighLevel: webhook-driven (push). Both support outbound webhooks on key events. Stripe fires on payment completion; GHL fires on contact creation and deal stage changes.
  • Facebook and Google Ads: scheduled pull (every 4 hours). Neither has real-time webhooks for the level of granularity we need. Hourly polling would have worked but created rate limit pressure.

The result: revenue and CRM events update within seconds of happening. Ad spend and performance metrics refresh every four hours. For the client's actual decision-making cadence, this was accurate enough.

The Normalisation Layer

The four data sources return very different shapes. Stripe gives you an amount in cents with a currency code. Facebook Ads gives you spend with a breakdown by ad set. GoHighLevel gives you a contact object with 30+ fields. Google Ads gives you keyword-level impressions, clicks, and conversions.

I built a normalisation workflow in n8n that sits between each data source and the central database. Each normaliser takes the raw payload from its source and outputs a standardised event record:

{
  "event_type": "revenue|lead|conversion|spend",
  "source": "stripe|ghl|facebook|google",
  "amount": number | null,
  "timestamp": ISO8601,
  "metadata": {}
}

The metadata field carries source-specific context — ad set name, contact ID, keyword, campaign — that matters for drill-down but doesn't need to be in the core schema.

This layer was the part that took the most iteration. Facebook's API returns spend aggregated by time window, not per-event, so I had to decide how to handle the delta between successive pulls — tracking last-seen totals per campaign and computing incremental changes.

The Database

Everything flows into a Postgres database with five tables:

  • events — the normalised event stream
  • daily_aggregates — pre-computed daily totals per source and event type
  • campaigns — Facebook and Google campaign metadata
  • contacts — GHL contact records
  • kpi_snapshots — point-in-time snapshots for trend analysis

The daily_aggregates table is recomputed every night by a scheduled n8n workflow. This pre-computation is what makes the dashboard fast — the frontend doesn't compute aggregates at query time.

The Dashboard

Rather than build a custom frontend, I pointed a Metabase instance at the Postgres database. The client's leadership team gets:

  • Live KPI tiles — today's revenue (Stripe), leads created (GHL), ad spend (Facebook + Google), cost per lead
  • Trend charts — 30-day rolling for each KPI with 7-day moving averages
  • Campaign breakdown — spend vs. conversions per Facebook and Google campaign, updated every 4 hours
  • Pipeline view — deal stage distribution in GHL, updated in real time

Metabase's question builder lets the ops manager create their own ad-hoc views without needing to touch the database. That was a specific request — the client wanted to reduce their dependency on me for routine reporting changes.

Error Handling

The webhook flows have a dead-letter queue: if the normalisation step fails (malformed payload, unexpected field names after an API update), the raw payload is stored with an error flag. A daily digest email shows any failed events so they can be manually reviewed and reprocessed.

The scheduled pulls have retry logic: if Facebook or Google Ads returns a rate limit error, the workflow waits 5 minutes and retries. After three retries, it alerts via Slack and marks the pull as failed.

Result

The ops manager's twice-daily manual process is gone. Revenue events appear on the dashboard within 5 seconds of Stripe processing the payment. Lead events appear within 10 seconds of a contact being created in GHL. Ad performance refreshes every 4 hours instead of twice a day.

The client estimates they recovered about 7.5 hours per week across the ops team (the pull itself, plus questions that would have required a manual check). The pipeline runs unattended and has handled two Facebook API version upgrades without breaking — both changes were non-breaking to the fields we use, and the normalisation layer absorbed the minor shape changes.

Newsletter

Automation Playbooks, Delivered

New playbooks and build logs on AI automation — no fluff, no cadence pressure. When something is worth sharing, it lands in your inbox.