Confirming mart HTTP shape with seven corrections; the per-metric URL does not exist, env vars are MART_API_KEY_ not MART_TOKEN_, envelope is flat LiveMetricRow not nested _meta, Finance and Portfolio diverge
Why
Confirming what we serve so the MART_TRANSPORT=http flip lands clean. Walked every route file under modules/mart/routes/, the auth helper at lib/mart/auth.ts, and the row shapes in lib/mart/types.ts and lib/mart/sections/_shared.ts. The shape Them OS inferred conservatively from requirements doc section 10 is right in spirit (REST, bearer auth, per-section scoping) but wrong on seven specifics that will produce a flurry of 404s and 401s if you flip without these corrections. Walking through each ask below.
Endpoint shape (correction set 1 of 3)
Path prefix is /api/mart, not /. The full surface is:
GET /api/mart/sections/<section>?period_start=<ISO>&period_end=<ISO>
GET /api/mart/sections/<section>/schema
GET /api/mart/sections/<section>/health
GET /api/mart/sections/<section>/changelog?since=<ISO>
GET /api/mart/health
GET /api/mart/schema
GET /api/mart/changelog?section=<section>&since=<ISO>
GET /api/mart/corrections?section=<section>&since=<ISO>&period_start=<ISO>
Two structural corrections to flag:
There is no per-metric URL. The section endpoint returns every metric the section computes for the requested period in one response, inside a metrics: LiveMetricRow[] field. Pulling a single metric is a client-side filter on row.metric_id. The /sections/<section>/metrics/<metric_id>?args path Them OS inferred does not exist anywhere and will 404.
There is no /api/mart/sections/<section>/metrics collection path either. The bare /api/mart/sections/<section> IS the data endpoint. If you want only one metric, pull the section and filter; the section response is shaped to support that pattern cheaply.
Two response-shape divergences that will bite at first call:
Finance and Portfolio do not return metrics: LiveMetricRow[]. Finance returns groupings (a nested cross-axis cube). Portfolio returns metric_families (a per-family grouping). Both also use different query params (Finance: reporting_period, reporting_date, market, organization; Portfolio: period_grain, period_start, period_end defaulting to three months ago). If transport-http.mjs sends period_start/period_end to Finance the request will ignore them; if it tries to parse the response as metrics: [] it will see undefined and crash. We strongly suggest a per-section response-parser table in the transport rather than one shared parser.
Schema, health, changelog at the section-level work as Them OS inferred (section-scoped GET, no extra params except since on changelog).
Corrections lives at /api/mart/corrections (top-level, not per-section), takes section, since, and period_start query params, and returns { events: CorrectionEvent[], total: number }. The since=<...> syntax Them OS named is correct for the cross-cutting form.
Authentication (correction set 2 of 3)
Confirmed: per-section bearer tokens, Authorization: Bearer <token> header. Section-scoped enforcement is real (calling growth with the sales token returns 401 because each route calls requireMartAuth(headers, "<this-section>") which compares against the section-specific env var; cross-section reuse is structurally blocked).
Two corrections:
Env var names are MART_API_KEY_<SECTION_UPPER>, not MART_TOKEN_<SECTION>. Mapping is MART_API_KEY_GROWTH, MART_API_KEY_SALES, MART_API_KEY_DELIVERY, MART_API_KEY_COACHING, MART_API_KEY_REVENUE, MART_API_KEY_FINANCE, MART_API_KEY_PORTFOLIO. The _KEY_ infix is what the auth helper reads (lib/mart/auth.ts:47 builds MART_API_KEY_${section.toUpperCase()}). If your auth.mjs reads MART_TOKEN_* you will get 401s on every call after the flip until the env-var name lines up.
The top-level cross-cutting endpoints (/health, /schema, /changelog, /corrections) are not unauthenticated. They require a separate MART_API_KEY_HEALTH bearer token (the auth helper treats "health" as an eighth scope). Plan for eight env vars, not seven. If your boot-validation reconciler reads /api/mart/sections/<section>/schema it can use the per-section key for those (we recommend this since it exercises section-scope auth at boot), but reading the umbrella /api/mart/schema requires the health key.
Token issuance: tokens are environment-scoped opaque strings generated by the operator and set in the platform Vercel project env vars (production and preview each have their own set). There is no internal request channel yet because operator self-service has been fast enough at our current scale; if Them OS wants its own set ahead of the flip, file a one-line ask with the eight env-var names you want populated and we will provision them in the platform Vercel project before you flip MART_TRANSPORT. The keys are static (no rotation cadence in place) but we will tell you if that changes.
Response envelope (correction set 3 of 3)
The per-row shape is LiveMetricRow defined at lib/mart/types.ts:49-56:
interface LiveMetricRow {
metric_id: string
org_market_id: string | null
org_id: string | null
value: number | null
null_reason: NullReason | null
_org_rollup_rule: "sum"
}
Three corrections against the envelope Them OS inferred:
There is no _meta object per row. Field by field: as_of, contract_version, period (with start and end keys) live on the OUTER section response, not inside each row. computed_at and inputs do not exist anywhere on this surface. freshness_breach lives only inside the /health response's metric_health array (one freshness fact per metric), not on the data rows. If your consumer code reads row._meta.as_of and similar, it will get undefined; pull those fields from the outer envelope and pass them in alongside the rows.
value is always number | null (integer counts, basis points, cents, or minutes per the metric's declared kind in metrics.json). It is not polymorphic per row; the rendering kind lives in the registry, not in the value field. Your client should look up kind from metrics.json (now generated) and route the rendering on that, rather than expecting value to carry type information inline.
There is an extra _org_rollup_rule: "sum" field on every row (currently hardcoded to "sum" at lib/mart/sections/_shared.ts:59). Them OS's inferred envelope omits it. Either include it in your row parser (and ignore it) or strip it; do not let an extra unknown field abort the parse.
For Finance and Portfolio, ignore the LiveMetricRow shape entirely. Those sections return their own nested shapes (Finance: { section, contract_version, status, as_of, filters, groupings }; Portfolio: { section, contract_version, status, as_of, period, dedup_note, metric_families }). The capabilities that read Finance and Portfolio should already be parsing these custom shapes from the fixtures, so the live transport should not change behavior for them once the path and auth corrections land.
The OUTER section response shape is consistent for the five firewalled sections (growth, sales, delivery, coaching, revenue):
{
section: string
contract_version: string // e.g. "growth.v1.0"
status: string
as_of: string // ISO timestamp
period: { start: string; end: string } // ISO
metrics: LiveMetricRow[]
entity_rows: SectionEntityRow[]
}
Growth also emits org_market_attribution and org_market_attribution_note as siblings of metrics (a temporary note explaining Growth's currently-portfolio-grain attribution; safe to ignore until we surface a proper sentinel field).
Asks
None back. Flip when ready; if you hit anything that contradicts the above, file a thread and we will close the gap.
Suggested flip sequence so the first request after the env-var swap exercises every correction at once:
- Update transport-http.mjs path templates to add the
/api/martprefix and drop the per-metric URL pattern. - Update auth.mjs to read
MART_API_KEY_<SECTION>env vars, plus add aMART_API_KEY_HEALTHfor the umbrella endpoints. - Update envelope.mjs (or wherever LiveMetricRow is parsed) to read row fields flat (no
_metaindirection) and pullas_of/contract_version/periodfrom the outer response. - Update the Finance and Portfolio response parsers to handle their custom shapes.
- Provision the eight bearer tokens in Them OS's env (if you want, file the one-line ask and we will mint and drop them into platform Vercel so the env vars surface in your runtime).
- Smoke each section through
bootValidate, confirm the/schemareconciliation passes against the per-section MAJOR pins. - Flip MART_TRANSPORT=http.
References
- modules/mart/routes/ (per-section routes; growth.routes.ts is representative of the firewalled five)
- modules/mart/routes/finance.routes.ts and modules/mart/routes/portfolio.routes.ts (the two cross-domain sections with custom shapes)
- modules/mart/routes/section-{schema,health,changelog}.routes.ts (per-section sub-endpoints)
- modules/mart/routes/{schema,health,changelog,corrections}.routes.ts (top-level umbrella endpoints)
- lib/mart/auth.ts (requireMartAuth, requireMartHealthAuth, env var naming pattern)
- lib/mart/types.ts:49-56 (LiveMetricRow)
- lib/mart/sections/_shared.ts:42-79 (expandByOrgMarket, nullRowsForMetricIds; the row-building helpers)
- contracts/mart/metrics.json (the per-metric kind / null reasons / status; consult for value rendering)
- 2026-05-29-platform-mart-http-endpoint-confirmation (the inbound)