About

Public API

Programmatic access to indicators, your watchlist, and your dashboards. API tokens require Research or Pro. Authenticate with a personal access token (PAT) minted in Account > Security.

Last reviewed: 2026-05-21.

Quickstart

  1. Sign in, open Account > Security, mint a token, copy the plaintext. The token is shown once and cannot be recovered later.
  2. Set it as MBM_PAT in your shell or app.
  3. Make a request:

curl

curl -H "Authorization: Bearer $MBM_PAT" \
  "https://macrobymark.com/api/v1/observations?slug=labor.unemployment.rate&country=US&from=2020-01-01"

Node / TypeScript

const response = await fetch(
  "https://macrobymark.com/api/v1/observations?slug=labor.unemployment.rate&country=US",
  { headers: { Authorization: `Bearer ${process.env.MBM_PAT}` } }
);
if (!response.ok) throw new Error(`API error ${response.status}`);
const { observations } = await response.json();
console.log(observations.slice(-12));  // last 12 monthly prints

Python

import os, requests

response = requests.get(
    "https://macrobymark.com/api/v1/observations",
    params={"slug": "labor.unemployment.rate", "country": "US"},
    headers={"Authorization": f"Bearer {os.environ['MBM_PAT']}"},
    timeout=10,
)
response.raise_for_status()
print(response.json()["observations"][-12:])  # last 12 monthly prints

A free /api/v1/platform-stats endpoint is unauthenticated and serves the public scale snapshot for journalists and integrators — useful as a smoke test before you mint a token.

Authentication

Every request includes the Authorization header:

Authorization: Bearer mbm_pat_<prefix>_<secret>

Tokens are scoped. A token without the required scope returns 403 with code pat_forbidden. A missing or invalid token returns 401 with code pat_unauthorized.

Rate limits

Each endpoint is rate-limited at 60 requests per minute per source IP. Research includes 1,000 API reads per rolling 30-day window; Pro includes 10,000. Exceeding a limit returns 429 with Retry-After and X-RateLimit-* headers. The bucket is process-local in test environments and durable in production (Supabase RPC).

Public API

GET /api/v1/indicators

Query normalized indicator metadata for dashboards, watchlists, and classroom pipelines without scraping product pages.

Scope: indicators:read

Request

curl https://macrobymark.com/api/v1/indicators?topic=gdp&limit=3 \
  -H "Authorization: Bearer mbm_pat_demo_demo"

Sample response

{
  "indicators": [
    {
      "slug": "gdp.real",
      "displayName": "Real GDP",
      "topicSlug": "gdp",
      "frequencyHint": "Q",
      "unitHint": "Index"
    },
    {
      "slug": "prices.cpi.headline",
      "displayName": "Headline CPI",
      "topicSlug": "prices",
      "frequencyHint": "M",
      "unitHint": "Index"
    }
  ],
  "pagination": { "total": 128, "returned": 2, "limit": 3 }
}
Filters: topic, q, limit60 requests per minuteJSON catalog payload

GET /api/v1/indicators

Required scope: indicators:read

Reads from the canonical indicator catalog (the same source that powers the marketing site's indicator routing). Filters apply server-side. No DB read.

Query parameters: topic (string), q (full-text fragment), limit (1-200, default 100).

{
  "indicators": [
    {
      "slug": "real-gdp",
      "displayName": "Real GDP",
      "definition": "...",
      "topic": "growth",
      "parentSlug": null,
      "frequencyHint": "quarterly",
      "methodologyUrl": "https://..."
    }
  ],
  "count": 1,
  "totalCount": 47,
  "query": { "topic": null, "q": null, "limit": 100 }
}

GET /api/v1/observations

Required scope: indicators:read

Returns the observation series for one canonical indicator slug. Optional country preference, date window, frequency assertion (no resampling — must match source frequency), and a row limit.

Query parameters: slug (required), country (ISO-3166-1 alpha-2), from, to (ISO date, inclusive), freq (daily | weekly | monthly | quarterly | annual; must match source), limit.

{
  "slug": "unemployment-rate",
  "series": {
    "id": "labor.unemployment.headline",
    "name": "Unemployment Rate",
    "source": "bls",
    "sourceAgency": "U.S. Bureau of Labor Statistics",
    "sourceId": "LNS14000000",
    "frequency": "monthly",
    "unit": "percent",
    "dataStatus": "live",
    "lastFetchedAt": "2026-05-19T13:30:00Z",
    "country": { "code": "US", "name": "United States" }
  },
  "query": {
    "country": "US", "from": "2020-01-01", "to": null, "freq": null, "limit": null
  },
  "count": 64,
  "observations": [
    { "date": "2020-01-01", "value": 3.6 }
  ],
  "compliance": { "source": "bls", "surface": "api", "decision": "allowed" },
  "generatedAt": "2026-05-20T11:30:00Z"
}

The endpoint enforces redistribution compliance per provider. A series whose provider blocks the api surface returns 451 with a structured compliance payload.

GET /api/v1/revisions

Required scope: indicators:read

Returns the revision history (vintages, restatement tags, or platform snapshots — see /about/trust for the per-provider capability matrix) for one canonical indicator slug as of a target vintageDate.

Rate limit on this endpoint is tighter (30 req/min/IP) because vintage assembly is more expensive than observation reads.

Query parameters: slug (required), country, vintageDate (defaults to today).

GET /api/v1/releases

Required scope: indicators:read

Live release calendar. Each event carries scheduleSourceKind (source-native official / API / provider calendar / static curated / heuristic / unknown), so consumers can filter on schedule-authority. When a release has an attached snapshot, the response embeds releaseSnapshot with actual / expected / surprise / previous values and an expectationSource label.

Query parameters: from, to, country (ISO-3166-1), importance (high | medium | watch), limit (1-500, default 250).

{
  "count": 12,
  "totalCount": 480,
  "matchedCount": 12,
  "query": { "from": "2026-05-01", "to": "2026-05-31", "country": "US", "importance": "high", "limit": 250 },
  "releases": [
    {
      "id": "evt-...",
      "date": "2026-05-13", "day": "Tue", "time": "08:30",
      "title": "CPI - May",
      "importance": "high",
      "indicatorId": "prices.cpi.headline",
      "scheduleSourceKind": "source_native_official",
      "scheduleSourceUrl": "https://...",
      "releaseSnapshot": {
        "actualValue": "2.7%", "expectedValue": "2.6%",
        "surpriseValue": "+0.1pp", "expectationSource": "modeled"
      }
    }
  ],
  "compliance": { "surface": "public", "filteredCount": 0 },
  "generatedAt": "2026-05-20T11:30:00Z"
}

GET /api/v1/watchlist

Required scope: watchlist:read

{
  "watchlist": {
    "indicatorIds": ["gdp.real", "prices.cpi.headline"],
    "createdAt": "2026-04-12T...",
    "updatedAt": "2026-05-02T..."
  }
}

PUT /api/v1/watchlist

Required scope: watchlist:write

Body:

{ "indicatorIds": ["gdp.real", "labor.unemployment.headline"] }

Up to 1000 ids. The server normalizes the list (dedupes, trims, drops empties) before storage. The complete new list replaces the previous one; this is upsert, not append.

GET /api/v1/dashboards

Required scope: dashboards:read

Supports limit (1-100) and offset (0 or higher).

{
  "dashboards": [
    {
      "id": "...",
      "title": "Recession watch",
      "description": "...",
      "gridCols": 4,
      "isPublic": false,
      "layout": [...],
      "createdAt": "...",
      "updatedAt": "..."
    }
  ],
  "pagination": {
    "total": 7,
    "returned": 1,
    "limit": 50,
    "offset": 0,
    "nextOffset": null
  }
}

Dashboard write surface (PUT, DELETE) is not in v1. Coming in a follow-up release.

What is not in v1

  • Account management (password, email, MFA, passkey).
  • Billing operations.
  • Admin endpoints.
  • Lab runs / forecasts.
  • Dashboard write surface (PUT, DELETE).

The scope set is pinned at the schema check constraint and the typed ALLOWED_SCOPES tuple in code; a token cannot acquire access to surfaces that are not on the list.

GET /api/v1/platform-stats (unauthenticated)

Public-safe snapshot of platform scale: total series count, provider count, country count, addressable indicator-country page count, plus a provider breakdown. No authentication required — this is the surface journalists, buyers, and integrators can use to independently verify our public claims.

The response includes an HMAC-SHA256 signature over the payload when PLATFORM_STATS_HMAC_SECRET is configured server-side. The signature lets you save a snapshot now and later prove "at time T, the platform's own infrastructure asserted X" without having to trust us unilaterally.

{
  "schemaVersion": "platform-stats-public/v1",
  "generatedAt": "2026-05-20T11:30:00.000Z",
  "asOf": "2026-05-20T11:00:00.000Z",
  "sourceOfTruth": "tiger",
  "isFallback": false,
  "isDegraded": false,
  "totalDiscoverySeries": 2900000,
  "providerCount": 16,
  "countryCount": 27,
  "trackedIndicatorCount": 23381,
  "providerBreakdown": [
    { "provider": "fred", "seriesCount": 582000 }
  ],
  "signing": {
    "signature": "<64-char hex sha256-hmac>",
    "signatureAlgorithm": "sha256-hmac",
    "signedAt": "2026-05-20T11:30:00.000Z"
  },
  "verification": {
    "algorithm": "sha256-hmac",
    "instructions": "Compute sha256-hmac of canonical-JSON-stringify(payload) + '\n' + signing.signedAt, using the platform-disclosed key."
  }
}

Edge-cached: s-maxage=60, stale-while-revalidate=600. Stats refresh roughly hourly server-side; a one-minute edge freshness is the right trade-off for an unauthenticated surface.

When the signing secret is absent (preview / dev), signature is null and signatureAlgorithm is "none". The numbers themselves remain accurate; only third-party signature verification is unavailable.

Error shape

All errors return JSON in this shape:

{ "error": "<human readable>", "code": "<machine readable>" }

Common codes:

  • pat_unauthorized -- 401, missing or invalid token.
  • pat_forbidden -- 403, scope mismatch.
  • pat_service_unavailable -- 503, server-side config issue.
  • watchlist_invalid_body -- 400, schema validation failed.

Operational policies for the API (incident response, retention, subprocessors) are documented at /about/trust.