# AEO Content - Business Impact reporting API

Updated: 2026-08-31 (growth endpoint added). Stable base URL: `https://studio.aeocontent.ai`

Programmatic access to the traffic + leads reporting AEO Content maintains for your domain:
organic-search and AI-engine visits, leads, call-to-action clicks, per-page detail and AI-answer
citations - the same numbers as the Studio "Business Impact" page, from the same code path.

## Authentication

Every request needs an API key (created in Studio under Integrations / API; format
`aeo_live_` + 32 hex chars). Send it either way:

    Authorization: Bearer aeo_live_YOUR_KEY
    x-api-key: aeo_live_YOUR_KEY

API access is included with the Business plan. A key can read every domain on the Business or
Business Plus plan that its account owns or is a member of. `GET /api/v1/domains` lists them.
When the key can read exactly one domain, the `domain` query parameter may be omitted. A domain
on a lower plan answers `403 plan_required`, naming the Business plan.

Rate limits (per key): 60 requests a minute and 2,000 a day by default. Every authenticated response carries
`X-RateLimit-Limit`, `X-RateLimit-Remaining` and `X-RateLimit-Reset` (Unix seconds by which the
window has fully refilled) for the window closer to running out. A request over a limit gets
`429 rate_limit_exceeded` with `Retry-After` (seconds until the next request is allowed).
CORS is open (`*`), so browser calls work; the key is a bearer secret, so prefer server side storage.

## Conventions

- Success envelope: `{ "data": ..., "meta": { "request_id", "timestamp" } }`
- Error envelope: `{ "error": { "code", "message" } }` with HTTP 400 / 401 / 403 / 409 / 429.
  `409 not_connected` = the domain has no Google Analytics link yet. `403 plan_required` = the
  domain is not on the Business plan or higher.
- Time window: `days=N` (default 28, max 366) or `from=YYYY-MM-DD&to=YYYY-MM-DD`.
  The window always ends on the last day data was received (`period.data_through`) - Google
  Analytics delivers with a 24-48h delay and the nightly sync adds a day, so "today" is never
  in a response. Every response echoes the resolved `period`.
- `format=csv` on /reports endpoints returns CSV instead of JSON (the daily series for the
  main report; row-per-page / row-per-event for the others).
- **`null` means "not measured for this window", never zero.** Rendering null as 0 is the
  main integration mistake to avoid.
- Numbers from different sources (Search Console, Cloudflare, Google Analytics, the first-party
  beacon) are independent measurement systems. Never sum across them.

## Measurement semantics (read before charting)

1. **`data.site.*` metrics count every visitor** (measured outside the browser): Search
   Console clicks/impressions, Cloudflare-edge AI-answer arrivals, first-party-beacon CTA
   clicks and form successes, RUM page views.
2. **`data.totals` and everything GA4-derived is a consent sample**: on sites with a cookie
   dialog, Google Analytics counts only visitors who accept (can be ~1 in 12). Use those
   figures for SHARES and behaviour splits, not absolute traffic.
3. **`change_pct` is coverage-gated.** It exists only when the source fully covers both the
   window and the previous window; otherwise it is null and `baseline_available_from` says
   when a full comparison first exists. GA4 change_pct values are additionally nulled when the
   sample size shifted >3x between the windows (`measurement.ga4_change_reliable=false`) -
   that is a consent/tagging regime change, not traffic.
4. Every SiteMetric carries `measured_from` - the day that measurement was switched on for
   the site. Days before it were unmeasured, not zero.

## Endpoints

### GET /api/v1/domains
Domains the key may read.

    { "data": { "domains": [
        { "domain": "example.com", "analytics_connected": true, "data_through": "2026-08-27" }
    ] } }

### GET /api/v1/reports/business-impact/summary?domain=&days=28
The period as short plain-English facts (organic search + AI engines only) - ready for an
email or a dashboard tile.

    { "data": {
        "domain": "example.com",
        "period": { "from", "to", "days", "previous": { "from", "to" } },
        "headline": "Google clicks 2,034 (up 61%) · brand in 48% of AI prompts (was 36%) · 98 organic leads",
        "sections": [ { "title": "Google search", "bullets": ["...", "..."] } ],
        "note": "how the facts are generated"
    } }

### GET /api/v1/reports/business-impact?domain=&days=28  (or from/to; format=csv)
The full report. Shape of `data`:

    {
      "domain", "generated_at",
      "period": { "from", "to", "days", "previous": {"from","to"}, "data_through", "note" },
      "definitions": { "organic", "segments": [...], "leads": { "events": [...], "all_key_events" }, "cta" },
      "coverage": { "sessions_from", "events_from", "cta_from" },
      "site": {                    // every-visitor metrics; each is a SiteMetric (below)
        "people",                  // RUM page views (whole site)
        "search_clicks",           // Google Search Console clicks
        "search_impressions",
        "ai_arrivals",             // + "engines": [{ "engine", "label", "visits" }]
        "cta_clicks",              // first-party beacon, organic+AI visitors;
                                   //   + "by_button": [{ "event","label","total",
                                   //       "organic_search","ai","paid","direct","other" }]
        "form_successes",          // beacon, confirmed submissions, organic+AI
        "applications": {          // milestones the client's app reports back (see "Reporting app milestones")
          "measured_from": "YYYY-MM-DD"|null,
          "funnel": [ { "event": "app_application_started", "label", "total",
                        "organic_search", "ai", "social", "paid", "direct", "other" } ]
        }
      },
      "measurement": { "ga4_change_reliable": true|false, "notes": ["..."] },
      "totals":   { /* GA4 consent sample */ "sessions", "engaged_sessions", "organic_sessions",
                    "ai_sessions", "paid_sessions", "direct_sessions", "leads",
                    "organic_leads", "ai_leads", "cta_clicks", "organic_cta_clicks", "ai_cta_clicks" },
      "previous": { same keys as totals },
      "change_pct": { same keys, number|null },
      "by_segment": [ { "segment": "organic_search|ai|organic_social|paid|direct|referral|other",
                        "label", "sessions", "engaged_sessions", "leads", "cta_clicks" } ],
      "ai_engines": [ { "engine", "label", "sessions", "leads", "cta_clicks" } ],
      "leads_by_event": [ { "event", "label", "total", "organic", "ai" } ],
      "cta_by_event":   [ { "event", "label", "total", "organic", "ai" } ],
      "daily": [ { "date", "sessions", "organic_sessions", "ai_sessions", "leads",
                   "organic_leads", "cta_clicks", "organic_cta_clicks" } ]   // every day, in order
    }

SiteMetric shape (used by everything under `site`):

    { "source", "label", "note",
      "measured_from": "YYYY-MM-DD"|null,   // when this measurement was switched on
      "days_measured": n, "window_covered": bool,
      "total": n|null, "previous": n|null, "change_pct": n|null,
      "baseline_available_from": "YYYY-MM-DD"|null,
      "daily": [ { "date", "value" } ] }    // starts at max(measured_from, window start)

### GET /api/v1/reports/business-impact/pages?domain=&days=28
One row per page (sort: `sort=organic_sessions|sessions|ai_sessions|leads|cta_clicks|search_clicks|people_views|ai_citations`, `limit` up to 1000, `format=csv`):

    { "data": { "domain", "period", "measured", "note", "citations_through", "pages": [ {
        "landing_page": "/peptides",
        "search_clicks": n|null,       // Search Console, every visitor
        "people_views": n|null,        // RUM, every visitor, view-based
        "sessions", "organic_sessions", "ai_sessions", "paid_sessions",   // GA4 sample
        "leads": n|null, "organic_leads": n|null,
        "cta_clicks": n|null, "organic_cta_clicks": n|null,
        "ai_citations": n|null,        // recorded AI answers citing this page, period start through data.citations_through (today for a preset window)
        "app_started": n|null,         // applications started by organic+AI visitors from this page (client app reports)
        "app_completed": n|null,
        "cited_by": ["chatgpt", ...],
        "events":         { "cta_apply_now": 12, "contact_us": 3 },
        "organic_events": { "cta_apply_now": 9 }
    } ] } }

### GET /api/v1/reports/business-impact/events?domain=&days=28
One row per lead / CTA event:

    { "data": { "domain", "period", "events": [ {
        "event", "label", "kind": "lead"|"cta"|"other", "total",
        "by_segment": { "organic_search": n, "ai": n, "organic_social": n,
                        "paid": n, "direct": n, "referral": n, "other": n },
        "top_pages": [ { "landing_page", "count" } ]
    } ] } }

### GET /api/v1/reports/business-impact/growth?domain=
The since-start growth view (window-independent). Consent-independent sources only.

    { "data": {
        "domain", "engagement_start": "YYYY-MM-DD"|null, "data_through",
        "baseline": { "from", "to" }|null,        // 28 days before the first article
        "now_window": { "from", "to" },           // last 28 measured days
        "before_now": [ { "key": "search_clicks|search_impressions|search_ctr|page_one_pages|ai_presence|ai_arrivals|articles_published",
                          "label", "source", "unit": "count"|"pct",
                          "before": n|null,       // null = source not measured before the start
                          "now": n|null, "change_pct": n|null, "measured_from" } ],
        "series": [ { "key", "label", "source", "unit", "measured_from",
                      "weeks": [ { "week": "YYYY-MM-DD (Monday)", "value" } ] } ],   // full weeks only
        "markers": [ { "week", "label", "count" } ],   // the recorded work, per week
        "note"
    } }

## Reporting app milestones back (for the client's application backend)

A client's application backend can report funnel milestones so they appear next to the click
counts. Server-to-server, aggregate-only, no PII:

    POST https://studio.aeocontent.ai/api/site-events
    Authorization: Bearer aeo_ingest_...          # per-app token issued by AEO Content, scoped to one domain
    { "domain": "example.com", "user_agent": "example-app/1.0",
      "events": [ { "event": "app_application_started", "path": "/peptides",
                    "referrer": "https://chatgpt.com/", "entry_query": "utm_source=google", "destination": "app.example.com" } ] }

Events: `app_arrived`, `app_application_started`, `app_application_completed`,
`app_application_approved`, `app_customer_activated`. `path` = the www page the visitor came
from; `referrer` / `entry_query` = the attribution captured on arrival (the source is classified
from them). Up to 20 events per request; send each milestone once per applicant. Responses:
`{ "accepted": n }`, 401 bad token, 403 domain not covered by the token.

## Event vocabulary

- Lead events are the Google Analytics events agreed for the site (listed in
  `definitions.leads.events` of every report response), e.g. `contact_us`.
  `contact_us` fires on any submit ATTEMPT; `lead_form_success` (fired by the site) counts
  confirmed submissions.
- CTA events are clicks on the site's calls to action, named per button:
  `cta_apply_now`, `cta_sign_up`, `cta_get_started`, `cta_talk_to_specialist`,
  `cta_contact`, `cta_request_demo`, `cta_phone_click`, `cta_apply_other`.
  They exist twice: in GA4 (consent sample, in `totals`/`cta_by_event`) and from the
  first-party beacon (every visitor, in `site.cta_clicks`). Beacon coverage starts at
  `site.cta_clicks.measured_from`.

## Examples

    # what can this key see?
    curl -H "Authorization: Bearer $KEY" https://studio.aeocontent.ai/api/v1/domains

    # last 28 days, JSON
    curl -H "Authorization: Bearer $KEY" \
      "https://studio.aeocontent.ai/api/v1/reports/business-impact?domain=example.com&days=28"

    # daily series as CSV
    curl -H "Authorization: Bearer $KEY" \
      "https://studio.aeocontent.ai/api/v1/reports/business-impact?domain=example.com&days=28&format=csv" -o daily.csv

    // JavaScript
    const res = await fetch(
      "https://studio.aeocontent.ai/api/v1/reports/business-impact?domain=example.com&days=28",
      { headers: { Authorization: `Bearer ${process.env.AEO_API_KEY}` } },
    );
    const { data } = await res.json();
    // reliable trend:   data.site.search_clicks.total / .change_pct
    // AI arrivals:      data.site.ai_arrivals.total (+ .engines)
    // leads (sample):   data.totals.organic_leads, data.leads_by_event
    // per day:          data.daily  (join on date)

## Integration checklist for agents

1. Call /api/v1/domains first; use the returned domain(s).
2. Treat null as "not measured" - render a dash or "collecting", never 0.
3. Only chart a period-over-period change when `change_pct` is non-null.
4. Label GA4-derived numbers as a consented sample; label `site.*` numbers with their source.
5. Do not add `site.*` values to `totals.*` values.
6. Cache responses for at least an hour - the data updates once a day.
