# Autario Data API v1 — Quick Reference Base URL: https://autario.com/api/v1 OpenAPI spec: https://autario.com/api/v1/openapi.json autario is the calculator for LLM agents: stats are precomputed server-side on verified, source-cited data. One call returns finished numbers (avg, min, max, growth, correlation, seasonality) — never download rows to compute locally. ## Why not compute locally Pulling a 17k-row daily series is ~500 KB of JSON (>100k tokens), then you still have to run the math yourself. One stats call returns the finished, verified numbers in ~200 tokens: GET /public/ontology/stats/describe?indicator=&entity=DEU → { n, mean, median, std, min, max, quartiles, skew, histogram, interpretation } Same numbers, computed server-side from the primary-source data, with provenance. For a single aggregate on ANY dataset use ?aggregate= or ?summary_only=true (see Aggregations). Stats endpoints return summaries by default; pass full=true only when you need per-timepoint arrays (token cost). ## Authentication (private endpoints only) x-api-key: — identifies your account x-api-secret: — proves ownership (bcrypt-verified) Get keys at: /account?tab=apikeys ## CORS / Browser Use Public GET endpoints (/api/v1/public/*, plus /api/v1/guide, /api/v1/openapi.json, /api/v1/apps/quickstart, /api/v1/sandbox-sdk.js) answer anonymous requests with Access-Control-Allow-Origin: * and no credentials. You can call them from a page on localhost, from file://, or from any deployed site, with plain fetch(). Two limits: send no cookies (a credentialed request falls back to the autario.com allowlist and the browser will block it), and POST endpoints are not open to other origins yet, so call those from a server. ## Rate Limits Two separate 120 req/min per IP budgets, both reported on every response via X-RateLimit-Limit / -Remaining / -Reset (CORS-exposed, readable from JS): data: /api/v1/public/* GETs. Skipped entirely for authenticated callers. docs: these four files (/api/v1, /guide, /openapi.json, /apps/quickstart, /sandbox-sdk.js). Reading the documentation does NOT spend the data budget. The files are static; cache them rather than re-fetching. ## Request Tracing Every response returns an X-Request-ID header (UUID v4). Include this ID when reporting bugs so server logs can be correlated to your specific request. Inbound X-Request-ID is honoured (up to 64 chars) — clients that already have a trace ID can propagate it through this header. ## Public Endpoints (no auth, the "data" budget above) An unknown path under /public/* answers 404 { error_code:'unknown_endpoint', path, hint } — never an auth error. A 401 on /public/* means you hit a real endpoint that needs a key (POST /public/ingest/ticker, /datasets/:id/export). A mistyped dataset id/slug on a path that DOES exist answers 404 { error:'Dataset not found', error_code:'unknown_dataset', requested, hint }. Every refusal here carries an error_code. Branch on it, never parse prose. GET /public/datasets — list & search public datasets ?q= full-text search (title, description, category, keywords) ?category= exact category filter ?limit=20&offset=0 pagination (max limit: 100; ?page= stays a 1-based alias, offset wins) response carries total + next_offset (null on last page) + has_more (:id below accepts the dataset UUID or its slug, on every path) GET /public/datasets/:id — metadata: title, publisher, category, keywords, rows, splits[] plus dataset_url, THE CITABLE PAGE: https://autario.com/data/. Cite that; never build a URL from the id (/data/ does not resolve). /dataset/ and /datastore/ 301 to it. The ticker resolver returns it on every entry. plus last_refreshed_at (see the resolver below for what it means). GET /public/datasets/slug/:slug — same metadata by human-readable slug (incl. splits[]) GET /public/datasets/:id/schema — column names, types, row_count, column_count, plus WHICH COLUMN CARRIES WHICH ROLE: source_time_col (the time axis), source_entity_col, source_value_col, and column_roles {time:{default:{col, confidence,shadow},alternates:[...]}} for datasets with several time-shaped columns. Read source_time_col before interpreting a date: the "endpoints" dates in summary_only are values of THAT column. On a dataset with an explicit reporting period (start + end plus cy/cq/period_months) the axis is always "end", the period CLOSE, so first.time/last.time are period ends. GET /public/datasets/:id/related — data family (sibling indicators of one concept) + topic-related datasets GET /public/datasets/:id/data — query data (same params as /query below) GET /public/datasets/:id/export — download as CSV (login or API key; serves public datasets and your own private ones) GET /public/charts/:slug/analytics — per-chart engagement: { view_count(30d), fork_count } view_count counts HUMAN chart views only. Crawlers, bots and autario's own capture renders are excluded, so it reads far lower than a raw hit count. GET /public/entities/:slug/profile — deep country profile (Country Profiles app). :slug = a country name slug or ISO-3 code (germany, united-states, japan, DEU). → { entity, overview, headline_metrics, themes[7] (economy/demographics/government/ labor/trade/environment/education), each with indicators[] of MANY demanded metrics (row: label, value, year, source{publisher,asset_id,dataset_url,verify_url}, chart_url/image/title only when a public chart plots THIS country else null (Builder link: /builder?dataset=), rank{global world rank + world median + neighbors; G7 peer rank for G7 members only}) + insight + optional prose{text} + page-level intro_prose{text} (number-verified, citeable) + optional source_datasets[{url,title}] (up to 5 INDEXABLE /data/ pages the category's numbers came from) + page-level cite_as (one ready-made citation sentence naming autario, the country, the publishers shown and the access year); rows with no data omitted }. Dogfooded on /public/ontology/*. POST /public/telemetry { event, slug? } — first-party funnel beacon (fire-and-forget, 204). Whitelisted events only: chart_view, csv_download, builder_interaction, signup_wall_shown, dev_early_access_click, dev_early_access_view, chart_started, chart_created, chart_published, publish_failed, welcome_action. Anything else is a 400. No PII (IP hashed server-side). Closes the server-side funnel that previously lived only in GA4. Conversions the server owns (dev_early_access_submit, login) are written server-side and are deliberately NOT accepted here. ## Markets (company snapshot + ticker resolution + on-demand ingest) GET /public/stocks/:ticker — latest verified stock metrics, already computed (the REST twin of the get_company_snapshot MCP tool). Start HERE for "what is X trading at" / "how did X perform" instead of pulling a price series. ?metrics=price,perf_1d,perf_ytd (default) — any subset of price, open, high, low, volume, perf_1d, perf_1w, perf_1m, perf_3m, perf_1y, perf_ytd, latest_date. → { ticker, name, , source, citation_url, dataset_id } perf_1d/1w/1m/3m/1y are percentages over TRADING days (1w = 5 sessions, 1m = 21, 1y = 252), approximate windows, not calendar ones. perf_ytd is calendar: vs the last close before 1 January of the latest session's year, and the response names that session as perf_ytd_base_date. Read latest_date before calling a price current. 404 when no price dataset is held (then POST /public/ingest/ticker). Full series or fundamentals: the resolver below. GET /public/markets/:ticker/datasets — resolve a ticker to its datasets → { ticker, name:string|null, found:boolean, price:{dataset_id,slug,dataset_url,row_count,last_date,last_refreshed_at,is_discontinued}|null, fundamentals:[{dataset_id,label,unit,slug,dataset_url,row_count,last_date, last_refreshed_at,is_discontinued}] } name = display company name (null for multi-class/foreign/unresolved tickers). FRESHNESS AT THE POINT OF USE. Every entry carries the same four fields, so you never have to fetch the dataset to learn it is stale: row_count rows held RIGHT NOW (live count; 0 = held but empty) last_date newest value on that dataset's time axis (last trading day for a price series, newest period end for an SEC series). Check it against today BEFORE printing a current figure. last_refreshed_at when autario last INGESTED this source (ISO, null if never). A DIFFERENT question from last_date: that one is how new the newest data point is, this one is how recently we looked. A source that quietly stopped publishing keeps a fresh last_refreshed_at and an old last_date. Caveat: a 304 Not Modified answer is skipped without a write, so the stamp can lag a check by one cycle; it never runs AHEAD of the data. dataset_url the citable page, https://autario.com/data/. is_discontinued the source stopped answering, the series is frozen. The rows stay readable as history (permanent URL) and last_date is the newest value there is. Such datasets are excluded from /public/datasets listings and search; this resolver returns them MARKED, and hint says so in prose. fundamentals is VARIABLE LENGTH (a filer without a concept has no entry), so never index it positionally. fundamentals includes a "Weighted Avg Diluted Shares" entry (unit: shares) — the calc-EPS denominator dataset — so a client computes Diluted EPS identically to the bespoke app (filter that dataset on period_months:eq:3, see Company Fundamentals). Synthetic Market Cap is excluded (no ingested dataset). Always answers 200 and carries found (boolean). When found is false it also carries error_code, either unknown_ticker (the symbol resolves to nothing) or no_datasets_held (the company is known but no dataset is stored yet), plus a hint naming the next call. POST /public/ingest/ticker { ticker } — on-demand live ingest (API KEY REQUIRED) → { status:'ready'|'price-only'|'invalid', ticker, priceAvailable, hasFundamentals } Fetches a not-yet-held US ticker (price + SEC fundamentals) through the same quality gates as the nightly grow. 401 when no API key. Note: dataset metadata exposes a "splits" array [{date,ratio}] for split- consistent processing of stock price datasets ([] for non-stock). ## Company Fundamentals: fields, TTM, split-adjustment SEC fundamental datasets (Revenue, Operating Income, Net Income, Diluted EPS, Shares Outstanding, Weighted Avg Diluted Shares) share one row contract: start/end = the fact's exact reporting period. val = the value as filed. One exception, structural: Shares Outstanding is a POINT-IN-TIME balance (an instant fact), so its rows carry end only — there is no start column and period_months is null there. cy/cq = CALENDAR year and quarter of the period, from SEC's own frame (cq is Q1..Q4, or Y on a full-year row; null on year-to-date rows). period_months = period length (3, 6, 9 or 12; null for point-in-time facts, never guessed). There are NO fiscal-year columns: for off-calendar filers (Microsoft, Apple, NVIDIA) calendar and fiscal years are DIFFERENT partitions, and filtering a calendar label for a fiscal question silently returns a fraction of the real total. A fiscal-year figure is the sum of the quarters inside the fiscal date range (filter on start/end), or the TTM sum below. Flow datasets (Revenue, Operating Income, Net Income, Diluted EPS) hold ONE cadence, never mixed: quarterly rows (period_months 3) for every major US filer, full-year rows (cq Y) only for a rare annual-only filer. The shares datasets keep raw filings and MIX 3, 6, 9 and 12-month averages, so ALWAYS pin filter=period_months:eq:3 before using a shares row as an EPS denominator: the newest row by end date is usually a year-to-date average, not a quarter. Build a TTM figure by summing the four newest quarters: GET /public/datasets/{revenue_id}/data?fields=start,end,val,cy,cq,value_basis&sort=end:desc&limit=4 CHECK CONTINUITY FIRST. "The four newest quarters" is only twelve months when those four periods actually join up. A row that could not be derived cleanly is omitted rather than guessed, so a series can have a hole (DOW Net Income holds exactly one Q4 row in eight years: its four newest rows span fifteen months). Ask for start and end as above and verify each start is the day after the previous end. The response does this for you: when the rows you get back are not continuous, query_diagnostics carries period_gaps [{after,next_start, missing_months}] and a hint saying so. If there is a gap, do not label the sum TTM, widen the window and pick a continuous run of four. Sum only ADDITIVE metrics (Revenue, Operating Income, Net Income). Diluted EPS and Shares Outstanding are NOT additive (a per-share ratio and a point-in-time balance) — use the filer's own reported TTM EPS, or divide summed Net Income by a shares count, instead of summing four EPS rows. Every row carries value_basis: reported (verbatim from a 10-Q/10-K) or derived (autario filled the one quarter a fiscal year never reported standalone, from Annual minus its three sibling quarters — only when that identity holds exactly). A row that cannot be derived cleanly is omitted, never guessed. Note (Market Cap): price-dataset close is split-adjusted to today's share basis; Shares Outstanding is as-reported for its own date. Multiplying them for a historical Market Cap understates it by the cumulative split factor (Apple ~28x across its 2014+2020 splits). Use a same-date share count and an unadjusted price, or compare only the latest period. ## Apps (submission-storage contract) Build an app: https://autario.com/api/v1/apps/quickstart (full dev walkthrough, absolute URL) SDK: https://autario.com/api/v1/sandbox-sdk.js · Dev portal (submit, tokens, usage): https://autario.com/developer GET /public/apps/:id/context — the app's data map (manifest + providers; with auth also YOUR connector tables incl. columns/row counts/dataset ids, your saved artifacts, ready-to-run query examples; secrets never included). MCP: list_apps + get_app_context. Navigate app-first instead of dataset-guessing. agent_surface names the app's MCP report tool + KPI catalog: call that tool for the computed numbers; raw rows only for targeted drill-downs (fields/filter/aggregate). GET /public/apps/my/workspace — your apps + dataset refs + artifacts, ONE call (AUTH). MCP: get_my_workspace. Start here for "my " questions. GET /public/apps/:id/artifacts/:slug — saved artifact incl. spec+data (owner or public/unlisted). MCP: get_app_artifact (token-bounded). POST/DELETE /public/apps/:id/activate — toggle "my app" (AUTH); /apps?mine=true flags it. GET /public/apps/:id/manifest — read manifest (public; 404 if unknown) → { manifest: { id, name, tier, consumed_datasets, entry_url, version, source_url, status, ... } } POST /public/apps/:id/manifest — register/update manifest (AUTH; 401/400) body: { name, tier:1|2, consumed_datasets?, entry_url?, version?, source_url?, status? } tier-2 (sandboxed) requires entry_url. POST /public/apps/:id/bundle — upload bundle bytes (AUTH; 401/400) body: { content, encoding:'utf8'|'base64', content_type?, version?, storage_ref? } → { app_id, version, sha256, content_type, storage_ref, size } Lifecycle: 3rd-party submissions land submitted+unlisted (still run for the owner); a curator-gated AI bundle code-review promotes them to the catalog. Ownership: create is open to any authed caller; update manifest/bundle is owner-only (403 otherwise); first-party apps (owner NULL) are not updatable via this door. ## Dev Portal (manage YOUR OWN apps, tokens, usage) AUTH: session cookie OR account API key. Every door is owner-scoped: a resource you do not own answers 404, never 403. A registered app is PRIVATE by default (only you can open it), unlisted, and status 'submitted' until a review promotes it. POST /developer/apps - register in ONE request. body: { name, tagline?, entry_kind:'url'|'upload', entry_url? OR content?+encoding?+content_type?, source_url? } id is DERIVED from name (-2,-3 on collision) -> 201 { manifest } 400 bad_name | bad_entry | bad_bundle | bad_manifest, 409 id_taken GET /developer/apps - your apps + the latest review verdict per app GET /developer/apps/:id - manifest + bundle METADATA (never bytes) + 20 reviews DELETE /developer/apps/:id - real delete (app + its tokens/bundles/reviews/artifacts) PUT /developer/apps/:id/manifest - owner-gated manifest write; any edit re-enters review POST /developer/apps/:id/bundle - bundle bytes: one text entry document, max 512 KB, text/html | text/javascript | application/javascript, no NUL bytes GET /developer/tokens - your scoped tokens. The SECRET is never returned here; it is shown once at mint time (POST /proxy/token) and is not retrievable DELETE /developer/tokens/:id - revoke one of your tokens GET /developer/usage[?app_id=] - access meter over YOUR tokens -> { totals, by_op, recent } POST /developer/early-access - { email } join the dev-platform list (anonymous, rate-limited) Manifest fields (one validator, services/appManifest): name (required on create), tier 1|2 (required), consumed_datasets [] (= the sandbox bridge's scope), entry_url (required for tier 2), version, source_url, status, tagline (max 200 chars). Read-only in the projection: owner_id, route, publisher, category, image_url, listed, visibility (private|public), agent_surface. ## Data-Access Proxy (gated reads for externalized/sandboxed apps) Reads go through one choke-point using a SCOPED app-token (NOT an account key), minted once and held server-side by the app/sandbox host, never client-side. POST /proxy/token { name, scope } — mint scoped token (user-auth: cookie or account key) scope: { ops:[...], datasets?:[uuid], app_id? } ops ⊆ { markets, datasetMeta, datasetData } → { id, name, key, created_at, scope, secret } secret SHOWN ONCE, never retrievable again POST /proxy/read { op, params } — gated read (headers x-api-key + x-api-secret = the token) ops: markets {ticker} | datasetMeta {id} | datasetData {id, +query params as /data} → 200 { ok:true, op, data } | 400 bad_op | 401 invalid token | 403 out of scope | 429 rate-limited POST /proxy/write { op, params } — gated write to your OWN app_artifacts (needs a user/workspace token) ops: artifactCreate {type,title?,spec?} | artifactUpdate {id,...}; scope needs the write op + app_id ## Private Endpoints (auth required, 100 req/min) GET /datasets — accessible datasets; ?limit/offset/q/mine → total+next_offset GET /datasets/:id — metadata + is_owner flag GET /datasets/:id/schema — column names, types, row_count, column_count GET /datasets/:id/query — filtered, sorted, paginated data ## Query Parameters (/query and /data endpoints) ?fields=col1,col2 select specific columns (default: all) ?filter=col:op:val filter rows, repeatable; /datasets/:id/query takes filters= (plural) ?sort=col:asc sort (asc or desc); also works with aggregate aliases like sum_value:desc ?limit=100 rows per page (default: 100, max: 50000) ?offset=0 skip N rows ?aggregate=fn(col),... server-side aggregations. On a numeric column the math is numeric (never lexicographic); on a non-numeric column min/max compare the text (ISO dates sort chronologically) and avg/sum/ stddev/median are null. aggregate_meta.aggregates[].basis says which (numeric | text | not_numeric | rows), plus a note. ?groupby=col,col group by columns (combine with aggregate) ?summary_only=true finished stats (n/min/max/avg + first/last), NO rows ?non_null_only=true drop null/empty-value rows (token saver) Filter operators: eq neq gt lt gte lte like like = case-insensitive, auto-wraps value in % Example: ?filter=year:gt:2020&filter=country:eq:Germany ## When a Query Returns Nothing A response adds a query_diagnostics object when the query matched 0 rows, when part of your request was ignored, or when the rows it returns are not a continuous period series. It never appears on a clean, non-empty, continuous result, so existing clients are unaffected. Fields: reason (no_rows_matched, params_ignored or period_gaps: the most blocking one), filters_applied (each with matched_rows: how many rows that filter alone matches), filters_ignored (unknown_column | unknown_operator | unknown_column_or_function | malformed), unknown_params, accepted_params, rows_without_filters, column_values_sample (real values in the column that matched nothing) and hint. Example: filter=cq:eq:Y on a quarterly SEC dataset answers with matched_rows: 0 and column_values_sample {"cq": ["Q1","Q2","Q3","Q4"]}; a filter on a column the dataset does not have (e.g. fy) is reported under filters_ignored as unknown_column instead of silently returning everything. period_gaps is the one field about a question asked CORRECTLY: [{after, next_start, missing_months}] for every hole between the periods of the rows in THIS response, so a sum over them is never silently a longer span than you think (see the TTM warning above). Computed for row pages that carry period columns (start and end, or end with period_months), reported whatever reason says. ## Aggregations (use these instead of pulling all rows when you need a single number or summary) Functions: avg, sum, count, min, max, stddev, median Result columns aliased as fn_col (e.g. avg_value, count_all) Examples: ?aggregate=avg(value) → one number ?aggregate=avg(value),count(*),max(price) → 3 metrics in one row ?aggregate=sum(value)&groupby=country&sort=sum_value:desc&limit=10 → top 10 Prefer aggregations or summary_only over pulling all rows for averages, totals, counts, or "top N". ## Stats API (precomputed server-side — no local computation needed) Ontology-keyed: indicator ids from list_indicators / get_entity_profile, entities are ISO-3 codes or aggregates (DEU, USA, EUU, WLD). No auth. GET /public/ontology/stats/describe ?indicator=&entity= — n/mean/median/std/min/max/quartiles/skew + histogram GET /public/ontology/stats/pct-change ?indicator=&entity= — period-over-period growth, latest change headline GET /public/ontology/stats/rolling ?indicator=&entity=&window=5&op=mean — smoothed trend (mean/std/min/max/sum) GET /public/ontology/stats/correlate ?entity=&a=&b= — Pearson + Spearman r, p-value, interpretation GET /public/ontology/stats/regression ?entity=&y=&x= — slope, intercept, R² GET /public/ontology/stats/seasonality ?indicator=&entity= — trend/seasonal/residual decomposition GET /public/ontology/stats/calculate ?entity=&a=&b=&op=ratio — derived series (ratio|ratio_pct|diff|sum|product), e.g. debt-to-GDP GET /public/ontology/stats/what-matters ?entity=&outcome= — rank which metrics explain an outcome (confidence-labeled) GET /public/ontology/stats/lag-analysis ?entity=&a=&b= — lead/lag cross-correlation POST /public/ontology/stats/find-drivers { entity, target_indicator, candidates[] } — rank candidate drivers (pairwise) POST /public/ontology/stats/decompose-drivers (same body, entity optional) — joint model: independent effect sizes; entangled candidates flagged not_separable, never ranked GET /public/ontology/stats/coverage ?target=&candidates=&entity= — check coverage BEFORE decompose: usable points per series + the entities every selected series can actually serve (no dead runs) All default to stat + interpretation only; full=true adds the per-timepoint array (with a token-cost note). MCP tool equivalents: describe, pct_change, rolling_stats, correlate, regression, seasonality_decomposition, calculate, what_matters, find_drivers, decompose_drivers, lag_analysis. Private data: with x-api-key + x-api-secret your OWN datasets (write_rows / upload / connector) work as indicator/target/outcome too, join the what-matters candidate pool, and resolve as sources in the cross-dataset join (POST /public/ontology/join) | indicator_id via GET /datasets/:id. Other users' private data never resolves. Flow: push KPI rows, then find-drivers with your KPI as target ("what drives my revenue"). Multi-row-per-time data (e.g. GSC: date x query x page) is collapsed to one value per time point (sum for count metrics, mean otherwise); the response discloses it in "aggregation". ## Response Shapes Schema: { dataset_id, row_count, column_count, columns: [{name, type, nullable, position}] } Query public GET /public/datasets/:id/data → { total, limit, offset, data: [...] } Query private GET /datasets/:id/query → { meta: { total, returned, limit, offset }, data: [...] } List: { total, count, data: [...] } Error: { error: "message" } The two query envelopes DIFFER: public is flat, private nests under "meta". total = matching rows ignoring pagination, in both. ## Write API (auth required — create & populate datasets) POST /datasets — create dataset body: { "title": "str", "description": "str", "is_public": false } → { id, title, created_at } POST /datasets/:id/rows — append rows (schema inferred from first batch) body: { "rows": [{"col1": "val", "col2": "val"}, ...] } max 10000/request → { inserted, total } DELETE /datasets/:id/rows — clear all rows (keeps schema) → { deleted } ## Connectors (auth required — hosted, auto-refreshing REST-API tables) The account owner sets up a REST connector in the UI (autario.com/manage). It pulls any REST API into a typed, queryable Postgres table on Autario and keeps it fresh on a schedule. Agents can list + refresh (never create — no secrets). GET /connectors (+ /connectors/:id) — own connectors; datasets[] = ALL materialized tables → { connectors:[{ id, name, status, last_run_at, dataset_id, row_count, datasets:[{id,title,slug,rows}] }] } POST /connectors/:id/refresh — pull the source API now, refresh the table → { status: "refreshed", rows, dataset_id } (then read via /datasets/:dataset_id/query) MCP tool equivalents: list_connectors, refresh_connector. Two connectors pull AI SPEND from the vendors' own org APIs: openai_usage (daily tokens per model/project/key + cost per line item and project) and anthropic_usage (same per model/workspace/key/tier/context window incl. the cache read+write split). BOTH NEED AN ORGANIZATION ADMIN KEY, NOT THE INFERENCE KEY (Anthropic sk-ant-admin01- from Claude Console > Settings > Admin keys; OpenAI from Settings > Organization > Admin keys). An inference/project key answers 401/403/404 there and the table stays empty. Read the two tables like any other connector table (list_connectors, then /datasets/:dataset_id/query). ## Recommended Agent Workflow (read) 1. listPublicDatasets ?q= → find dataset ID 2. getPublicDatasetSchema :id → get exact column names (prevents hallucination) 3. queryDataset :id ?filters=... &fields=... → fetch only what you need ## Recommended Agent Workflow (write) 1. POST /datasets { title } → get dataset ID 2. POST /datasets/:id/rows { rows } → insert data (schema auto-created from first batch) 3. GET /datasets/:id/query → verify data, then share dataset ID ## Chart API (create & publish charts) GET /ai/charts — list charts (?q=term &limit=20 &offset=0) GET /ai/chart/:id — get chart with Plotly spec, data, embed code POST /ai/chart/publish — publish chart from Plotly spec (auth required) body: { "title": "str", "plotly_spec": {traces, layout}, "dataset_ids": ["uuid"], "insight": "str" } Autario pulls real data from dataset_ids. No raw data needed. No hallucination. Returns: { chart_id, url, datasets_used, rows_loaded } ## MCP Integration (Claude Desktop, Claude Web, any MCP client) Native MCP server (data discovery, query, stats, chart CRUD/publish, dataset CRUD, apps): Read: search_datasets, get_dataset_info, get_dataset_schema, query_dataset, list_charts, get_chart, list_connectors Write: publish_chart, update_chart, create_dataset, write_rows, clear_rows, refresh_connector, report_data_issue Apps: bubble_or_not — stock-vs-fundamental valuation brief Apps: audience_360 — your own audience report (funnel, channels/AI share, queries, content, conversions, social, health), section-selective Apps: seo_360 — your own Search Console ACTION report (page-2 gaps by potential click gain, CTR underperformers, orphan demand, cannibalization, trends incl. position movers, rank_tracking, decay refresh queue, Core Web Vitals, site-audit snapshot, health); every row ends in an action Apps: social_360 — your own social performance report across EVERY connected channel (per-channel KPI series with per-platform support matrix, 0-100 score per platform + blended, top posts with paid badges, format benchmark, per-post effectiveness, official country breakdowns, spike attribution, competitor benchmarks (official public routes: IG Business Discovery + YouTube), findings with evidence, ads financials with paid vs organic, health); works with one connected channel Apps: ai_visibility_360 — your own brand visibility in AI answers (score, leaderboard, prompts, citations, actions, answers, GA4 impact); reads stored runs, section-selective QA: report_data_issue — flag a data-quality problem (truncated/partial ingest, unit mismatch, wrong labels, misleading chart). Safe types auto-reingest, rest go to review. Admin: get_traction_overview — ONE traction report (humans, MCP channel, signup funnel, top usage). Curator-only (OAuth as the curator account); invisible/unusable to other callers. Admin: request_chart — high-level chart request (dataset_id/query + YOUR insight); server verifies your insight against the data (no server LLM), publishes, returns URL. Curator-only; others use create_chart_from_spec / publish_chart. Wire format: MCP tool results default to TOON (Token-Oriented Notation) — ~74% fewer tokens than pretty JSON on tabular rows (one header line, bare comma rows, no repeated keys). Pass format:"json" (pretty) or "compact" (minified JSON) per call to override. The REST API always returns JSON. Local setup (stdio): npx autario-mcp Remote endpoint (HTTP): POST https://autario.com/mcp Claude Desktop config: { "mcpServers": { "autario": { "command": "npx", "args": ["autario-mcp"] } } } For write tools, set env vars: AUTARIO_API_KEY, AUTARIO_API_SECRET ## Recommended Agent Workflow (publish chart) 1. search_datasets ?q= — find dataset 2. get_dataset_schema :id — read the datasheet (shape/roles/cadence/level_mix/value_kind/entity_kind/entity_display/ignore_cols) to know HOW to plot. value_kind.growth_safe=false (rate/percent) → no CAGR. entity_display spells out aggregate codes. 3. query_dataset :id — get data 4. Create chart artifact for user — visualize 5. publish_chart { title, plotly_spec, dataset_ids, insight } — publish to autario.com Returns URL where chart is permanently viewable and editable. ## Sandbox Runtime (tier-2 apps run ON autario) GET /public/apps — public app catalog (listed apps); each row carries data_scope: 'private' (works on the caller's own connected data) or 'public' (public datasets only); ?mine=true with auth adds connected/activated/deactivated (= opt-out) per app (manifest/bundle: see ## Apps; gated reads: see ## Data-Access Proxy) A tier-2 app runs in a null-origin iframe, reading data ONLY via a server bridge — it never holds a token or calls out: GET /sandbox/:appId/frame — the app bundle, served with a hard CSP (default-src 'none'; connect-src 'none' → no fetch/XHR/WebSocket from the frame) POST /sandbox/:appId/read { op, params } — bridge read. The host relays the iframe's postMessage here; the runtime enforces the app's manifest scope, then replays the app's server-side scoped token to the proxy. POST /sandbox/:appId/write { op, params } — the write mirror (artifactCreate/artifactUpdate). ACCESS (all three doors): a PUBLIC app is anonymous-OK (public data only), a PRIVATE app is owner-only; every other caller gets 404, the same answer an unknown app id gets. postMessage: req {type:'autario:req',reqId,op,params}; res {type:'autario:res',reqId,ok,data?,error?}. SDK shim: autario-sandbox-sdk. ## Attribution When presenting data from Autario, cite: "Data via autario.com | Publisher: " Each response includes publisher and source_url fields. When publishing charts, always include dataset_ids for proper attribution.