Internal · v1.2.0 · Read-only

GA4 Commentary Gateway

A thin, read-only API that fetches normalized month-on-month Google Analytics 4 data for AI commentary workflows.

This service returns structured data. It does not generate commentary.

What it does

Built for one job, done well.

A disciplined surface area: deterministic data, no model output, no surprises.

Allowlisted Properties

Only preconfigured GA4 properties can be queried. Callers pass a client_key slug, not raw property IDs.

Month-on-Month Comparison

Computes current vs previous month for 7 key metrics with absolute and percentage change.

Channel Breakdown

Optional per-channel metrics (Organic Search, Direct, Social, etc.) with the same comparison structure.

Derived Insights

Flags like largest growth/decline channel, conversion efficiency changes, and traffic-vs-quality trade-offs.

6-Hour Caching

Monthly data is cached for 6 hours to minimise GA4 API calls.

Request Audit Log

Every API call is logged with client key, status, duration, and error details.

AI Commentary Skill

Generate commentary in your editor.

A Codex / Cursor skill that calls this gateway and writes polished, client-ready month-on-month performance commentary. Non-technical users can use the interactive wizard; power users can pass parameters inline.

How it works

01

Invoke skill

Type @@ga4-seo-commentary in your editor

02

Provide inputs

Inline params, wizard, or screenshots

03

Gateway fetches data

Live GA4 metrics via this API

04

Commentary generated

Polished month-on-month narrative

Three ways to use it

Inline parameters

Fastest path — provide client and month directly and skip the wizard.

@ga4-seo-commentary
client_key: elixirr
month: 2026-04

Interactive wizard

Just invoke the skill and answer the prompts. The wizard fetches the live client list from the properties endpoint and offers the last 3 months to pick from.

@ga4-seo-commentary

Screenshots or pasted data

No API access? Attach GA4 screenshots or paste the numbers. The skill handles both.

@ga4-seo-commentary
# Then attach your GA4 screenshots or paste the metrics table

What you get back

1

Traffic overview

Sessions movement and top channel driver

2

Engagement quality

Engagement rate trends by channel

3

Conversion performance

Conversion rate and key event changes

4

Channel insight

Standout acquisition channels

5

Key takeaways

Wins, concerns, and next steps

Installation

01

Download and extract

Download the skill zip and extract it into your Codex skills folder.

# Download from the gateway
curl -O https://poc-ga4-mcp-gateway.j.layershift.co.uk/downloads/ga4-seo-commentary-skill.zip

# Extract into your skills directory
unzip ga4-seo-commentary-skill.zip -d ~/.codex/skills/
02

Verify installation

Check the skill directory exists with the right structure.

ls ~/.codex/skills/ga4-seo-commentary/
# SKILL.md  agents/  assets/  references/

Requirements

  • Cursor IDE or OpenAI Codex with skills support
  • A bearer token for this gateway (ask the gateway admin)
  • The skill directory extracted to ~/.codex/skills/

API Key Management

Create, scope, and revoke keys.

Database-backed API keys replace the single shared secret. Keys are hashed at rest, support optional scoping to specific clients, and can be managed via CLI or the admin API.

Hashed at Rest

Keys are SHA-256 hashed. The plaintext is shown once at creation and never stored.

Role-Based Access

Admin keys can manage other keys. Standard keys can only access data endpoints.

Client Scoping

Keys can be restricted to specific client_key slugs, or left global for full access.

Soft Revocation

Revoked keys are timestamped for audit. Optional expiry dates for time-boxed access.

CLI commands

# Create a new admin key
php artisan api-key:create --name="Ross - Admin" --role=admin

# Create a scoped standard key
php artisan api-key:create --name="Acme Bot" --scoped-client-keys=acme,globex

# List active keys
php artisan api-key:list

# Revoke a key
php artisan api-key:revoke 3

# Seed legacy env var into database
php artisan api-key:seed-from-env

Admin API

POST /api/v1/api-keys

Create a key (admin only). Returns plaintext key once.

GET /api/v1/api-keys

List keys (admin only). Never returns hashes.

DELETE /api/v1/api-keys/{id}

Revoke a key (admin only). Cannot self-revoke.

The legacy GA4_GATEWAY_API_KEY env var still works as a fallback. Existing integrations are unaffected.

Property Management

Admin panel for GA4 properties.

A web-based admin panel for managing allowlisted GA4 properties. Login at /admin with credentials created via artisan.

Getting started

# Create an admin user
php artisan admin:create-user

# Start the server
php artisan serve

# Visit the admin panel
open http://127.0.0.1:8000/admin/login

What you can do

Add Properties

Register new GA4 properties with client_key, name, and property ID.

Edit Properties

Update property details and toggle active/inactive status.

Test Connection

Verify GA4 API credentials work for a property before going live.

Remove Properties

Soft-delete properties. They stop appearing in API responses but data is preserved.

API reference

Three endpoints. No more, no less.

All endpoints are read-only and authenticated with a Bearer token.

GET /api/v1/health No auth required

Smoke test. Returns 200 if the service is up.

{
  "status": "ok"
}
GET /api/v1/ga4/properties Bearer token

Lists all active, allowlisted properties the caller can query.

{
  "data": [
    { "client_key": "elixirr", "name": "Elixirr.com" },
    { "client_key": "acme", "name": "Acme Corp" }
  ]
}
GET /api/v1/ga4/monthly-commentary-data Bearer token

The main endpoint. Returns month-on-month comparison data for a given property.

Parameters

Name Type Required Description
client_key string Yes The slug of the GA4 property to query
month string Yes Target month in YYYY-MM format
include_channels boolean No Include per-channel breakdown (default: false)
{
  "data": {
    "property": {
      "client_key": "elixirr",
      "name": "Elixirr.com",
      "ga4_property_id": "properties/123456789"
    },
    "period": {
      "current_month": "2026-04",
      "previous_month": "2026-03",
      "current_month_range": { "start": "2026-04-01", "end": "2026-04-30" },
      "previous_month_range": { "start": "2026-03-01", "end": "2026-03-31" }
    },
    "summary": {
      "sessions": {
        "current": 12450, "previous": 11200,
        "absolute_change": 1250, "percent_change": 11.16
      },
      "total_users": { "current": 9800, "previous": 8900, "absolute_change": 900, "percent_change": 10.11 },
      "new_users": { "current": 7200, "previous": 6500, "absolute_change": 700, "percent_change": 10.77 },
      "engaged_sessions": { "current": 8100, "previous": 7300, "absolute_change": 800, "percent_change": 10.96 },
      "engagement_rate": { "current": 65.06, "previous": 65.18, "absolute_change": -0.12, "percent_change": -0.18 },
      "key_events": { "current": 340, "previous": 290, "absolute_change": 50, "percent_change": 17.24 },
      "session_key_event_rate": { "current": 2.73, "previous": 2.59, "absolute_change": 0.14, "percent_change": 5.41 }
    },
    "channels": [
      {
        "channel": "Organic Search",
        "metrics": {
          "sessions": { "current": 6200, "previous": 5500, "absolute_change": 700, "percent_change": 12.73 }
        }
      }
    ],
    "insights": {
      "top_growth_channel": { "channel": "Organic Search", "metric": "sessions", "percent_change": 12.73 },
      "top_decline_channel": { "channel": "Direct", "metric": "sessions", "percent_change": -3.45 },
      "conversion_efficiency": "improving",
      "traffic_quality_tradeoff": null
    },
    "notes": [
      "Data cached at 2026-05-12T10:30:00Z. Next refresh in ~5h."
    ]
  }
}

Metrics reference

Seven summary metrics. Five per channel.

Every metric ships in the same comparison shape — current, previous, absolute_change, percent_change.

Summary metrics

Key GA4 API Name Type
sessions sessions integer
total_users totalUsers integer
new_users newUsers integer
engaged_sessions engagedSessions integer
engagement_rate engagementRate percentage (0–100)
key_events keyEvents integer
session_key_event_rate sessionKeyEventRate percentage (0–100)

Channel metrics

sessions engaged_sessions engagement_rate key_events session_key_event_rate

Comparison shape

{
  "current": 12450,
  "previous": 11200,
  "absolute_change": 1250,
  "percent_change": 11.16
}

Note: percent_change is null when previous is zero (division by zero guard).

Error codes

Predictable failures.

All errors return the same shape so downstream prompts can branch cleanly.

Code Meaning When
401 Unauthorized Missing or invalid Bearer token
404 Not Found Unknown or inactive client_key
422 Validation Error Invalid or missing query parameters
502 Bad Gateway Downstream GA4 API or auth failure
{
  "error": {
    "code": 422,
    "message": "The client_key field is required.",
    "details": {
      "client_key": ["The client_key field is required."]
    }
  }
}

Quick start

From clone to first call in six steps.

01

Install dependencies

composer install
02

Configure environment

cp .env.example .env
php artisan key:generate

# Add your GA4 credentials to .env:
# GOOGLE_APPLICATION_CREDENTIALS=/path/to/service-account.json
# GA4_GATEWAY_API_KEY=your-secret-token
03

Run migrations

php artisan migrate
04

Add a GA4 property

php artisan ga4:property:upsert \
  --client-key=elixirr \
  --name="Elixirr.com" \
  --property-id=123456789
05

Start the server

php artisan serve
06

Test it

curl -s http://127.0.0.1:8000/api/v1/health
# {"status":"ok"}

curl -s -H "Authorization: Bearer your-secret-token" \
  "http://127.0.0.1:8000/api/v1/ga4/monthly-commentary-data?client_key=elixirr&month=2026-04" \
  | jq .

Known limitations

Read this before you ship.

Ten honest gotchas. None are blockers — but assume them, not around them.

01

Monthly granularity only.

No daily, weekly, or custom date ranges. The API always compares full calendar months.

02

Partial-month data is real-time.

If you query the current month mid-way through, you'll get data up to today — not a projection.

03

Channel grouping is GA4-default.

We use the Default Channel Group dimension. Custom channel definitions in your GA4 property are not reflected.

04

Seven metrics, not more.

The metric set is fixed. If GA4 adds new metrics or you need custom ones, the API must be updated.

05

Service account, not OAuth.

GA4 access uses a service account JSON key. There is no user-level OAuth flow.

06

Single service account.

One set of Google credentials serves all properties. Ensure the service account has Viewer access to every property you configure.

07

Cache is time-based, not event-based.

Data refreshes every 6 hours. There is no webhook or manual purge endpoint.

08

No write operations.

This API is strictly read-only. It cannot modify GA4 properties, goals, or events.

09

Multi-key auth replaces single shared secret.

API keys are now database-backed with SHA-256 hashing, optional scoping, and revocation. The legacy GA4_GATEWAY_API_KEY env var still works as a fallback. See the API Key Management section.

10

No rate limiting.

The API does not enforce per-caller rate limits. Abuse will hit the GA4 API quota. Use responsibly.