Developers

The layer other products are built on.

One search and retrieval primitive over the normalised graph, exposed over REST, MCP, A2A and CLI — versioned, spec-first, and plumbed to the standards agents already speak. MaterialGraph’s own products call it the same way yours will.

2,536

Brands

~204K

Products

~455K

Variants

~18M

Attribute values

31

Blocks

Visual layer

Three questions, answered in your identifiers.

Given a material, what else looks like it. Given a photograph, which materials are in it. Given a colour, what sits near it. Each answer is a ranked page of real material records — and for every orderable material, the record carries the SKU you already key your own systems by.

Given a material

what else looks like it.

Seed with a variant id or a Material Bank SKU. Back comes a ranked page of look-alikes, scored by visual likeness of the product imagery itself.

Likeness is visual, computed from imagery — a look-alike shares an appearance, not necessarily a specification.

Given a photograph

which materials are in it.

Post a captured image. Back come candidate materials for what the photograph shows, each scored, each a real record in the graph.

A match is a visual candidate bounded by what the photograph shows — not an identification.

Given a colour

what sits near it.

Post a hex value or a weighted palette. Back come the materials whose imagery sits nearest it, across every brand at once.

Colour is derived from product imagery, never from a manufacturer's stated colour specification.

Boundaries

What MaterialGraph does not do.

MaterialGraph returns identity and order — which materials, in what ranking. Everything commercial stays where it already lives. Adopting the visual layer adds no new holder of stock, price, or permission anywhere in your architecture.

No inventory

Stock is resolved in your stack, after ranking.

No pricing

Commercial terms never enter or leave the graph.

No entitlements

Who may see what is your rule set, applied by you.

No user

Requests carry a key, never an identity or a session.

No cart

Ordering happens where it already happens.

The one behaviour that moves: a rail’s stock state comes from your own product lookup, because MaterialGraph holds no inventory and never will. It is the only change on the consuming side, and it is stated here rather than discovered late.

The call

Seed with the SKU you hold. Receive SKUs back.

One POST. The seed is a Material Bank SKU — the one external identifier the API accepts, so a caller already holding one needs no resolve round trip. The response is a ranked page of look-alikes, each carrying materialBankId, and corpusScope: "materialBankOrderable" restricts retrieval to materials carrying that identity — applied inside the query, so the page fills with eligible rows rather than being thinned after the fact. A SKU that names no variant is a 400 naming the offending value, never an empty page.

curl -X POST https://beta.materialgraph.com/api/v1/search \
  -H "Authorization: Bearer $MG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "similarTo": { "materialBankId": "102082383" },
    "corpusScope": "materialBankOrderable",
    "limit": 12
  }'
Runs server-side with the demo key — no key in your browser.

Identifiers

Your identifiers, in both directions.

98.47%

of visible variants carry materialBankId — the caller’s own SKU, on the record itself. Seed with a SKU, receive SKUs. Nothing to map, no crosswalk to maintain, no id to translate.

The remainder is the corpus MaterialGraph carries beyond the Material Bank feed — and corpusScope: "materialBankOrderable" removes it server-side, inside the query, before the page is filled. Under the scope, every result carries your SKU and a rail is never short. That is what post-filtering a returned page cannot give you.

Visible variants445,192
Carrying materialBankId438,369
Brands · with a Material Bank identity947 · 931

Counted against production, 2026-08-07. “Visible” is the same predicate the search arms apply.

Numbers

Measured over the wire, on the path you would take.

SKU-seeded similar, limit: 12, over the public wire against the production deployment — 80 requests per leg, zero failures, 2026-08-07.

Unscoped — the whole graph

242ms

p50

382ms

p95

With corpusScope: "materialBankOrderable"

278ms

p50

440ms

p95

The scope costs 36ms at p50. A first invocation on a fresh function instance runs ~2.3s — serverless cold start, not the database, which never suspends.

Worked scenario

A product page wants a similar-materials rail.

Your existing machinery already does every step but the first.

  1. 1Seed with the SKU the product page holdsthe new call
  2. 2Take the ranked SKUsyours, unchanged
  3. 3Hydrate via your product service — images, stock, priceyours, unchanged
  4. 4Apply your own entitlement rulesyours, unchanged
  5. 5Renderyours, unchanged
The rail
// 1 · Seed with the SKU the product page already holds — the only new call.
const { results } = await searchSimilar({ materialBankId: sku });

// 2 · Take the ranked SKUs.
const ranked = results.map((r) => r.materialBankId);

// 3 · Hydrate through your own product service — images, stock, price.
const products = await yourProductService.byVariantSkus(ranked);

// 4 · Apply your own entitlement rules. MaterialGraph never sees them.
const visible = products.filter(yourEntitlementRules);

// 5 · Render. Over-request depth (limit: 12 → render 8–12) so your
//     own rules can remove results without the rail running short.
render(visible.slice(0, railSize));

Scoping

Ask for enough, and say which corpus.

Two knobs keep a rail full when your own rules will remove some of what we return: scope server-side what the engine can gate, and over-request depth for the rules only you can apply.

Scope what the engine can gate

corpusScope: "materialBankOrderable" is applied inside the query, so a page of N is N post-scope — orderability costs you no over-fetch. Absent means the whole graph; absence is never a narrower default. An unknown scope is a 400 naming it, never a silent widening. When a scope narrowed the corpus, the response says so under corpusScope.

Over-request for the rules only you can apply

To render N results when your own rules remove an expected fraction k, request limit = ceil(N / (1 − k)) — up to the per-page cap of 48 — and hold the response’s cursor for backfill if the page still runs short. For a rail of 12, one call at limit: 24 tolerates half your candidates being removed.

Read responses the way they are written: a page is a page, not a corpus — opt-in counts describe the match set, each with the basis it states. An absent field means unknown, never false. And ids are opaque tokens — round-trip them verbatim.

Proof

Material Desk runs on the published SDK.

A different product, a different team — and no private API. Every screen Desk draws reached the graph through the same versioned surface documented on this page, consumed through the published client. If the public surface can’t do something, neither can we.

Downstream surfaces that need a governed, rights-aware slice of a record consume the .mgx twin — the same graph, compiled for one consumer.

The seam
npm install @instruments/materialgraph

import { MaterialGraphClient } from "@instruments/materialgraph";

const graph = new MaterialGraphClient({
  apiKey: process.env.MATERIALGRAPH_API_KEY,
});
// one generated package, one env var, one instantiation

First call

Verify your key, then run a real query.

The API is called server-side — a bearer key belongs on your backend, never in client JS. The buttons below run each request through our server with a read-only demo key, so you see real data now.

1 · Confirm the credential resolves — GET /api/v1/whoami:

curl https://beta.materialgraph.com/api/v1/whoami \
  -H "Authorization: Bearer $MG_API_KEY"
Runs server-side with the demo key — no key in your browser.

2 · Post a weighted palette; get the closest materials across every brand:

curl -X POST https://beta.materialgraph.com/api/v1/search \
  -H "Authorization: Bearer $MG_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "colours": [
      { "hex": "9CAF88", "weight": 0.6 },
      { "hex": "E8E0D0", "weight": 0.4 }
    ],
    "colourTolerance": "normal",
    "limit": 5
  }'
Runs server-side with the demo key — no key in your browser.

3 · Read the full record — identity, colour, category, palette, facets, and typed dimensions — by variant id:

curl https://beta.materialgraph.com/api/v1/materials/6g23prj45moq6scfh2mn4?include=siblings \
  -H "Authorization: Bearer $MG_API_KEY"

Authenticate

Four key tiers, one header, called from your backend.

Send your mg_read_… key as Authorization: Bearer. It grants the mg:read scope, every error is an RFC 7807 problem document, and — because a bearer key is a secret — the call belongs server-side, never in client JavaScript. There is no CORS: browsers reach the API only through your own backend.

The key today

mg_<read|compose|ingest|write|admin>_…

Sent as Authorization: Bearer or X-API-Key. Prefix-scoped, compared in constant time.

Scope hierarchy

readmg:read
composemg:read, mg:compose
ingestmg:read, mg:ingest, mg:compose
writemg:read, mg:write
adminmg:read, mg:write, mg:admin, mg:ingest, mg:compose

RFC 8693

Token exchange — 300s tokens, a delegation act chain, scope narrowing only.

POST /api/auth/token

RFC 9728

Protected-resource metadata, discoverable by any OAuth client.

/.well-known/oauth-protected-resource

RFC 7807

Every error is a typed problem document — type, title, status, detail.

application/problem+json

  1. 1 · Now

    Env-provisioned scoped keys

    Keys are minted by the MaterialGraph team and read from the environment, compared in constant time.

  2. 2 · Next

    DB-backed keys + self-serve portal

    Rotation, per-key rate limits, and self-service issuance.

  3. 3 · Endgame

    WorkOS orgs + client-credentials

    Client-credentials mint RS256 JWTs from a MaterialGraph issuer with a /.well-known/jwks.json.

The wire format never changes — Authorization: Bearer.

Get a key

An mg_read_… key, minted for you.

Keys are provisioned by the MaterialGraph team today. Operators mint a scoped mg_read_* key on the spot from the operator desk (shown once, copy-and-go); external partners request one below and we deliver it for your backend’s environment.

Generate a client

A typed client from the spec. No SDK to install.

The whole surface is one OpenAPI 3.1 document. Point openapi-generator (or zod-openapi) at it for a fully typed client in your language — the exact path the Material Desk app uses.

generate
curl -O https://beta.materialgraph.com/openapi.yaml

npx @openapitools/openapi-generator-cli generate \
  -i openapi.yaml \
  -g typescript-fetch \
  -o ./mg-client

Reference

Every endpoint, generated from the contract.

137 operations across 17 groups — parameters, request and response schemas, typed errors, and a copy-paste curl for each. Generated from the same OpenAPI document served at /openapi.yaml, so it can never drift from what the API actually serves.

Colour

GET/api/v1/colours/facetsmg:read

Enumerate the colour value dictionaries

The three canonical colour vocabularies — family, mood, and undertone — each with its full list of terms and how many material variants and brands carry every term. This is the source for a canvas's colour filters: every canonical term appears even at zero counts, terms are sorted most-used first, and the dictionaries are returned in the order family, mood, undertone. No query parameters. The dictionaries and their counts shift only when an ingest lands, so responses carry `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Response

200The colour value dictionaries with live counts.
countinteger

The total number of terms across all three colour vocabularies.

dictionariesobject[]

The colour vocabularies, in the order family, mood, undertone.

attributeIdenum

Which colour vocabulary this dictionary describes.

colour_familycolour_moodcolour_undertone
namestring

The display name of the colour vocabulary.

descriptionstring

A short explanation of what this colour vocabulary captures.

termsobject[]

The canonical terms, most-used first (by variant count, then term id).

Example

curl
curl 'https://beta.materialgraph.com/api/v1/colours/facets' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/colours/palettesmg:read

Build a palette from a seed colour or a phrase

The GENERATIVE colour endpoint, and the only one — its neighbours run the other way: `/api/v1/search/palettes` takes colours in and finds materials, `/api/v1/search/extract-palette` reads a palette off an image. Send EITHER `seedHex` with a `harmony`, OR a text `seed` for a brief that names no colour ("coastal, weathered"); sending both is a 400 rather than a silent preference for one. Deterministic by construction: the same request always returns the same palette, in every process, which is what makes a palette citable. Every colour carries a human name and its WCAG contrast against white and black, because a palette that cannot carry text is not usable in a specification. No database read and no model call, so no cache window. Requires the `mg:read` bearer scope.

Request body

harmonyenum

How to derive the palette from `seedHex`. Required with `seedHex`, ignored with `seed`.

complementaryanalogoustriadicsplit-complementarytetradic
seedstring

A phrase to derive a palette from, for a brief that names no colour ('coastal, weathered'). Deterministic: the same phrase always returns the same palette. NOT interpreted semantically — it does not know what 'coastal' looks like, only that the string maps to a stable, well-spaced set.

seedHexstring

A six-digit hex colour to build the palette around, with or without the leading '#'. The seed is always returned first.

sizeinteger

How many colours to return for a text seed. 1-8; the library clamps past that, so a larger request is refused rather than quietly narrowed. Ignored with 'seedHex', where the harmony decides the count.

Response

200The derived palette, seed first for a harmony.
basisoneOf

What the palette was derived from, echoed back so a stored result explains itself without its request.

kind: harmonyobject
harmonyenum
complementaryanalogoustriadicsplit-complementarytetradic
kind"harmony"
seedHexstring
kind: textobject
kind"text"
seedstring
coloursobject[]

The palette. For a harmony the seed is first, then the derived colours.

hexstring

The colour as '#rrggbb', lowercase.

legibilityobject

Contrast against white and black, and whether each clears WCAG AA. White and black rather than a real background because this is palette exploration, not layout — the pair brackets the useful range.

namestring

A human colour name from the shared naming tables, e.g. 'dark green'. Descriptive, not a manufacturer's colour name.

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/colours/palettes' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "harmony": "complementary",  "seed": "…"}'
GET/api/v1/colours/profilesmg:read

Get the representative colour palette of one or more categories

The colours a product category actually comes in, for up to 8 categories in ONE call, at either taxonomy tier. Each profile carries the category's display `name`/`path`, its total coloured variants, its variant-weighted mean OKLab lightness and chroma, and its representative colours as hex + OKLab + variant count + proportion. `groupBy=group` accepts product-type GROUP keys and merges every leaf type under a group into one bucket histogram BEFORE ranking, so a group's palette is the group's palette rather than the union of its leaves' heads. `groupBy=merged` merges across the KEYS as well and returns a SINGLE profile over the union of their leaves — the palette of a caller-defined facet such as "Wood" (`wood_surface,wood_flooring`) that the taxonomy carries no single node for; its keys may sit at either tier, `categoryKey` is those keys sorted and joined with `+`, and `members` hands them back so nothing has to split that key. `select=diverse` runs a greedy max-min OKLab sampler (0.02 minimum separation, backfilled by frequency if fewer survive) so the swatches are visually spread — the one thing a client cannot reconstruct from a truncated frequency list. `basis` reports the snapshot behind the answer: how many variants the underlying materialized view holds and when it was last refreshed, so a partial or stale view is visible rather than assumed away. A category with no colour data returns a zero-count profile, not a missing key. This is a category-level aggregate, not a per-material read, so responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Parameters

productTypeIds
stringquery1–8 comma-separated category keys from `taxonomy/categories` — product-type ids, group ids when `groupBy=group`, or keys at either tier when `groupBy=merged`.
groupBy
enumqueryHow the keys map onto profiles. `type`/`group` name a taxonomy tier and return one profile per key, `group` merging each group's leaf product types into one palette. `merged` returns ONE profile over the union of every key's leaves.
topN
integerqueryRepresentative colours to keep per category. The ceiling leaves headroom for client-side diversity sampling over a frequency list.
select
enumquery`frequency` returns the most common colours; `diverse` returns the most visually distinct ones (0.02 minimum OKLab separation).

Response

200One colour profile per requested category, in request order — or, under `groupBy=merged`, a single composite profile.
profilesobject[]

One profile per requested category, in request order — or, under groupBy=merged, a single composite profile.

categoryKeystring

The selected category's taxonomy key — a product type id, a group id under groupBy=group, or the sorted keys joined with "+" under groupBy=merged.

levelenum

Which taxonomy tier `categoryKey` names, or `merged` when it is a composite over a caller-defined key set.

grouptypemerged
namestring

Display name of the category, from the taxonomy.

pathstring

Display-name path, " / " separated, e.g. "Tile Surfaces / Ceramic and Porcelain Tile". A merged profile joins its members' paths with " + ".

membersstring[]

Under groupBy=merged, the caller's keys in composite-key order, so `categoryKey` never has to be split. Absent for single-category profiles.

variantCountinteger

Total coloured variants behind this category in the materialized view.

avgLightnessnumber

Variant-weighted mean OKLab lightness across the category.

avgChromanumber

Variant-weighted mean OKLab chroma across the category.

coloursobject[]

The category's representative colours, in the order the chosen `select` mode produced.

basisobject

The snapshot these profiles were read from, so a caller can see it is reading a partial or stale view.

variantCountinteger

Total variants materialized in `mv_category_colour_profiles` across ALL categories — the size of the snapshot these profiles are computed from, not the size of the corpus.

refreshedAtstring| null

When the view was last refreshed (ISO 8601), or null if it has not been refreshed since refresh-time tracking was added.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/colours/profiles?productTypeIds=text' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/colours/statsmg:read

Get corpus-level colour coverage

How much of the graph can answer a colour question at all: total variants and the share carrying an extracted dominant colour (`corpus`), the split across the canonical colour families with per-family variant and brand counts (`families`), and coloured-variant coverage per product-type group (`groups`). Every number comes from an aggregate that already exists, so the call stays cheap; the three sections are at DIFFERENT freshnesses on purpose and `basis` states the source and freshness of each, plus `basis.omissions` — the questions this endpoint deliberately does not answer, listed rather than silently absent. No query parameters. This is a corpus-level aggregate, not a per-material read, so responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Response

200Corpus colour coverage, by family and by product-type group.
corpusobject
variantsinteger

Total material variants in the graph.

withColourinteger

Variants carrying an extracted dominant colour (OKLab lightness present).

coveragenumber

`withColour / variants`, 0–1 — the share of the corpus that can answer a colour question.

familiesobject[]

Colour-family coverage, most-used first.

idstring

The canonical `colour_family` term id.

labelstring

Human-readable family name.

variantsinteger

Variants classified into this family.

brandsinteger

Distinct brands with a material in this family.

groupsobject[]

Per product-type-group colour coverage, most-covered first.

keystring

Product-type group key, as returned by `taxonomy/categories`.

namestring

Display name of the group.

productTypesinteger

Leaf product types under this group with colour data.

variantsinteger

Coloured variants under this group, per the per-category colour view.

rowsobject[]

The brand x category x colour-family matrix — one row per combination, each carrying `brandId`/`brandSlug` to join on. Present ONLY when `groupBy=brand,category` or a `brandId`/`brandSlug`/`categoryKey`/`family` scope was asked for; ABSENT means nobody asked, never that the matrix is empty. `families` above is NOT scoped by those parameters and stays platform-wide under all of them.

brandIdstring

Opaque brand id — the join key for `brands` reads.

brandSlugstring

Brand slug, the stable public identifier used in routes.

brandNamestring

Brand display name.

categoryKeystring| null

Product-type key, as `taxonomy/categories` issues them. NULL means the variants counted here carry NO accepted primary product type — a real population, not an error and not a residual bucket. Those rows are included so the matrix sums to the corpus; dropping them would make it quietly short.

familystring

Canonical `colour_family` term id.

variantsinteger

Distinct variants at this (brand, category, family). Counts variants carrying an ACCEPTED `colour_family` value — not every variant of the brand, and not variants whose family is only proposed.

rowsMatchedinteger

Rows matching the query BEFORE the response cap — the honest total. Compare against `rows.length`: a shortfall means `rows` is a prefix, and `rowsTruncated` says so.

rowsTruncatedboolean

TRUE when the matrix exceeded the response cap and `rows` is a prefix rather than the whole answer.

basisobject
corpusstring

Where the corpus totals came from, and how fresh they are.

familiesstring

Where the family counts came from, and how fresh they are.

groupsstring

Where the group coverage came from, and how fresh it is.

groupsRefreshedAtstring| null

When the per-category colour view was last refreshed (ISO 8601), or null if unknown.

groupsVariantCountinteger

Variants materialized in the per-category colour view. Compare against `corpus.withColour`: a large shortfall means the group section is a partial snapshot.

rowsstring

Where the `rows` matrix came from, how fresh it is, and what it excludes. Present only when `rows` is.

omissionsstring[]

Questions this endpoint deliberately does NOT answer, and why — never silently absent.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/colours/stats' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/colours/provenancemg:read

Get per-brand paint colour provenance

Which paint brands publish a colour value and which do not. Per brand: the paint variants MaterialGraph holds (`paintVariants`, the denominator), how many carry a colour DECLARED by the manufacturer (`declared`), how many carry one DERIVED from product imagery (`derived`), how many carry neither, and `declaredCoverage` — the number that matters. The population is paint and coating product types only, because a declared colour is not expected of a stone slab and counting its absence would be a slur rather than a measurement. `declared` and `derived` OVERLAP and are not a split: most declared paints also carry a derived reading, and only `neither` is exclusive. There is deliberately no corpus average in the response — a single rate is the one shape that hides the finding, since a handful of publishing brands would mask every brand that publishes nothing. `totals` therefore carries counts, including `brandsPublishingNothing`. No query parameters. This is a corpus-level aggregate, not a per-material read, so responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Response

200One row per brand, largest paint population first, with corpus totals and the stated basis.
brandsobject[]

One row per brand, largest paint population first.

brandstring

Brand display name, or `(unattributed)` for paint variants with no brand on record.

paintVariantsinteger

The denominator: variants whose PRIMARY accepted product type is a paint or coating type.

declaredinteger

Variants carrying an accepted `colour_hex` with `source_basis = declared` — the brand's own published value.

derivedinteger

Variants carrying an extracted dominant colour read from product imagery. Overlaps `declared`; not a complement of it.

neitherinteger

Variants with NO colour from either side — MaterialGraph holds no colour for these at all.

declaredCoveragenumber

`declared / paintVariants`, 0–1. The number that matters.

totalsobject
brandsinteger

Brands with at least one paint variant.

brandsPublishinginteger

Brands with at least one declared colour value.

brandsPublishingNothinginteger

Brands whose declared count is zero — they publish nothing.

paintVariantsinteger
declaredinteger
derivedinteger
neitherinteger
variantsWithoutDeclaredinteger

`paintVariants - declared` — paint variants with no manufacturer-published colour, including the partial gaps inside publishing brands.

basisobject
populationstring

How the paint denominator was chosen, and from which tags.

declaredstring

What counts as a declared colour value.

derivedstring

What counts as a derived colour value.

omissionsstring[]

Questions this endpoint deliberately does NOT answer, and why — never silently absent.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/colours/provenance' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/colours/consistencymg:read

Audit whether a brand's declared colour hex agrees with its declared light reflectance

Compares each variant's DECLARED colour hex against its DECLARED light reflectance value and reports the residual — `lrv` minus the sRGB relative luminance of the hex, both on 0–100. The instrument CANNOT say which side of a divergence is wrong: the hex, the LRV, the sheen (LRV is finish-specific), the observer/geometry mismatch between BS 8493 and sRGB, and legitimate gamut clipping all land in the same number. The signal is declared-colour INCONSISTENCY, never "the LRV is wrong". Read `verdict` before any number. A brand whose LRV was COMPUTED from its hex piles every residual inside two-decimal rounding and would otherwise read as a clean bill of health for data carrying no independent information; two screens run first — the zero spike, and whether median |Δ| rises with chroma as optical error must — and such a brand comes back `circular` and MUST be excluded from any consistency claim. `verdict.reason` states which screen fired. `bands` splits the residuals into ≤1 / 1–5 / 5–20 / >20; a |Δ| of 1–5 is expected optical residual, not fault, and only |Δ| above `thresholds.defect` is a candidate defect. `brand` is optional and scopes the audit, because the corpus-wide join is the heavy one; omit it and `report` is null while `brands` still lists every auditable brand with its pair count, so no caller has to guess a name. `worst` bounds the offender list — a page of examples, never the raw pairs. `thresholds` carries the kernel's own constants so no client restates them. Requires the `mg:read` bearer scope.

Parameters

brand
stringqueryBrand display name, exactly as `brands[].brand` reports it. Omit to receive the auditable-brand index alone, with `report: null`.
worst
integerqueryHow many of the largest residuals to return in `report.worst`. Ignored when no `brand` is given.

Response

200The auditable-brand index, plus the audit for `brand` when one was given.
brandsobject[]

Every brand holding declared hex + declared LRV pairs, most first. Counts only — always returned, so `brand` never has to be guessed.

brandstring

Brand display name, as `brand` accepts it.

pairsinteger

Variants of this brand carrying BOTH a declared colour hex and a declared light reflectance value.

reportobject| null

The audit for `brand`, or null when no `brand` was asked for.

brandstring
pairsinteger

Pairs measured for this brand, after the skips below.

skippedobject
bandsobject

Residual counts per band. |Δ| of 1–5 is EXPECTED optical residual, not fault.

medianAbsDeltanumber| null
defectsinteger

Pairs above `thresholds.defect` — the actionable count.

verdictobject
worstobject[]

The largest residuals, bounded by `worst`. A page of examples, never the raw pairs.

thresholdsobject

The kernel's own constants, carried over the wire so no client restates them.

zeroSpikeEpsilonnumber
zeroSpikeCircularnumber
flatChromaTolerancenumber
minSamplesinteger
defectnumber
bandsstring[]

The residual band ids, in order.

bandNotestring
basisobject
populationstring
instrumentstring
screensstring
omissionsstring[]

Example

curl
curl 'https://beta.materialgraph.com/api/v1/colours/consistency' \  -H "Authorization: Bearer $MG_API_KEY"

Answers

POST/api/v1/answermg:readmg:compose or mg:write

Ask the corpus a question and get a cited answer

Grounded Q&A over the MaterialGraph corpus. Post a question in plain language; the endpoint runs the Material Research Agent against the normalised cross-brand graph and returns a cited answer. **The endpoint classifies the question's breadth itself — the caller never does.** A narrow question (one with a determinate answer) is answered tersely, fact first, in at most three sentences; an open-ended one gets a short cited summary. The classification is reported as `breadth` so a client can lay the answer out accordingly. **Citations are RECORD REFS, never URLs.** Each is `{ kind, id, label }` — a `variant` id or a `brand` slug you can hydrate through `/api/v1/materials/{variantId}` or `/api/v1/brands/{brandSlug}`. Ids are opaque tokens: never parse, construct or tidy one. Every citation was OBSERVED in a tool result during the run; refs the model named but the run never saw are discarded before the response is built, and `grounding.citationsDropped` counts them. There is deliberately no self-reported `confidence` — `grounding` reports what happened in counts instead. **Absent is not false.** When the graph does not hold what was asked, `answer` says so and `citations` is empty; that is a statement about coverage, never a denial about the material world. Supply `outputSchema` (a JSON Schema) to get an additional structured payload filled from the same grounded run, returned verbatim as `data`. Supply `systemPrompt` to shape tone or audience — it supplements the answering policy and cannot override the citation contract. **Latency: seconds, not milliseconds.** This is a two-phase LLM synthesis over live corpus queries, not a search: a research phase reads the graph, then a tool-free phase structures only the retained evidence. Read each response's own `elapsedMs` rather than assuming, and do not put it on a keystroke path. `stream: true` is not supported and returns 400 — citations can only be reconciled once the workflow has finished, so there is nothing this endpoint can honestly emit mid-flight. Requires either the `mg:compose` or the `mg:write` scope — this runs agents in the foreground, so it takes the same gate as starting a Run rather than the one a catalogue lookup takes. Responses carry `Cache-Control: no-store`. After provider work runs, the route books one soft-tier `run_medium` operation under the current placeholder schedule; this is an estimate, not token-priced measured cost.

Request body

querystring
outputSchemaobject
systemPromptstring
streamboolean

Response

200The grounded answer, its breadth, and the records it rests on.
answerstring
breadthenum
narrowopen
citationsobject[]
kindenum
variantbranddocument
idstring
labelstring
dataunknown
groundingobject
toolCallsinteger
recordsObservedinteger
citationsDroppedinteger
elapsedMsinteger

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/answer' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "query": "…"}'

Vocabulary

POST/api/v1/resolvemg:read

Resolve phrases to MaterialGraph's canonical vocabulary

**THE QUESTION: "I have words — what do they mean in YOUR vocabulary?"** Post up to 50 phrases as a designer (or a model) would type them; get back, per phrase, every canonical token MaterialGraph knows them as. This is a different lens from its neighbours, and picking the wrong one is where most wrong answers come from. `/api/v1/search` answers *I want materials* and returns products. `/api/v1/answer` answers *I want an answer* and returns cited prose in seconds. This one answers *what do these words mean here*, returns vocabulary in milliseconds, and never returns a material. **It exists so you can delete your phrase tables.** A hand-maintained map of the words your users type onto the tokens this API accepts is a copy of a registry you do not own, and it drifts silently the moment that registry moves. Resolve the words instead. **A phrase can mean several things at once, and they are all returned**, best first — "sage" is both two vendor paints and a colour family. `kind` says which vocabulary each reading came from: `brand`, `paint_identity`, `colour`, `category`, `attribute`, `attribute_value`, `jurisdiction`, `occupancy_group`. **The two code axes resolve out of a sentence.** "a maximum security prison in Kansas" returns `jurisdiction: us-ks` and `occupancy_group: I-3` — the exact pair `/api/v1/codes/requirements` and `/api/v1/codes/advisory` are keyed on, so you never need a copy of our jurisdiction list or your own guess at what "prison" means. Those ids are BARE (`us-tx`, `R-1`), not namespaced, because that is the token those endpoints take. Two honest limits: a jurisdiction or a group is only offered when MaterialGraph holds requirement rows for it, and the pair is HALF a context — `/codes/advisory` also wants a `spaceType` (or a `roomType` to correlate one from) and a `sprinklered` flag, and validates the whole thing before it answers. **Every reading says what STANDS BEHIND it.** `coverage.variants` is how many variants carry the reading corpus-wide, across every brand. `0` is a measured answer rather than missing data — MaterialGraph knows the word and the catalogue holds nothing under it. That case is real and common: 105 active registry attributes currently hold no accepted value at all (measured 2026-08-09), every one of which resolves confidently here. Before this, learning that took a second, differently-shaped call, on a hot path, between two questions — which is exactly where a second round-trip gets skipped. **An ABSENT `coverage` is not a zero.** Its absence is a statement about the MEASUREMENT, never about the catalogue. `attribute` and `attribute_value` readings carry it from the precomputed facet-count view, and `brand` carries the corpus weight the brand anchor already holds; `paint_identity`, `colour`, `category`, `jurisdiction` and `occupancy_group` have no equivalently cheap count behind them and report nothing rather than a figure measured a different way that you would then compare against these. For an `attribute` reading over an ARRAY-valued attribute a variant is counted once per token it carries, so the figure is an upper bound on distinct variants there; it is exact for scalar attributes, exact for every `attribute_value`, and the zero is exact in every case. **Send `category` when you are grounding these words FOR a slot.** Each `attribute` and `attribute_value` reading then also carries `coverage.inCategory`: whether that category holds any accepted value for the attribute at all. A corpus-wide count is NOT a promise the reading is reachable inside a category — `slip_resistance_dcof` has 8,197 variants across the catalogue and `inCategory: false` in Textile Materials (measured 2026-08-09). Conflating the two is what turns a top-ranked reading into an unsatisfiable required criterion, and it is the one mistake this field exists to prevent. The probe is ATTRIBUTE-level, so `true` says the category holds SOME value for the attribute, not that it holds THIS one; and `inCategory` is absent when it could not be determined, which is not a `false`. **`unresolved` is stated, never inferred.** A phrase this vocabulary has no word for comes back with `resolved: []` AND `unresolved: true`. The redundancy is the point: a consumer that cannot see what failed to resolve cannot tell an absent word from a dropped one. **Ids are opaque** — hand them back verbatim, never parse, tidy or construct one. The one thing worth knowing about their shape: a value-dictionary term id is unique only WITHIN its dictionary, so `colour` and `attribute_value` ids arrive namespaced (`sheen.matte`, `colour_family.green`). **`rung` says how the phrase matched** — `exact` (it IS the token), `alias` (it IS a recorded alternative name), `lexical` (it resembles one above the resolver's own confidence floor). There is deliberately no `semantic` rung: every tier that would cost an embedding round-trip is disabled, which is what keeps this millisecond-class. A phrase only a semantic match could have caught comes back unresolved rather than slow. `confidence` is present only when the underlying resolver produces one — its absence means the rung is the whole statement, as an exact paint identity is an equality rather than a score. `ambiguity.gap` is the leader's margin over the runner-up; when a resolver refused to break a tie, every tied reading is listed and the gap is 0. **Phrases resolve independently.** One phrase that means nothing here never spoils the batch, and results come back one-per-phrase in request order, never deduplicated. A malformed REQUEST is still a 400. **Latency: milliseconds.** Measured 2026-08-01 — a warm 7-phrase batch ~130ms, a full 50-phrase batch ~1.4s; a cold instance adds ~1.5s once while the colour index and brand corpus build. The two code axes added ~2.7ms per phrase when they landed (measured 2026-08-09, 50-phrase batch: 1551ms without them, 1687ms with), because they resolve against closed registry vocabularies with no index behind them. Coverage (2026-08-09) costs ONE batched read for the whole request, never one per phrase, and its counts are memoised for 15 minutes: cold it added ~115ms (7-phrase 118ms to 232ms, 50-phrase 824ms to 942ms), warm it is within noise (7-phrase 119ms to 118ms, 50-phrase 827ms to 832ms). Deterministic: the same batch resolves to the same bytes every time. Success responses carry `Cache-Control: public, max-age=300, stale-while-revalidate=3600` (vocabulary class, MG-924). Requires the `mg:read` bearer scope. Responses carry `Cache-Control: no-store`.

Request body

phrasesstring[]

The phrases to resolve, 1-50 of them. Each resolves independently — one phrase that means nothing never spoils the batch.

categorystring

Optional. A product-category name or key — the slot you are grounding these words FOR. It adds `coverage.inCategory` to every `attribute` and `attribute_value` reading: whether that category holds any accepted value for the attribute at all. Absent means corpus-wide, and a corpus-wide `coverage.variants` is NOT a promise the reading is reachable inside a given category — an attribute with 40,000 values across the catalogue can hold none at all in Wall Finishes. Conflating the two is what turned a top-ranked suggestion into an unsatisfiable required criterion in MG-994; send the category and read `inCategory` rather than inferring reachability from a count. Resolved through the same two-pass category resolver retrieval uses, so the scope measured is the scope searched; a name that resolves to no category leaves `inCategory` absent.

Response

200One resolution per phrase, in request order.
resolutionsobject[]

One entry per phrase, in request order — never reordered, never deduplicated.

phrasestring

The phrase exactly as sent, so a caller can rejoin the batch by value.

normalisedstring

The phrase in the single normalised form the resolvers fold to — the same `normalise` stage a search query passes through. Empty when the phrase was all punctuation.

resolvedobject[]

Everything the phrase means, best first. Empty when it means nothing here.

unresolvedboolean

TRUE when nothing resolved. Stated rather than inferred: a consumer that cannot see what failed to resolve cannot tell an absent word from a dropped one.

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/resolve' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "phrases": [    "…"  ]}'

Variants

GET/api/v1/variants/{variantId}mg:read

Get a variant's canonical Public Record

The Variant item IS the Public Record: identity and product / family / brand context, typed blocks, and one cell per attribute carrying its state (`stated` / `conflicting` / `stale` / `unknown` / `missing` / `not_applicable` / `not_evaluated`), source reference, confidence, verification and freshness. Variant ids are opaque tokens — pass back exactly what you were given. Absent means unknown, never false; a `conflicting` cell reports two claims on the SAME scope target only, so sibling colourways differing is not a conflict. This is the canonical name for the projection also served by the older `GET /api/v1/materials/{variantId}/record`; both call one reader and return identical bodies. Success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200The canonical Variant Public Record.
variantIdstring
identityobject
variantIdstring
variantNamestring| null
brandstring
brandSlugstring
productstring
productIdstring
colourNamestring| null
categorystring| null
categoryPathstring[]
imageUrlstring| null
installationImageUrlstring| null
dominantHexstring| null
siblingCountinteger
availabilityobject
stateenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
labelstring| null
supersededByobject| null
blocksobject[]
blockIdstring
blockNamestring
statusenum
verifiedpartialmissingnot_applicable
cellsobject[]
tallyobject
tallyobject
statedinteger
conflictinginteger
staleinteger
unknowninteger
missinginteger
not_applicableinteger
not_evaluatedinteger
notApplicableBlockCountinteger
stalenessThresholdDaysinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/variants/{variantId}' \  -H "Authorization: Bearer $MG_API_KEY"

Materials

GET/api/v1/materials/{variantId}mg:read

Get a single material's detail record

Returns the `MaterialDetail` projection for a variant id: identity, image + source, colour block, accepted category path, explicit product-type review receipt, palette, variant-scoped facets + dimensions, installation shot. `?include=siblings` (comma list; only `siblings` recognised — an unknown value 400s naming the valid set) additionally hydrates the other variants of the same product as tiles. A variant's detail changes when an ingest lands and at no other time, so success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.
include
enumqueryComma list of includes. Only `siblings` is recognised.

Response

200The material detail record, with optional siblings.
materialMaterialDetail
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

productTypeReceiptoneOf
state: acceptedobject
state: proposedobject
state: unknownobject
supersededByobject| null
brandSlugstring
productIdstring
colorstring| null
materialBankIdstring| null
projectSaveCapability"allowed"

Explicit source-governed permission to save this exact source-coded variant to a Project. Absent means unknown/not allowed; it is never inferred from the brand or name.

sourceStatus"provisional"

The governed data status supporting Project capture. Absent means unknown. Provisional data remains source-backed but is not a verified catalogue claim.

installationImageUrlstring| null
displayCategorystring| null
categoryPathstring[]
colourEvidenceoneOf
status: no_eligible_assetobject
status: extractor_not_runobject
status: availableobject
status: projection_missingobject
status: genuinely_colourlessobject
paletteobject[]
facetsobject[]

Accepted searchable facet values on this material (attribute id + canonical term), explicitly classified as identity or specification. Only `specification` facets are queryable through bySpec. Does NOT carry the virtual `certification_programme` facet — certifications live in the certification attributes' own records — so an absence here can never prove a product lacks a certification.

dimensionsobject[]| null
siblingsMaterialTile[]

Present only when `?include=siblings` was requested.

variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/{variantId}/blocksmg:read

List a material's canonical blocks

Returns the canonical block completeness and status projection for one variant: canonical block ids and names, field values and statuses, and missing attribute ids. A variant's detail changes when an ingest lands, so success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200The material's canonical block projection.
variantIdstring
blocksobject[]
canonicalBlockIdstring
canonicalBlockNamestring
statusenum
verifiedpartialmissingnot_applicable
fieldsobject[]
missingAttributeIdsstring[]

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/blocks' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/{variantId}/assetsmg:read

List a material's safe assets

Returns proxy-addressed images, safe downloadable files, and derived asset counts for one variant. Raw PIM image URLs and internal storage/provider metadata are excluded. A variant's assets change when an ingest lands, so success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200The material's safe asset projection.
variantIdstring
imagesobject[]
keystring
ordinalinteger
proxyPathstring
visualRoleenum
griddetailsceneswatchtechnicalunknown
statusenum
downloadedlinked_not_downloadedfailedretired_placeholder
contentTypestring| null
fileExtensionstring| null
filenamestring| null
byteSizenumber| null
downloadsobject[]
ordinalinteger
urlstring
labelstring| null
assetKindstring
statusstring
contentTypestring| null
fileExtensionstring| null
filenamestring| null
documentsobject[]
idstring
scopeobject
declaredScopesvariant[]
kind: productobject
kind: variantobject
variantAttributionstring[]
appliesToRequestedVariantboolean
declarationsobject[]
documentTypestring| null
originalNamestring
statusenum
downloadedlinked_not_downloadedunavailable_sourcedead_linkfailed_ingest
urlstring| null
contentTypestring| null
fileExtensionstring| null
sourceobject
documentCoverageobject
productUnionobject
requestedVariantobject
sourcesobject[]
summaryobject
imageCountinteger
downloadCountinteger
documentCountinteger
readableDocumentCountinteger
downloadedImageCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/assets' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/{variantId}/documentsmg:read

List an owning Product's technical documents

Returns a paginated, URL-deduplicated union of Product- and Variant-grained technical documents. Every Variant declaration retains its contributing Variant ids and states whether it applies to the requested Variant. Coverage distinguishes an explicit empty source collection from a collection not yet ingested, and unavailable documents never carry a usable URL. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.
cursor
stringqueryOpaque nextCursor returned by the previous page.
limit
integerquery

Response

200One page of the owning Product's document union.
variantIdstring
documentsobject[]
idstring
scopeobject
declaredScopesvariant[]
kind: productobject
kind: variantobject
variantAttributionstring[]
appliesToRequestedVariantboolean
declarationsobject[]
documentTypestring| null
originalNamestring
statusenum
downloadedlinked_not_downloadedunavailable_sourcedead_linkfailed_ingest
urlstring| null
contentTypestring| null
fileExtensionstring| null
sourceobject
coverageobject
productUnionobject
requestedVariantobject
sourcesobject[]
totalDocumentsinteger
nextCursorstring| null

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/documents' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/{variantId}/render-assetsmg:read

Read a material's render-ready appearance projection

Returns accepted derived PBR channels and their held-evidence summary, or a typed unavailable response with stable reason codes. The projection contains relative render URLs only; it never exposes storage keys or raw artifact references. Requires the mg:read bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200Available render assets or an explicit unavailable state.

oneOf

schemaVersion: pbr-render-assets/v1object
schemaVersion"pbr-render-assets/v1"
variantIdstring
status"available"
setIdstring
state"accepted"
sourceBasis"derived"
fidelity"unassessed"
acceptanceobject
idstring
verificationDecisionIdstring
acceptedAtstring<date-time>
checkVersionstring
qaobject
checkCountinteger
physicalRepeatobject
uMmnumber
vMmnumber
basisenum
declaredmeasured
tilingobject
uboolean
vboolean
normalConventionenum
opengldirectx
heightAmplitudeMmnumber
channelsobject[]
roleenum
base-colornormalroughnessmetalnessheightambient-occlusion
urlstring
sha256string
byteSizeinteger
mimeType"image/png"
widthinteger
heightinteger
bitDepthanyOf
Variant 1"8"
Variant 2"16"
colorSpaceenum
srgblinear
supersedesSetIdsstring[]
freshnessobject
revisionstring
updatedAtstring<date-time>| null
checkedAtstring<date-time>| null
schemaVersion: pbr-render-assets/v1object
schemaVersion"pbr-render-assets/v1"
variantIdstring
status"unavailable"
setIdstring| null
stateenum| null
pendingwithheldrejectedsupersededaccepted
sourceBasisenum| null
catalog_feeddeclaredderived
reasonCodesenum[]
no_accepted_appearancependingwithheldrejectedsupersededvariant_inactive+2 more
freshnessobject
revisionstring
updatedAtstring<date-time>| null
checkedAtstring<date-time>| null

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/render-assets' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/{variantId}/render-assets/{setId}/{role}mg:read

Download one accepted render channel

Returns one accepted PBR channel PNG after the caller has read the parent render-assets projection. The set and role are immutable path identities; stale or unavailable channels return a typed problem response. Requires the mg:read bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.
setId
stringpath
role
enumpath

Response

200The accepted channel PNG.
string<binary>

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/render-assets/{setId}/{role}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/{variantId}/siblingsmg:read

Get a material's sibling variants

The other variants of the same product (colourways / finishes), as tiles. Returns `{ siblings: [] }` when the variant resolves but is the only variant of its product; 404 when the variant id itself does not resolve (the null-vs-empty-array distinction is load-bearing). Siblings change when an ingest lands, so success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200Sibling tiles.
siblingsMaterialTile[]
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/siblings' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/{variantId}/operatorsmg:read

List the relationship questions a material can be asked

Reports two separate decisions for every graph traversal. `available` says the product graph holds enough seed evidence to answer honestly; its `basis` changes with data ingestion. `discoverable` says a consumer may offer the control because that evidence exists and the complete consumer path has cleared its interaction budget; `discoveryBasis` can change when code or a new latency receipt lands. Filter on `discoverable` for the offer alone. A false value is a refusal, never an empty result or a temporary request outage. Success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200Every operator, available or not, in a fixed vocabulary order.
variantIdstring
operatorsobject[]

EVERY operator, available or not, in a fixed vocabulary order. Refusals are reported rather than omitted because a consumer explaining a control it did NOT render needs the basis for the absence; a caller that only wants the offer filters on `discoverable`. Ordering is the vocabulary's, never availability's, so a control row does not reshuffle as a seed changes.

operatorenum

Which traversal to perform from the seed. An operator is answerable only when the seed carries its evidence basis — ask for the seed's available operators rather than assuming, because an operator that cannot be answered is withheld rather than answered emptily.

colourwayssame_categorystill_listedlooks_likesame_colourpairs_with+6 more
operatorClassenum
identityperceptualrelationalspec
labelstring

A human label for the traversal. A consumer with its own vocabulary may relabel freely; this is the honest default.

availableboolean

Whether the product graph holds enough evidence to ask this seed the question at all. It is not a promise that the answer is interesting: `looks_like` being available means the seed has imagery to search FROM, not that anything near it is worth showing.

basisstring

The fact that decided it, in the vocabulary of the thing measured — readable enough that you can tell what would have to change for the answer to flip.

discoverableboolean

Whether a consumer may currently offer this traversal. This is true only when the seed is answerable AND the complete consumer path has cleared its interaction budget.

discoveryBasisstring

The consumer-path qualification or withholding reason. Unlike the data evidence basis, this can change when code is deployed or a new latency receipt lands.

countinteger

Present only where the deciding probe produced a count anyway. Its ABSENCE means this probe does not count, never that the count is zero — an available operator with no count is normal.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/operators' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/materials/{variantId}/candidatesmg:read

Read one material-relative candidate plane

Runs one explicit graph operator from a canonical seed and returns the seed plus zero to eight candidates. The first qualified operator is `looks_like`: native Pinecone image similarity is the rank evidence, not a consumer re-score. Exclusions and product dedupe are applied before the final bounded answer. Cards prefer an analysed non-isolated display image, retain image/palette identity, and state whether the display image matches the ranking evidence. Seed evidence separately identifies the centre display image and the Pinecone image record that drove likeness. `no_candidates`, `no_unseen_candidates`, `bounded_retrieval_unavailable`, and `operator_unavailable` remain distinct successful answers; `no_unseen_candidates` requires proof that exclusions exhausted otherwise eligible candidates, while `bounded_retrieval_unavailable` says eligible unseen answers exist but the bounded retrieval could not return one. The receipt reports corpus scope, exclusions, dedupe, internal seed-projection timing, retrieval, projection, and complete response timing. Responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Request body

operator"looks_like"

The single qualified candidate operator. It ranks overall visual likeness from the seed's eligible image embedding.

corpusScopeenum

Which corpus to answer from. samplizeCatalogue admits published US paint colours regardless of stock. ABSENT (or 'all') means the WHOLE GRAPH — every material MaterialGraph knows, orderable or not; absence is never a narrower default. Three transaction-oriented scopes gate on governed CHANNEL membership: 'materialBankNaOrderable' (Material Bank North America, ~277,000 materials), 'materialBankEuOrderable' (Material Bank Europe, ~65,000) and 'designShopOrderable' (DesignShop, ~34,000, with the products DesignShop hides from browse excluded). 'materialBankNaAndCatalogue' is the one-query union of Material Bank North America and the governed Catalogue editorial discovery corpus; that union does not by itself claim every member is transactable. 'materialBankOrderable' is NOT one of them: it selects materials carrying a Material Bank identity, which is a provenance signal rather than a channel one, and it admits 98.4% of the graph — use it to exclude crawl-sourced brands, not to mean orderable. A scope answers membership in its named governed corpus, never suitability: scope on channel, then refine on sector. Every scope is applied INSIDE the query: `limit` fills with eligible rows and `counts` describe the scoped corpus, which post-filtering a page cannot give you. An unknown value is a 400 naming it, never a silent fall back to the whole graph.

allsamplizeCataloguematerialBankOrderablematerialBankNaOrderablematerialBankNaAndCataloguematerialBankEuOrderable+1 more
limitinteger
excludeVariantIdsstring[]

Opaque canonical variant ids removed inside retrieval before the bounded answer is selected.

Response

200The canonical seed and an honest zero-to-eight candidate answer with native evidence and receipt.

oneOf

status: readyobject
seedobject
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
receiptobject
operator"looks_like"

The single qualified candidate operator. It ranks overall visual likeness from the seed's eligible image embedding.

corpusScopeobject| null

The corpus this page AND its `counts` were drawn from. Present only when a narrowing scope applied — absence means the whole graph. Read it alongside `counts`: a short page under a scope is a small SCOPED corpus, not a truncated one.

requestedLimitinteger
returnedCountinteger
exclusionsobject
dedupeobject
timingobject
seedEvidenceobject
evidenceImageSha256string

Content identity of the seed image embedding that drove likeness.

displayImageSha256string| null

Content identity of the image selected for the centre card.

basis"active_main_image_embedding"
provenanceGrade"measured"
displayImageMatchedEvidenceboolean| null

Whether the centre image is the exact seed image that drove likeness. Null when no centre image is available.

status"ready"
candidatesobject[]
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
evidenceobject
reasonstring

A specific account of the native rank evidence for this answer.

reasonnull
status: no_candidatesobject
seedobject
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
receiptobject
operator"looks_like"

The single qualified candidate operator. It ranks overall visual likeness from the seed's eligible image embedding.

corpusScopeobject| null

The corpus this page AND its `counts` were drawn from. Present only when a narrowing scope applied — absence means the whole graph. Read it alongside `counts`: a short page under a scope is a small SCOPED corpus, not a truncated one.

requestedLimitinteger
returnedCountinteger
exclusionsobject
dedupeobject
timingobject
seedEvidenceobject
evidenceImageSha256string

Content identity of the seed image embedding that drove likeness.

displayImageSha256string| null

Content identity of the image selected for the centre card.

basis"active_main_image_embedding"
provenanceGrade"measured"
displayImageMatchedEvidenceboolean| null

Whether the centre image is the exact seed image that drove likeness. Null when no centre image is available.

status"no_candidates"
candidatesobject[]
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
evidenceobject
reasonstring

A specific account of the native rank evidence for this answer.

reasonstring
status: no_unseen_candidatesobject
seedobject
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
receiptobject
operator"looks_like"

The single qualified candidate operator. It ranks overall visual likeness from the seed's eligible image embedding.

corpusScopeobject| null

The corpus this page AND its `counts` were drawn from. Present only when a narrowing scope applied — absence means the whole graph. Read it alongside `counts`: a short page under a scope is a small SCOPED corpus, not a truncated one.

requestedLimitinteger
returnedCountinteger
exclusionsobject
dedupeobject
timingobject
seedEvidenceobject
evidenceImageSha256string

Content identity of the seed image embedding that drove likeness.

displayImageSha256string| null

Content identity of the image selected for the centre card.

basis"active_main_image_embedding"
provenanceGrade"measured"
displayImageMatchedEvidenceboolean| null

Whether the centre image is the exact seed image that drove likeness. Null when no centre image is available.

status"no_unseen_candidates"
candidatesobject[]
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
evidenceobject
reasonstring

A specific account of the native rank evidence for this answer.

reasonstring
status: bounded_retrieval_unavailableobject
seedobject
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
receiptobject
operator"looks_like"

The single qualified candidate operator. It ranks overall visual likeness from the seed's eligible image embedding.

corpusScopeobject| null

The corpus this page AND its `counts` were drawn from. Present only when a narrowing scope applied — absence means the whole graph. Read it alongside `counts`: a short page under a scope is a small SCOPED corpus, not a truncated one.

requestedLimitinteger
returnedCountinteger
exclusionsobject
dedupeobject
timingobject
seedEvidenceobject
evidenceImageSha256string

Content identity of the seed image embedding that drove likeness.

displayImageSha256string| null

Content identity of the image selected for the centre card.

basis"active_main_image_embedding"
provenanceGrade"measured"
displayImageMatchedEvidenceboolean| null

Whether the centre image is the exact seed image that drove likeness. Null when no centre image is available.

status"bounded_retrieval_unavailable"
candidatesobject[]
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
evidenceobject
reasonstring

A specific account of the native rank evidence for this answer.

reasonstring
status: operator_unavailableobject
seedobject
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
receiptobject
operator"looks_like"

The single qualified candidate operator. It ranks overall visual likeness from the seed's eligible image embedding.

corpusScopeobject| null

The corpus this page AND its `counts` were drawn from. Present only when a narrowing scope applied — absence means the whole graph. Read it alongside `counts`: a short page under a scope is a small SCOPED corpus, not a truncated one.

requestedLimitinteger
returnedCountinteger
exclusionsobject
dedupeobject
timingobject
status"operator_unavailable"
seedEvidencenull
candidatesobject[]
variantIdstring
productIdstring
brandSlugstring
materialBankIdstring| null
brandstring
productstring
variantNamestring| null
colorstring| null
imageobject| null
paletteobject[]
evidenceobject
reasonstring

A specific account of the native rank evidence for this answer.

reasonstring

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/materials/{variantId}/candidates' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "operator": "looks_like"}'
GET/api/v1/materials/{variantId}/recordmg:read

Get a material's public record projection

The public record (MG-774): identity and product/family/brand context, typed blocks, and one cell per attribute carrying its state (`stated` / `conflicting` / `stale` / `unknown` / `missing` / `not_applicable` / `not_evaluated`), source reference, confidence, verification and freshness. This is the SAME payload the login-free record page at `/materials/{variantId}` renders — neither surface computes anything the other does not. Absent means unknown, never false; a `conflicting` cell reports two claims on the same scope target only (sibling colourways differing is not a conflict). Success responses carry `Cache-Control: public, max-age=300`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200The public record projection.
variantIdstring
identityobject
variantIdstring
variantNamestring| null
brandstring
brandSlugstring
productstring
productIdstring
colourNamestring| null
categorystring| null
categoryPathstring[]
imageUrlstring| null
installationImageUrlstring| null
dominantHexstring| null
siblingCountinteger
availabilityobject
stateenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
labelstring| null
supersededByobject| null
blocksobject[]
blockIdstring
blockNamestring
statusenum
verifiedpartialmissingnot_applicable
cellsobject[]
tallyobject
tallyobject
statedinteger
conflictinginteger
staleinteger
unknowninteger
missinginteger
not_applicableinteger
not_evaluatedinteger
notApplicableBlockCountinteger
stalenessThresholdDaysinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/record' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/materials/{variantId}/substitutesmg:read

Find exact-spec substitute candidates across brands

Finds cross-brand candidates with the same accepted leaf product type, primary material, and discrete performance class as the seed variant. The response states the exact `signatureUsed`, preserves per-criterion trust in `matched[].confidence` and `matched[].sourceBasis`, and reports the weakest-link `minConfidence`. Candidate reads are capped at 10,000 before deterministic trust/diversity ranking. Pagination uses an opaque `cursor`: pass the previous response's `nextCursor` unchanged to continue without duplicates or skips. Unsupported category families and insufficient signatures return explicit empty 200 envelopes. Results are discovery candidates only: this endpoint does not approve a project substitution or establish regulatory, performance, or specification equivalence. Responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Request body

excludeBrandsstring[]
diversityBoostnumber
limitinteger
cursorstring
includeSamplingArtifactsboolean
includeUnavailableboolean

When true, keep candidates that are themselves discontinued or superseded. Default false: unorderable candidates are dropped and counted in `droppedUnavailable`.

Response

200Exact-spec candidates, or an explicit unsupported/insufficient-signature envelope.

oneOf

status: availableobject
status"available"
signatureUsedobject
familyenum
textiletilecoatinglighting_electricalacousticsurface+4 more
formstring
criteriaunknown[]
itemsobject[]
variantIdstring
brandSlugstring
brandstring
productstring
variantstring
colorstring| null
imageUrlstring| null
hexstring| null
matchedobject[]
minConfidencenumber| null
baseScorenumber
diversityAdjustmentnumber
scorenumber
verdictobject
candidatePoolSizeinteger
droppedUnavailableinteger
poolTruncatedboolean

True when more materials match than the candidate pool could rank. What was ranked is an even sample across the whole match set, so it is representative rather than a slice of one end of it, but it is not every match. It qualifies a page that has items; a cut pool that produced none answers `inconclusive` instead, so `available` with an empty `items` always means a complete search that found nothing.

supersededByobject| null
variantIdstring
brandSlugstring
productIdstring
brandstring
productstring
variantstring
colorstring| null
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
nextCursorstring
status: inconclusiveobject
status"inconclusive"
signatureUsedobject
familyenum
textiletilecoatinglighting_electricalacousticsurface+4 more
formstring
criteriaunknown[]
itemsunknown[]
candidatePoolSizeinteger
droppedUnavailableinteger
reasonstring

What happened and what changes it: how many matches were read, how many were dropped as unorderable, and whether there is a cursor to continue from.

supersededByobject| null
variantIdstring
brandSlugstring
productIdstring
brandstring
productstring
variantstring
colorstring| null
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
nextCursorstring
status: unsupportedobject
status"unsupported"
signatureobject
familyenum
textiletilecoatinglighting_electricalacousticsurface+4 more
formstring
reasonstring
itemsunknown[]
supersededByobject| null
variantIdstring
brandSlugstring
productIdstring
brandstring
productstring
variantstring
colorstring| null
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
status: insufficient_signatureobject
status"insufficient_signature"
signatureobject
familyenum
textiletilecoatinglighting_electricalacousticsurface+4 more
formstring
missingRolesenum[]
primary_materialperformance
itemsunknown[]
supersededByobject| null
variantIdstring
brandSlugstring
productIdstring
brandstring
productstring
variantstring
colorstring| null
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/materials/{variantId}/substitutes' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "excludeBrands": [    "…"  ],  "diversityBoost": 0}'
GET/api/v1/materials/{variantId}/revitmg:read

Download a material's Revit specification package (.zip)

Compiles a single material into a Revit "specification package" and streams it as a zip attachment: a shared-parameters definition, a type catalog (one row, keyed by stable GUID-derived parameter ids), a CSI MasterFormat classification, a manifest.json, a README, and — when the variant has one — a swatch.png. No PBR and no self-authored `.adsklib`; the native-appearance rung is marked "planned" in the manifest (it goes through APS Design Automation). Bytes are built fresh per request, so the response is `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.

Response

200The Revit spec-package zip.
string<binary>

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/revit' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/materials/batchmg:read

Batch-read materials by id (query-string form)

Cacheable query-string form of the batch read: `?ids=a,b,c&view=tile&idKind=variantId`. `missingIds` echoes any requested id (in the `idKind` namespace) that did not resolve. Mounted as a static sibling of the dynamic `/materials/{variantId}` segment — Next's router always prefers the static match, so this never falls through. Returns the same `MaterialDetail`-shaped records for known ids, so success responses carry the `material-detail` window, `Cache-Control: public, max-age=120, stale-while-revalidate=1800`. Requires the `mg:read` bearer scope.

Parameters

ids
stringqueryComma-separated ids, in the `idKind` namespace (max 500; at that size the query string nears proxy URL ceilings — prefer POST for bulk).
view
enumquery
idKind
enumquery

Response

200The resolved batch.
materialsvariant[]

`MaterialTile[]` when `view=tile` (default), `MaterialDetail[]` when `view=detail`. Order follows the resolved subset of `ids`.

MaterialTileMaterialTile
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

MaterialDetailMaterialDetail
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

productTypeReceiptoneOf
state: acceptedobject
state: proposedobject
state: unknownobject
supersededByobject| null
brandSlugstring
productIdstring
colorstring| null
materialBankIdstring| null
projectSaveCapability"allowed"

Explicit source-governed permission to save this exact source-coded variant to a Project. Absent means unknown/not allowed; it is never inferred from the brand or name.

sourceStatus"provisional"

The governed data status supporting Project capture. Absent means unknown. Provisional data remains source-backed but is not a verified catalogue claim.

installationImageUrlstring| null
displayCategorystring| null
categoryPathstring[]
colourEvidenceoneOf
status: no_eligible_assetobject
status: extractor_not_runobject
status: availableobject
status: projection_missingobject
status: genuinely_colourlessobject
paletteobject[]
facetsobject[]

Accepted searchable facet values on this material (attribute id + canonical term), explicitly classified as identity or specification. Only `specification` facets are queryable through bySpec. Does NOT carry the virtual `certification_programme` facet — certifications live in the certification attributes' own records — so an absence here can never prove a product lacks a certification.

dimensionsobject[]| null
missingIdsstring[]

Requested ids (in the `idKind` namespace) that did not resolve.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/batch?ids=text' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/materials/batchmg:read

Batch-read materials by id (JSON body form)

POST body is the full `getMaterialsInputSchema` contract (`{ ids, view?, idKind? }`, up to 500 ids per call). `missingIds` echoes any requested id (in the `idKind` namespace) that did not resolve. POST is `Cache-Control: no-store` (arbitrary id sets aren't cache-key-friendly). Requires the `mg:read` bearer scope.

Request body

idsstring[]

The ids to hydrate, in the `idKind` namespace.

viewenum

'tile' (grid card, default) or 'detail' (full record).

tiledetail
idKindenum

Whether `ids` are variant ids or Material Bank ids.

variantIdmaterialBankId

Response

200The resolved batch.
materialsvariant[]

`MaterialTile[]` when `view=tile` (default), `MaterialDetail[]` when `view=detail`. Order follows the resolved subset of `ids`.

MaterialTileMaterialTile
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

MaterialDetailMaterialDetail
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

productTypeReceiptoneOf
state: acceptedobject
state: proposedobject
state: unknownobject
supersededByobject| null
brandSlugstring
productIdstring
colorstring| null
materialBankIdstring| null
projectSaveCapability"allowed"

Explicit source-governed permission to save this exact source-coded variant to a Project. Absent means unknown/not allowed; it is never inferred from the brand or name.

sourceStatus"provisional"

The governed data status supporting Project capture. Absent means unknown. Provisional data remains source-backed but is not a verified catalogue claim.

installationImageUrlstring| null
displayCategorystring| null
categoryPathstring[]
colourEvidenceoneOf
status: no_eligible_assetobject
status: extractor_not_runobject
status: availableobject
status: projection_missingobject
status: genuinely_colourlessobject
paletteobject[]
facetsobject[]

Accepted searchable facet values on this material (attribute id + canonical term), explicitly classified as identity or specification. Only `specification` facets are queryable through bySpec. Does NOT carry the virtual `certification_programme` facet — certifications live in the certification attributes' own records — so an absence here can never prove a product lacks a certification.

dimensionsobject[]| null
missingIdsstring[]

Requested ids (in the `idKind` namespace) that did not resolve.

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/materials/batch' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "ids": [    "…"  ]}'
GET/api/v1/catalogsmg:read

List retained catalog documents

Lists immutable catalog editions with their source, page, render, extraction, and failure receipts. `availability=source_unavailable` keeps a known edition visible without inventing a document; `document.contentScope` distinguishes a retained full publication from cover-only evidence. Live registry truth carries `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Response

200The retained catalog-document shelf.
catalogsvariant[]
kind: documentobject
brandobject
editionobject
idstring
kind"document"
productStatusenum
partialnot_processed
titlestring
availability"available"
documentobject
receiptobject
kind: documentobject
brandobject
editionobject
idstring
kind"document"
productStatus"not_processed"
titlestring
availability"source_unavailable"
documentnull
receiptobject
source"verified-source-material-registry"

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalogs' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalogs/{catalogId}/pagesmg:read

List stable pages in a catalog edition

Returns stable one-based page identities and explicit representation coverage. A page whose derived WebP is not retained carries `status=unavailable`, the required representation version and media type, and no URL or fabricated object key. A known edition with no retained source returns `source_unavailable`. Requires the `mg:read` bearer scope.

Parameters

catalogId
stringpathThe catalog edition id returned by `GET /api/v1/catalogs`.
limit
integerquery
offset
integerquery

Response

200The page list or explicit source-unavailable result.

oneOf

status: availableobject
catalogobject
brandobject
editionobject
idstring
kind"document"
productStatusenum
partialnot_processed
titlestring
availability"available"
documentobject
receiptobject
pagesvariant[]
Variant 1object
catalogIdstring
documentIdstring
editionIdstring
idstring
originalobject
pageobject
representationobject
Variant 2object
catalogIdstring
documentIdstring
editionIdstring
idstring
originalobject
pageobject
representationobject
paginationobject
countinteger
hasMoreboolean
limitinteger
offsetinteger
totalinteger
representationCoverageobject
availableinteger
failuresByReasonobject[]
totalinteger
unavailableinteger
status"available"
reason: retained_source_not_foundobject
catalogobject
brandobject
editionobject
idstring
kind"document"
productStatus"not_processed"
titlestring
availability"source_unavailable"
documentnull
receiptobject
reason"retained_source_not_found"
status"source_unavailable"

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalogs/{catalogId}/pages' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalogs/{catalogId}/referencesmg:read

List verified products in a retained catalog

Returns either every reviewed product/page relationship for one available catalog or an explicit `source_unavailable` result. Each verified reference carries the exact manufacturer code printed in the publication and a normalized anchor for that printed label. `productRegion` is present only when a distinct product-image rectangle has its own explicit review receipt; it is never inferred from the label anchor. Live registry truth carries `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Parameters

catalogId
stringpathThe retained catalog id returned by `GET /api/v1/catalogs`.

Response

200The reviewed product/page index or explicit source-unavailable result.

oneOf

source: verified-source-material-registryobject
catalogobject
brandobject
editionobject
idstring
kind"document"
productStatusenum
partialnot_processed
titlestring
availability"available"
documentobject
receiptobject
coverageobject
documentPagesobject
pagesWithReferencesinteger
precisionobject
productCoverageobject
referencesinteger
unmatchedobject
referencesobject[]
anchorobject
brandobject
catalogueobject
documentobject
editionobject
isPrimaryboolean
pageobject
productRegionobject
sourceobject
status"verified"
variantIdstring
source"verified-source-material-registry"
status"available"
reason: retained_source_not_foundobject
catalogobject
brandobject
editionobject
idstring
kind"document"
productStatus"not_processed"
titlestring
availability"source_unavailable"
documentnull
receiptobject
reason"retained_source_not_found"
status"source_unavailable"

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalogs/{catalogId}/references' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalog-pages/{pageId}/representationmg:read

Read a catalog page representation receipt

Returns a retained page artifact when available, the exact expected artifact without a URL when absent, or `unresolved_page` for an unknown page id. Requires the `mg:read` bearer scope.

Parameters

pageId
stringpathThe opaque page id returned by the catalog page list.

Response

200The retained, unavailable, or unresolved page representation result.

anyOf

status: availableobject
artifactobject
bucket"materialgraph"
keystring
mediaType"image/webp"
urlstring<uri>
pageobject
catalogIdstring
documentIdstring
editionIdstring
idstring
originalobject
pageobject
representationobject
status"available"
reason: retained_representation_not_foundobject
requiredArtifactobject
mediaType"image/webp"
version"catalogue-page-webp-v1"
pageobject
catalogIdstring
documentIdstring
editionIdstring
idstring
originalobject
pageobject
representationobject
reason"retained_representation_not_found"
status"unavailable"
status: unresolved_pageobject
pageIdstring
status"unresolved_page"

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalog-pages/{pageId}/representation' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/catalogues/referencesmg:read

Find verified catalogue pages for material variants

Returns exact, human-reviewed relationships between opaque variant ids and pages in immutable brand publications. Every requested id receives an explicit result: `verified`, `no_verified_reference`, `unresolved_variant`, `unavailable`, or `upstream_failure`. A generic brand PDF is never treated as page evidence, and an unreviewed relationship is never guessed. `coverage` reconciles exactly with the deduplicated results. The read reflects the current product graph and therefore carries `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Request body

variantIdsstring[]

Response

200One explicit catalogue-reference result per unique requested variant id.
coverageobject
noVerifiedReferenceinteger
requestedinteger
unavailableinteger
unresolvedVariantinteger
upstreamFailureinteger
verifiedinteger
resultsvariant[]
status: verifiedobject
referenceobject
status"verified"
variantIdstring
status: no_verified_referenceobject
status"no_verified_reference"
variantIdstring
status: unresolved_variantobject
status"unresolved_variant"
variantIdstring
status: unavailableobject
status"unavailable"
variantIdstring
status: upstream_failureobject
status"upstream_failure"
variantIdstring

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/catalogues/references' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "variantIds": [    "…"  ]}'
GET/api/v1/catalogues/collectionsmg:read

List product collections in the catalogue channel

Returns a flat, paginated shelf of product collections whose variants have governed catalogue-channel membership. Each collection includes exact current product and variant counts plus a bounded representative sample. Requires the `mg:read` bearer scope.

Parameters

limit
integerquery
offset
integerquery
sampleLimit
integerquery

Response

200The current catalogue-channel collection shelf.
collectionsobject[]
idstring
slugstring
namestring
homepageUrlstring| null
productCountinteger
variantCountinteger
sampleProductsobject[]
channelobject
token"catalogue"
basis"governed_variant_membership"
countsobject
basis"exact_current_channel_membership"
paginationobject
countinteger
totalinteger
limitinteger
offsetinteger
hasMoreboolean

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalogues/collections' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalogues/documents/{documentId}mg:read

Resolve access to an immutable catalogue document

Resolves one opaque catalogue document id to browser-safe reader and original URLs, content type, and byte-range capability. Unknown or unavailable documents remain explicit discriminated results rather than guessed URLs. The access decision is read at request time and carries `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Parameters

documentId
stringpathThe opaque catalogue document id returned by a verified reference.

Response

200The available access details or an explicit unresolved/unavailable state.

oneOf

status: availableobject
accessobject
byteRangesenum
supportedunknown
contentType"application/pdf"
documentIdstring
originalUrlstring<uri>
readerUrlstring<uri>
documentIdstring
status"available"
status: unresolved_documentobject
documentIdstring
status"unresolved_document"
status: unavailableobject
documentIdstring
status"unavailable"
status: forbiddenobject
documentIdstring
status"forbidden"
status: upstream_failureobject
documentIdstring
status"upstream_failure"

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalogues/documents/{documentId}' \  -H "Authorization: Bearer $MG_API_KEY"

Pairings

GET/api/v1/materials/{variantId}/pairingsmg:read

Get the "what pairs with this material" rail

A tiered rail: real co-specification evidence (`source: "cospec"`) first, board curation (`source: "considered"`) only when sampled evidence is unavailable, then colour palette-similarity (`source: "colour"`). EVIDENCE AND ORDERING ARE INDEPENDENT: `order` sorts only within the fixed `cospec` → `considered` → `colour` precedence; tiers are never blended or relabelled. `relevance` is anchor-colour similarity descending. `lightness_asc` is dark-to-light (canonical OKLab L 0→1); `lightness_desc` is light-to-dark (L 1→0). Missing/unmeasurable values are always last, with `variantId` ascending as the stable tie-break. Sorting and the optional Material Bank corpus gate occur before limit/cursor pagination. Omit `order` to preserve the legacy rail during the compatibility window. HONESTY CONTRACT: no source is true semantic pairing relevance — `cospec` is demand-lift co-occurrence, `considered` is board activity, and `colour` is a look-alike. Never present a `considered` or `colour` item as "sampled together". Success responses carry the `material-detail` window, `Cache-Control: public, max-age=120, stale-while-revalidate=1800` (same invalidation event as `/materials/{variantId}`). Requires the `mg:read` bearer scope.

Parameters

variantId
stringpathThe variant id to resolve.
categories
stringqueryComma-separated coarse category display names; filters both arms.
limit
integerquery
sampledOnly
booleanqueryReturn sampled order evidence only, without board or colour fallback.
order
enumqueryGraph-owned ordering within each fixed evidence tier. Omit to preserve the legacy rail.
corpus
enumqueryCandidate corpus gate. `material_bank` retains variants carrying a Material Bank id. Requires `order`.
cursor
stringqueryOpaque continuation bound to the seed, categories, corpus, order, tier, and last total-order tuple. Requires `order`.

Response

200The pairings rail.
seedMaterialTile
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

itemsvariant[]
source: cospecobject
variantIdstring
brandstring
productstring
colorstring| null
variantNamestring| null
imageUrlstring| null
brandSlugstring
productIdstring
relationenum
companionbrand_siblingcolourway
categorystring| null
projectCountnumber
customerCountnumber
liftnumber
liftLabelstring
demandobject| null
topProjectTypesobject[]
source"cospec"
rankingobject
source: consideredobject
variantIdstring
brandstring
productstring
colorstring| null
variantNamestring| null
imageUrlstring| null
brandSlugstring
productIdstring
relationenum
companionbrand_siblingcolourway
categorystring| null
projectCountnumber
customerCountnumber
liftnumber
liftLabelstring
demandobject| null
topProjectTypesobject[]
source"considered"
rankingobject
source: colourobject
variantIdstring
brandstring
productstring
variantNamestring| null
imageUrlstring| null
imageSourceenum| null
r2origin
categorystring| null
colourobject
availabilityenum

Orderability: 'available' (listed in the catalog of record), 'discontinued' (withdrawn from that catalog — not a reviewed manufacturer-discontinuation claim), 'superseded' (a reviewed successor relationship exists and the material is no longer listed), 'unknown' (never checked).

availablediscontinuedsupersededunknown
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

colourNamestring

The accepted Colour Name attribute — the colourway's true name where variants.name collapsed to the product's; absent means the attribute is absent (MG-940).

source"colour"
scorenumber
rankingobject
cospecCountinteger

How many items came from real order evidence (the cospec arm).

skippedCandidatesinteger

Candidates dropped at hydration for violating the material contract; present only when > 0. A malformed row costs itself, never the rail (MG-985).

orderingPairingOrdering
requestedenum
relevancelightness_asclightness_desc
appliedenum
relevancelightness_asclightness_desc
directionenum
ascendingdescending
anchorColourBasis"one_minus_oklab_euclidean_distance"
colourSpace"oklab"
lightnessRangeobject
missingValues"last"
tieBreak"variantId_asc"
nextCursorstring

Opaque continuation when another ordered page exists.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/materials/{variantId}/pairings' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/pairings/coordinatemg:read

Coordinate a palette of already-chosen materials

Given several already-chosen materials (by free-text `materials[]` and/or `variantIds[]`), returns demand-lift-ranked palette companions annotated with colour swatches and a `colourFit` score. v1 is ANNOTATE-ONLY: `colourFit` never reorders companions — ranking stays demand-lift. The engine result (`coordinateMaterials`) is passed through verbatim. Coordination is dynamic, so responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Request body

materialsstring[]
variantIdsstring[]
projectTypestring
limitinteger
includeSamplingArtifactsboolean

Response

200The coordinated companion set.
seedsobject[]
variantIdstring
brandstring
productstring
colorstring| null
variantNamestring| null

The variant's own true name (paint colourway names live here).

imageUrlstring| null
brandSlugstring
productIdstring
resolvedFromstring

The input reference this seed resolved from.

unresolvedstring[]

References that did not resolve to any variant.

window"trailing_36mo"

The demand window this endpoint serves.

companionsobject[]
variantIdstring
brandstring
productstring
colorstring| null
variantNamestring| null

The variant's own true name (paint colourway names live here).

imageUrlstring| null
brandSlugstring
productIdstring
totalLiftnumber

Summed lift across the seeds this companion co-specifies with.

liftLabelstring
seedMatchCountinteger

Number of the input seeds this companion co-specifies with.

seedContributionsobject[]
demandobject| null
swatchesobject[]
colourFitnumber| null

Anchor-relative appearance similarity between the seed and companion palettes. v1 is annotate-only — never reorders.

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/pairings/coordinate' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "materials": [    "…"  ],  "variantIds": [    "…"  ]}'
GET/api/v1/demandmg:read

How much a material is specified, plus what's specified alongside it

Composes two cospec domain reads for one seed and returns them side by side: `demand` (`getMaterialDemand` — headline spec/project/customer counts, the quarterly series, and momentum) and `cospec` (`getCoSpecifiedMaterials` — the materials specified alongside it, ranked by real order-history lift). Both domain results are passed through VERBATIM (no reshaping) — the route only runs them in parallel and echoes the resolved `query`. Provide EXACTLY ONE of `variantId` (an opaque material id) or `material` (free text); supplying neither is a 400. This is the transport the ops demand console fetches. Demand is dynamic, so responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Parameters

variantId
stringqueryAn opaque material (variant) id to seed the demand read. Provide exactly one of `variantId` or `material`; `variantId` wins if both are somehow present.
material
stringqueryFree-text material reference (e.g. a product or colour name) to resolve to a seed. Provide exactly one of `variantId` or `material`.

Response

200The seed's demand and co-specified materials, side by side (both domain results verbatim).
querystring

Echo of the resolved reference (`variantId` or `material`).

demandMaterialDemandResult
foundboolean
querystring
seedobject| null
window"trailing_36mo"

The demand window v1 serves (trailing 36 months).

demandobject| null

Headline counts + quarterly series + momentum; null when no stats are tracked.

cospecCoSpecifiedResult
foundboolean
resolutionenum

Three-valued seed resolution; distinct from `found`.

resolvedambiguousnot_found
querystring
seedobject| null
seedCategorystring| null
seedAlternativesobject[]

Best-per-other-brand seeds for the same colour name when steered to one brand.

candidatesobject[]

Best-per-brand candidates when `resolution === "ambiguous"`; empty otherwise.

window"trailing_36mo"

The demand window v1 serves (trailing 36 months).

companionsobject[]

Example

curl
curl 'https://beta.materialgraph.com/api/v1/demand' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/demand/batchmg:read

Enrich a bounded set of exact candidate ids with specification demand

Given 1–48 already-selected canonical variant ids, return each one's real specification demand in INPUT ORDER and CARDINALITY (duplicates echoed) — with no fuzzy resolution, ranking, or reordering. The bounded batch complement to the single-seed `/demand` read; mirrors the `/materials/batch` POST convention. Each input yields one discriminated `state`: `available` (headline spec/project/customer `population`, the exact quarterly `intervals`, and `momentum`), `insufficient_data` (a stats row exists but is below the quality floor), `no_signal` (no stats), or `unknown_variant` (no such variant). PRIVACY: a public (`mg:read`) caller can never tell a privacy-suppressed variant from an absent one — both read `no_signal`; an elevated (`mg:admin`) principal is `internal` and receives exact per-result `evidence`. The envelope carries the `generationId` (the dataset build), the `metric`/`universe`, the `guardrail` floors, and `coverage` tallies (never a denominator over the submitted ids). Demand is dynamic, so responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Request body

variantIdsstring[]

1–48 canonical variant ids. Order AND cardinality are preserved in `results` — duplicates are echoed, never collapsed.

signalenum

Which behavioural signal to read demand from — `sampled` order co-occurrence (default), `considered` board curation, or `specified` (stats only).

sampledconsideredspecified
contextobject

Optional declared context (e.g. `{ projectType: 'hospitality' }`). Echoed back for provenance; never used to filter, rank, or derive a denominator.

Response

200One discriminated demand result per input id, in input order and cardinality.
generationIdstring

The completed ingestion-session id these counts were mined in (the dataset generation).

metric"specification_demand"
universeobject

The metric universe — window and signal the counts are scoped to.

window"trailing_36mo"
signalenum
sampledconsideredspecified
descriptionstring
guardrailobject

The guardrail status under which `results` were projected.

audienceenum
publicinternal
minProjectCountinteger

Quality floor applied for this signal.

minCustomerCountinteger

Privacy floor applied for this signal.

coverageobject

Outcome tallies over `results` — never a denominator for the metric.

requestedinteger
availableinteger
insufficientDatainteger
noSignalinteger
unknownVariantinteger
contextobject| null

The echoed declared context, or null when none was supplied.

resultsvariant[]
state: availableobject
variantIdstring
state"available"
populationobject
intervalsobject[]

Exact per-quarter counts (sparse — zero-spec quarters are absent).

momentumobject
funnelobject| null

The consideration→specification funnel (project-grain, both audiences); null when the signal is not `sampled` or no `considered` row/session exists.

evidenceobject| null

Exact guardrail evidence for `internal` callers; null for public.

state: insufficient_dataobject
variantIdstring
state"insufficient_data"
state: no_signalobject
variantIdstring
state"no_signal"
state: unknown_variantobject
variantIdstring
state"unknown_variant"

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/demand/batch' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "variantIds": [    "…"  ]}'

Associations

GET/api/v1/applicabilitymg:read

Read manufacturer-declared finish applicability

Reads manufacturer-declared finish applicability without changing entity grain. A subject remains the Product or Variant named by the source; every finish remains a Variant. Results are partitioned by source authority and locator, each with its own coverage state, count kind, and source revision, so consumers never infer that an empty result means the source declared none and never invent precedence between sources. Reverse reads start from a finish Variant. Active facts only. Requires the `mg:read` bearer scope.

Parameters

subjectKind
enumqueryThe exact graph grain of the requested subject.
subjectId
stringqueryOpaque Product or Variant id; pass it back unchanged.
direction
enumquery`forward` reads finishes for a Product or Variant; `reverse` reads application subjects for a finish Variant.

Response

200Source-scoped applicability facts and explicit collection coverage.
kind"manufacturer_declared_applicability"
requestedSubjectoneOf
kind: productobject
kind"product"
idstring
kind: variantobject
kind"variant"
idstring
directionenum
forwardreverse
sourcesobject[]
scopeobject
coverageobject| null
factRevisionsstring[]
factsobject[]
unresolvedobject[]

Example

curl
curl 'https://beta.materialgraph.com/api/v1/applicability?subjectKind=text&subjectId=text&direction=text' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/associated/finishesmg:read

List the finishes a manufacturer declares for a product

"What finishes can I get on this chair?" — the finish variants (fabrics, leathers, surfaces) a manufacturer states are available on the product variant given as `variantId`. DECLARED, NOT INFERRED: every association is one the manufacturer enumerated in their own catalog data, so the response is stamped `kind: "declared_association"` and shares no field with the `/pairings` rail — do not merge the two sets or present one as the other. Each association identifies its endpoint `role` (`finish` in the forward read, `product` in the reverse read) and carries the directed `relation: "applicable_finish"`, so the role is available from the item payload itself. Each edge carries a `declared` block: `basis: "manufacturer_declared"`, `source: "warehouse"`, and a `confidence` that is a FIXED convention value (0.9), never a measurement — it cannot rank anything. `totalAssociations` is the TRUE count for this subject, direction and corpus; `associations` is one page of it, so render "(N)" from the total but walk `nextCursor` before claiming anything about the whole set. Each association carries `productTypeReceipt`: `accepted`, `proposed`, or `unknown`; proposed entries are review evidence and must never be rendered as an accepted category. A variant that exists and declares nothing is a 200 with an empty page and a zero total; only an unresolvable id is a 404. These edges change only when an ingest lands, so success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800` — a window justified by that event and set from the read surface's one central cache policy, not guessed per route. `totalAssociations` does not force the response uncached: it is a property of this subject's own record and moves on the same ingest as the body carrying it. Requires the `mg:read` bearer scope.

Parameters

variantId
stringqueryThe PRODUCT variant to read finishes for. Opaque — pass back exactly the id you were given.
limit
integerqueryHow many associations to return on this page. Associations are browsed wider than pairings, so the page is wider: 1–48, default 24.
corpus
enumquerySource identity scope. `material_bank` returns only associations pointing at a variant carrying a Material Bank SKU. A SKU alone does not establish current sampleability; use `sampleableOnly` for an accepted availability fact.
sampleableOnly
booleanqueryWhen true, return only associations whose effective accepted `sampleable` value is true. False and absent preserve the complete declared set; unknown values are excluded only by this explicit true-only filter.
cursor
stringqueryOpaque keyset continuation — pass the previous response's `nextCursor` unchanged. A cursor is bound to its subject, direction and corpus; replaying it against a different one is a 400, never a silently different page.

Response

200The declared finish associations for this product.
subjectobject
variantIdstring
brandstring
productstring
variantNamestring| null
kind"declared_association"

Constant discriminator. These are manufacturer-declared facts; `/pairings` items are inferred companionship. Never merge the two sets.

associationsobject[]
variantIdstring
brandstring
productstring
variantNamestring| null
roleenum
finishproduct
relation"applicable_finish"
imageUrlstring| null

Canonical thumbnail (same policy as siblings/pairings); null when there is none.

declaredobject
productTypeReceiptoneOf
state: acceptedobject
state: proposedobject
state: unknownobject
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

totalAssociationsinteger

The TRUE number of declared associations for this subject and direction under the requested corpus — not the size of this page. Safe to render as "Compatible Finishes (N)"; `associations` is one page of that set, so walk `nextCursor` before making any claim about the whole set.

returnedCountinteger

How many associations this page carries.

nextCursorstring

Opaque continuation for the same subject, direction and corpus; absent on the last page.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/associated/finishes?variantId=text' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/associated/productsmg:read

List the products a manufacturer declares a finish for

"What furniture takes this fabric?" — the product variants a manufacturer states the finish variant given as `variantId` is available on. The exact reverse of `/api/v1/associated/finishes` over the same directed edge: one stored relationship answers both questions, so the two directions can never disagree. DECLARED, NOT INFERRED: every association is one the manufacturer enumerated in their own catalog data, so the response is stamped `kind: "declared_association"` and shares no field with the `/pairings` rail — do not merge the two sets or present one as the other. Each association identifies its endpoint `role` (`finish` in the forward read, `product` in the reverse read) and carries the directed `relation: "applicable_finish"`, so the role is available from the item payload itself. Each edge carries a `declared` block: `basis: "manufacturer_declared"`, `source: "warehouse"`, and a `confidence` that is a FIXED convention value (0.9), never a measurement — it cannot rank anything. `totalAssociations` is the TRUE count for this subject, direction and corpus; `associations` is one page of it, so render "(N)" from the total but walk `nextCursor` before claiming anything about the whole set. Each association carries `productTypeReceipt`: `accepted`, `proposed`, or `unknown`; proposed entries are review evidence and must never be rendered as an accepted category. A variant that exists and declares nothing is a 200 with an empty page and a zero total; only an unresolvable id is a 404. These edges change only when an ingest lands, so success responses carry `Cache-Control: public, max-age=120, stale-while-revalidate=1800` — a window justified by that event and set from the read surface's one central cache policy, not guessed per route. `totalAssociations` does not force the response uncached: it is a property of this subject's own record and moves on the same ingest as the body carrying it. Requires the `mg:read` bearer scope.

Parameters

variantId
stringqueryThe FINISH variant to read products for. Opaque — pass back exactly the id you were given.
limit
integerqueryHow many associations to return on this page. Associations are browsed wider than pairings, so the page is wider: 1–48, default 24.
corpus
enumquerySource identity scope. `material_bank` returns only associations pointing at a variant carrying a Material Bank SKU. A SKU alone does not establish current sampleability; use `sampleableOnly` for an accepted availability fact.
sampleableOnly
booleanqueryWhen true, return only associations whose effective accepted `sampleable` value is true. False and absent preserve the complete declared set; unknown values are excluded only by this explicit true-only filter.
cursor
stringqueryOpaque keyset continuation — pass the previous response's `nextCursor` unchanged. A cursor is bound to its subject, direction and corpus; replaying it against a different one is a 400, never a silently different page.

Response

200The declared product associations for this finish.
subjectobject
variantIdstring
brandstring
productstring
variantNamestring| null
kind"declared_association"

Constant discriminator. These are manufacturer-declared facts; `/pairings` items are inferred companionship. Never merge the two sets.

associationsobject[]
variantIdstring
brandstring
productstring
variantNamestring| null
roleenum
finishproduct
relation"applicable_finish"
imageUrlstring| null

Canonical thumbnail (same policy as siblings/pairings); null when there is none.

declaredobject
productTypeReceiptoneOf
state: acceptedobject
state: proposedobject
state: unknownobject
sampleableboolean

Whether a physical sample is explicitly known to be orderable. Absent means unknown, never unavailable.

totalAssociationsinteger

The TRUE number of declared associations for this subject and direction under the requested corpus — not the size of this page. Safe to render as "Compatible Finishes (N)"; `associations` is one page of that set, so walk `nextCursor` before making any claim about the whole set.

returnedCountinteger

How many associations this page carries.

nextCursorstring

Opaque continuation for the same subject, direction and corpus; absent on the last page.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/associated/products?variantId=text' \  -H "Authorization: Bearer $MG_API_KEY"

Composition

POST/api/v1/schemes/varymg:read

Derive an option-B scheme from an existing one along a constraint axis

Re-derive a composed scheme along one constraint axis, re-picking every unlocked slot while preserving the source brief's hard gates. The scheme is referenced by `runId` (a succeeded `compose-material-schemes` Run, owned by the caller) and, optionally, `schemeId` (defaults to the Run's first scheme). Axes: `palette` (shift each unlocked slot's colour cues to the programme palette rotated by one position), `sustainability` (add a certification floor filter on top of the required criteria), `brandDiversity` (hard-exclude any brand used ≥2× in the source scheme and double the brand-repeat penalty), and `priceTier` — an HONEST brand-level prestige-tier proxy (there is NO per-SKU price data) that leans toward the OPPOSITE tier half from the source's dominant band. `locks` keep chosen slots pinned to their current pick; every unlocked slot's source pick is excluded from its own re-derivation, so option B differs wherever the pool allows. Returns the hydrated compose wire shape stamped with the composition version, so option B renders identically to option A, plus axis + source provenance. Deterministic (it skips the LLM parse pipeline) and `Cache-Control: no-store`. A pick carries `locked: true` when it is an ANCHOR — a variant the designer pinned to the slot on the source compose run (MG-613); anchored slots are held fixed in every variation round, whether or not `locks` repeats them. Each pick's `reason.matched[]` cell is the shared MaterialGraph verdict cell (MG-627): its `state` is one of `matched`, `deviated`, `failed`, `unverified` (a real value that missed the trust bar), `missing`, or `not_evaluated` (the engine did not check it at this slot's retrieval tier), plus the legacy `mismatched`/`unknown` tokens that older persisted runs still carry (never emitted again). The state enum is OPEN per the response-enum policy. Requires the `mg:read` bearer scope. 404 when the run is unknown, not owned, or not a compose Run; 409 when it has not succeeded or its output is unusable; 400 on an invalid body, unknown `schemeId`, or an invalid lock.

Request body

runIdstring

The succeeded `compose-material-schemes` Run to vary from.

schemeIdstring

Which scheme in the Run to treat as option A. Defaults to the Run's FIRST scheme when omitted.

axisenum

The constraint axis to vary along. 'palette' shifts colour cues (each unlocked slot retrieves against the programme palette rotated by one position). 'sustainability' adds a certification floor filter on top of the slot's required criteria. 'brandDiversity' hard-excludes any brand used ≥2× in the source scheme and doubles the brand-repeat penalty. 'priceTier' is an HONEST brand-level prestige-tier proxy — there is NO per-SKU price data. It reads the source scheme's dominant `price_tier` band and boosts (never hard-gates) candidates in the OPPOSITE half (dominant luxury {$$$,$$$$} → prefer value {$,$$}, and vice-versa).

palettesustainabilitybrandDiversitypriceTier
locksobject[]

Slots to keep pinned to their source pick (each lock's `variantId` must equal that slot's current pick). Locked slots are copied through verbatim; only unlocked slots are re-derived.

slotIdstring
variantIdstring
exclusionsobject

Caller exclusions applied on top of the automatic per-slot source-pick exclusion (option B differs from option A wherever the pool allows).

variantIdsstring[]
brandSlugsstring[]

Response

200The hydrated option-B scheme with axis and source provenance.
version"2026-07-25.verdict.1"
schemeobject
schemeIdstring
titlestring
picksobject[]
objectiveobject
provenanceobject
axisenum

The constraint axis to vary along. 'palette' shifts colour cues (each unlocked slot retrieves against the programme palette rotated by one position). 'sustainability' adds a certification floor filter on top of the slot's required criteria. 'brandDiversity' hard-excludes any brand used ≥2× in the source scheme and doubles the brand-repeat penalty. 'priceTier' is an HONEST brand-level prestige-tier proxy — there is NO per-SKU price data. It reads the source scheme's dominant `price_tier` band and boosts (never hard-gates) candidates in the OPPOSITE half (dominant luxury {$$$,$$$$} → prefer value {$,$$}, and vice-versa).

palettesustainabilitybrandDiversitypriceTier
sourceRunIdstring
sourceSchemeIdstring
lockedSlotIdsstring[]
notesstring[]
shortfallobject| null
requestedinteger
producedinteger
reasonsstring[]

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/schemes/vary' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "runId": "…",  "axis": "palette"}'
POST/api/v1/schemes/poolsmg:read

Rank a bounded candidate pool for each of up to 24 material slots

Turn a list of material slots and their requirements into ranked candidate pools, in one call. Each TASK is self-contained — a category, a short visual `intent`, `required` and `preferred` criteria, colour context, an optional `anchorSeed` to rank around, variant/brand `exclude` lists, an availability policy and its own `limit` — and carries YOUR `taskKey`, which comes back on the matching pool verbatim. There is no project or programme: this endpoint publishes the intelligence and keeps none of the ownership. Retrieval walks the relaxation ladder `strict` → `presence` → `category-only`, stopping at the first rung that returns anything; the category filter NEVER relaxes, so a pool cannot bleed across categories. THREE SEMANTICS ARE THE CONTRACT. (1) Partial failure stays visible: a task whose search does not run to an answer returns an empty pool with a typed `search-failed` shortfall carrying the failure's own message, and every other task returns normally — there is no whole-request failure mode for a per-task search error. (2) Unknown is not failure: a criterion the engine did not check at the tier that ran grades `not_evaluated` and one the material holds no accepted value for grades `missing`, both distinguishable from `failed`, which means an accepted value exists and does not satisfy the ask. `unknownCoverage` names the criteria nothing on a page could be graded on; a criterion listed there is a statement about catalogue coverage, never about the candidates. (3) Required failures are visible but never eligible: a candidate retrieved at a rung that could not apply every required criterion is returned FLAGGED (`below-floor`, `required-not-applied`) with `eligible: false`, and a `discontinued` or `superseded` material can never be eligible whatever the availability policy asked for — `unknown` availability means never checked and is never treated as unavailable. Each candidate carries its identity, category, availability and declared successor, the normalised retrieval score with its verdict-state counts, the shared MaterialGraph verdict cells (state, strength, baseline, value, `confidence`, `sourceBasis`, trust gap) with the rolled-up grade, and the features a composer needs: dominant colour, stored image palette, accepted attributes and dimensions. A SOURCE BASIS RANKS THE SOURCE, NOT THE ACCURACY. Colours are extracted from product imagery — visual likeness, never a declared colour specification. `coSpecification` returns the sparse pairing relationships AMONG the returned candidates in ONE bounded read, with its own coverage receipt, so a composer never issues one pairings request per candidate; an empty edge list under a null `datasetSessionId` means the dataset is absent, not that these materials are never specified together. Each pool also states HOW its candidates were retrieved (`arm`: `visual-text`, `image-similarity`, or `none`) and HOW STRICTLY (`retrievalTier`: the relaxation rung it settled on). `arm` is DERIVED from the retrieval plan and the pool's own receipt — MaterialGraph does not report an arm per candidate — and is constant across the relaxation rungs, so read it with `retrievalTier` rather than as the whole story. `calls` reports CALL VOLUME ONLY — how many searches, hydration batches and co-specification queries the request performed. It is never a cost or token figure: slot retrieval is database and embedding work that does not go through the model-call path carrying token and spend accounting, so no such figure exists for it. Do not render it as usage or spend. Bounds are explicit: at most 24 tasks, at most 48 candidates per task, a 256 KB body, bounded query concurrency and a 240s retrieval budget whose expiry becomes a per-task shortfall rather than a dropped task. A pool is true only when computed, so responses are `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Request body

tasksobject[]

The slots to build pools for (max 24). Each is independent: one task's search failing costs that task only.

taskKeystring

Your own key for this task. Returned verbatim on the matching pool — this is how you correlate results. Opaque to MaterialGraph.

categorystring

Catalog category (a display-name group, e.g. 'Flooring'). Omitting it yields an empty pool with a `no-category` shortfall — category is the only cross-kind gate, and retrieval will not rank without one.

intentstring

A short VISUAL description of the material wanted — what it looks like, not a keyword list. Ranked as a visual text query.

requiredvariant[]

Hard requirements. Applied as spec filters at the strict tier; a candidate retrieved at a tier that could not apply one of these is returned FLAGGED and never eligible.

kind: attributeobject
kind: attribute-presenceobject
kind: attribute-exclusionobject
kind: attribute-numericobject
kind: unresolvedobject
preferredvariant[]

Soft preferences. Never a filter — genuinely evaluated per candidate and reported as `preferred` verdict cells.

kind: attributeobject
kind: attribute-presenceobject
kind: attribute-exclusionobject
kind: attribute-numericobject
kind: unresolvedobject
coloursobject[]

Palette context for this task. Colours RERANK within the retrieved source; they never admit a candidate the category gate excluded.

anchorSeedobject

Rank this task around an already-chosen variant (an image-similarity search seeded by it). The category filter and every required criterion remain hard gates. Variant ids are opaque tokens.

excludeobject

Candidates to keep out of this task's pool. Variant ids are OPAQUE tokens — send them back exactly as MaterialGraph issued them.

availabilityenum

'flag-unavailable' (default) returns discontinued/superseded candidates labelled and ineligible, so a specifier can see what the catalogue used to hold and follow a successor; 'exclude-unavailable' drops them from the pool entirely. Neither ever marks them eligible. 'unknown' availability is never excluded — it means never checked, not unavailable.

flag-unavailableexclude-unavailable
limitinteger

Candidates returned for this task (max 48, default 12). Retrieval scores more than it returns; `consideredCount` on the pool says how many.

coSpecificationboolean

Include the co-specification evidence block — the sparse pairing relationships AMONG the returned candidates, in ONE bounded read. Set false to skip that query when your composer does not score pairs.

Response

200One bounded pool per task, correlated by `taskKey`, with per-task shortfalls, the co-specification evidence for the returned union, and the contract/retrieval/evaluator/dataset version stamps.
versionsobject
contractstring
retrievalstring
evaluatorstring
datasetobject
poolsobject[]
taskKeystring
categorystring| null
retrievalTierenum

'strict' is the unrelaxed search; 'presence' demoted required term filters to presence checks; 'category-only' dropped attribute filters. Category NEVER relaxes, so a pool can never bleed across categories.

strictpresencecategory-only
armenum

How this pool's candidates were retrieved: 'visual-text' (the default text→image rank over the task's `intent`), 'image-similarity' (an `anchorSeed` took the retrieval), or 'none' (no search ran — an unplaceable or failed task). DERIVED from the retrieval plan and the pool's receipt, never observed per candidate, and constant across the relaxation rungs — read `retrievalTier` for the rung the pool settled on.

visual-textimage-similaritynone
candidatesobject[]
consideredCountinteger
returnedCountinteger
excludedCountinteger
unavailableDroppedCountinteger
unknownCoverageobject
shortfalloneOf| null

Why this pool is short. 'no-category' — the task carried no category, so retrieval had no cross-kind gate and never searched. 'category-coverage' — the category holds no accepted value for one or more required attributes, with `gaps` saying whether the miss is attribute-level (the category records nothing for it) or value-level (it records plenty and none of them the asked term). 'search-failed' — the search did not run to an answer; the message is the failure's own. 'no-match' — every rung ran and nothing qualified, with `consideredCount` saying how much was scored.

kind: no-categoryobject
kind: category-coverageobject
kind: grounding-failedobject
kind: search-failedobject
kind: no-matchobject
seededFromobject| null
warningsstring[]
coSpecificationobject| null
edgesobject[]
coverageobject
summaryobject
tasksinteger
eligibleCandidatesinteger
flaggedCandidatesinteger
tasksWithEligibleCandidatesinteger
tasksWithoutEligibleCandidatesinteger
searchFailedinteger
noCategoryinteger
categoryCoverageinteger
noMatchinteger
callsobject
searchesRuninteger
searchesFailedinteger
candidatesConsideredinteger
candidatesReturnedinteger
hydrationBatchesinteger
coSpecificationQueriesinteger
warningsstring[]

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/schemes/pools' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "tasks": [    {      "taskKey": "…"    }  ]}'

Catalog

GET/api/v1/catalogues/samplize/productsmg:read

Read the complete Samplize paint catalogue

Version-pinned pagination over Snowflake products reconciled with US storefront publication. Includes out-of-stock products, selected paint hexes, colour analysis, source hex comparisons and size variants. Collect total unique variant IDs across all pages to establish completeness.

Parameters

limit
integerquery
after
stringquery
sourceVersion
stringqueryRequired after the first page; use its sourceVersion.

Response

200A page of canonical paint colours.
schemaVersion"2"
corpusScope"samplizeCatalogue"
sourceVersionstring
sourceobject
totalinteger
itemsobject[]
variantIdstring
productIdstring
namestring
codestring
brandobject
hexstring
hexSourceobject
lrvnumber| null
lrvSourceobject| null
lrvObservationsobject[]
demandobject| null
collectionMembershipsobject[]
colourAnalysisobject
oklabobject
sourceProductIdstring
urlstring
sizeVariantsobject[]
publishedHexesobject[]
hexObservationsobject[]
nextCursorstring| null
completeboolean

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalogues/samplize/products' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/codes/contextmg:read

The vocabulary for stating a code context

Everything needed to compose a valid code context, derived from the in-repo registry at call time: seeded `jurisdictions` (with their coverage tier — `thorough` where the AHJ's own table was reviewed, `model_baseline` where unamended model IBC stands in — and an honest adoption note), the `occupancyGroups` requirement rows actually key on, Table 803.13 `finishLocations`, the four `dimensions` and the MaterialGraph attribute each is scored against, `scales` with their classes ordered BEST FIRST (index 0 outranks index 1), and `roomCorrelations` mapping `room` terms onto occupancy group and finish location. A correlation the registry refuses to make is `null`, never a guess. ADVISORY ONLY — the AHJ caveat ships with the vocabulary. Static per deploy: `Cache-Control: private, max-age=3600`. Requires the `mg:read` bearer scope.

Response

200The code-context vocabulary.
jurisdictionsobject[]
idstring
namestring
codeFamilystring
editionstring
coverageenum
thoroughmodel_baseline
adoptionNotestring
occupancyGroupsobject[]
idstring
labelstring
finishLocationsobject[]
idstring
labelstring
shortLabelstring
dimensionsobject[]
idenum
firefloorslipdecorative
labelstring
descriptionstring
attributeIdstring
scalesobject[]
idstring
labelstring
detailstring
classesstring[]
roomCorrelationsobject[]
roomTypestring
labelstring
occupancyGroupstring| null
finishLocationstring| null
caveat"Advisory — model code as adopted, not a legal determination. Always verify with the authority having jurisdiction."

Example

curl
curl 'https://beta.materialgraph.com/api/v1/codes/context' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/codes/requirementsmg:read

What a space demands, before you have candidates

Every code requirement that applies to one context, with no variant ids involved — the requirement list a specifier reads while setting up a project, not one derived from materials already on the table. State the context as `jurisdiction` + `occupancy` + `space`, or as `jurisdiction` + `room` to correlate the regulatory axes from a `room` term; `sprinklered` is always required and never defaulted, because sprinkler protection changes the required class. Each requirement carries its code `section`, the demand itself (`minClass` on an ordinal scale, or a `numericFloor`), the `attributeId` it is scored against, and a readable `label` such as `Class A or better · ASTM E84 / UL 723`. An empty `requirements` array is a legitimate answer meaning nothing in the registry applies — it is not a pass. This is the same filter the advisory evaluator runs, so the list is exactly what a variant would be scored against. ADVISORY ONLY. Static per deploy: `Cache-Control: private, max-age=3600`. Requires the `mg:read` bearer scope.

Parameters

jurisdiction
stringquerySeeded jurisdiction id, e.g. `us-tx`. List them at `codes/context`.
sprinklered
booleanqueryWhether the space is sprinklered. Exactly `true` or `false` — there is no default.
occupancy
stringqueryIBC occupancy group, e.g. `A-2`. Required unless `room` correlates to one.
space
stringqueryFinish location term id, e.g. `corridors`. Required unless `room` correlates to one.
room
stringqueryA `room` term id, e.g. `hotel_room`, to correlate the regulatory axes from. Explicit `occupancy`/`space` always win.

Response

200The applicable requirements for the context.
jurisdictionobject
idstring
namestring
codeFamilystring
editionstring
coverageenum
thoroughmodel_baseline
adoptionNotestring
contextobject
jurisdictionIdstring
occupancyGroupstring
spaceTypestring
sprinkleredboolean
basisenum
explicitderived_from_room
roomTypestring| null
requirementsobject[]
requirementIdstring
dimensionenum
firefloorslipdecorative
sectionstring
attributeIdstring
requirementobject
labelstring
caveat"Advisory — model code as adopted, not a legal determination. Always verify with the authority having jurisdiction."

Example

curl
curl 'https://beta.materialgraph.com/api/v1/codes/requirements?jurisdiction=text&sprinklered=true' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/codes/advisorymg:read

Badge materials against the advisory building-code registry

Evaluate up to 100 materials against the in-repo code-requirement registry for one context (`jurisdictionId`, `sprinklered`, plus either `occupancyGroup` + `spaceType` or a `roomType` the axes are correlated from — `resolvedContext` reports which axes were used and whether they were stated or derived). Each evaluated result carries `material`: what the variant IS (`productTypeId`, `applicationIds`) and the regulatory `dimensions` it can be held to, with a `basis` of `product_type` / `application` / `both` / `unresolved` (null only for `state: "unknown_variant"`). Read `basis` before `dimensions`: `unresolved` does NOT mean the material is answerable for nothing — it means no mapped primary product type and no usable applications were found, so every requirement the space imposes was scored and `notApplicable` is empty; that is a coverage gap in our own product-type map, not a verified pass. A short `requirements` list under `product_type` / `application` / `both` is a genuine narrow intersection between space and material. Requirements the space imposes that this material cannot answer (e.g. slip resistance for a wallcovering) are excluded from `requirements` and reported in `notApplicable` instead, each with a `reason` and a readable `detail`; `notApplicable` is `[]` when nothing was excluded. For each variant, every requirement it IS answerable for is badged `meets` / `shortfall` / `unknown` from the variant's accepted `fire_classifications` and `slip_resistance_dcof` facts (variant-scope facts shadow product-scope). `results` preserve input order and cardinality; a variant that does not resolve is returned with `state: "unknown_variant"`, `material: null`, `requirements: []`, `notApplicable: []` and a null `summary`. Each evaluated variant also carries a rolled-up `summary` — `meets` / `shortfall` / `unverified` / `not_applicable` plus one deterministic sentence — in which a shortfall outranks an unknown and an unknown never becomes a pass. Fire tokens with no defensible mapping onto an IBC scale (euroclass, CAL TB 117, …) resolve to `unknown`, never a guessed equivalence. ADVISORY ONLY — every verdict and the envelope carry the AHJ caveat; this is model code as adopted, never a legal determination. A 400 is returned for an unknown jurisdiction, an uncovered occupancy group, or an unrecognised space type. Reads live facts, so responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Request body

variantIdsstring[]
jurisdictionIdstring
occupancyGroupstring

The IBC occupancy group for the space, e.g. `A-2` (restaurant) or `I-2` (hospital). CONDITIONALLY REQUIRED — the validator resolves `occupancyGroup ?? correlate(roomType)` and refuses a request where that resolves to nothing. Omit both and the 400 reads: "An occupancy group is required — state occupancyGroup, or a roomType that correlates to one." Send only a `roomType` that maps to no group and it reads: 'Room type "{roomType}" does not correlate to an IBC occupancy group — state occupancyGroup explicitly.' An explicit value always wins over a correlated one. A group no requirement row covers is a separate 400 listing the covered ones; enumerate them at `GET /api/v1/codes/context`.

spaceTypestring

The Table 803.13 finish location: `corridors`, `exit_stairs_passageways`, or `rooms_enclosed_spaces`. CONDITIONALLY REQUIRED on the same rule as `occupancyGroup`, resolved independently of it as `spaceType ?? correlate(roomType)`. Omit both and the 400 reads: "A space type is required — state spaceType, or a roomType that correlates to one." Send only a `roomType` that maps to no location and it reads: 'Room type "{roomType}" does not correlate to a Table 803.13 finish location — state spaceType explicitly.' An unrecognised location is a separate 400 listing the accepted ones.

roomTypestring

A `room` term id, e.g. `hotel_room`, to correlate the regulatory axes from — the alternative to stating them. Consulted per axis and ONLY for an axis you did not state. Explicit values always win. It may supply `occupancyGroup`, `spaceType`, both, or (alongside explicit values) neither. Correlation never guesses: a room that maps to nothing on an axis you also left unstated is a 400 naming that axis. `resolvedContext` in the response reports which axes were used and whether the context was `explicit` or `derived_from_room`. Discover the correlations at `GET /api/v1/codes/context`.

sprinkleredboolean

Response

200Per-variant, per-requirement advisory verdicts.
jurisdictionobject
idstring
namestring
codeFamilystring
editionstring
adoptionNotestring
contextobject
occupancyGroupstring
spaceTypestring
sprinkleredboolean
resolvedContextobject
jurisdictionIdstring
occupancyGroupstring
spaceTypestring
sprinkleredboolean
basisenum
explicitderived_from_room
roomTypestring| null
caveat"Advisory — model code as adopted, not a legal determination. Always verify with the authority having jurisdiction."
resultsobject[]
variantIdstring
stateenum
evaluatedunknown_variant
materialobject| null
requirementsobject[]
notApplicableobject[]
summaryobject| null

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/codes/advisory' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "variantIds": [    "…"  ],  "jurisdictionId": "…",  "sprinklered": true}'
GET/api/v1/clusters/visualmg:read

List the whole corpus's visual collections

Automatic visual collections — clusters of catalog images a fitted snapshot found to be visually alike, then labelled, covered and measured. Nobody curated them: the grouping is what the corpus says looks the same, across every brand at once. Scope is part of the PATH, not a filter, because which family a read addresses is its identity rather than a narrowing of one collection: this operation serves the `catalog` family — every brand's imagery clustered together, which is precisely the view a brand-by-brand catalog cannot produce, and the default read of this resource. Narrow the read with `level=specific`, with one broad collection's children (`parent=`), or by pinning a named `snapshot=` — those are modifiers on one family and stay in the query string. Each collection carries its cover, its six published exemplars and its diagnostics (mean similarity, exemplar floor, stability, brand concentration, product-type diversity, image-role mix) — never its memberships, which run to hundreds of thousands of rows. A family that exists but was never fitted returns a well-formed envelope with an empty `collections` array; a read that cannot be answered at all (a foreign cursor, an unknown snapshot) is a 400, and a scope key passed as a query parameter is a 400 naming it rather than a value that silently contradicts the URL. Every response stamps the snapshot it was served from. EXPERIMENTAL — see `x-status`. Snapshots are immutable, so success responses carry `Cache-Control: private, max-age=60` (`private` because this is a metered bearer surface). Requires the `mg:read` bearer scope.

Parameters

level
enumquery`broad` (the default) is the family's top-level map; `specific` is its subdivisions.
parent
stringqueryOnly the children of this collection id. Implies `level=specific`. This is the contract's `parentClusterId`.
snapshot
stringqueryRead a named snapshot run id instead of the ACTIVE one. An unknown id is a 400, never a silent fall back to the active snapshot.
limit
integerqueryCollections per page. Defaults to 24, ceiling 100. A non-integer is a 400.
cursor
stringqueryOpaque cursor from a previous page's `nextCursor`. Only valid for the query that produced it.

Response

200A page of visual collections, with the snapshot stamp.
status"experimental"

The contract's advertised stability; mirrors `x-status` on this operation.

snapshotobject

Which snapshot served the read — the reproducibility anchor.

runIdstring
statestring
corpusRunIdstring
corpusContentDigeststring
activatedAtstring| null
labelModelstring| null
motifModelstring| null
styleBriefVersionstring| null
scopeobject
kindenum
catalogbrandcategory_groupproduct_typebrand_type
keystring

Partition-directory key: `catalog`, a brand id, or `{brandId}:{productTypeId}`.

selectorstring

The canonical, operator-typable selector for the family.

labelstring

Resolved display name, e.g. `Marazzi · Tile`. Never an id.

levelenum
broadspecific
returnedCountinteger

Collections on this page — never a total.

totalMatchesinteger

Collections matching the filter across every page.

nextCursorstring| null
collectionsobject[]
idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/clusters/visual' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/clusters/visual/category-group/{categoryGroupId}mg:read

List one category group's visual collections

Automatic visual collections — clusters of catalog images a fitted snapshot found to be visually alike, then labelled, covered and measured. Nobody curated them: the grouping is what the corpus says looks the same, across every brand at once. Scope is part of the PATH, not a filter, because which family a read addresses is its identity rather than a narrowing of one collection: this operation serves the visual collections fitted over one category group. Narrow the read with `level=specific`, with one broad collection's children (`parent=`), or by pinning a named `snapshot=` — those are modifiers on one family and stay in the query string. Each collection carries its cover, its six published exemplars and its diagnostics (mean similarity, exemplar floor, stability, brand concentration, product-type diversity, image-role mix) — never its memberships, which run to hundreds of thousands of rows. A family that exists but was never fitted returns a well-formed envelope with an empty `collections` array; a read that cannot be answered at all (a foreign cursor, an unknown snapshot) is a 400, and a scope key passed as a query parameter is a 400 naming it rather than a value that silently contradicts the URL. Every response stamps the snapshot it was served from. EXPERIMENTAL — see `x-status`. Snapshots are immutable, so success responses carry `Cache-Control: private, max-age=60` (`private` because this is a metered bearer surface). Requires the `mg:read` bearer scope.

Parameters

categoryGroupId
stringpathThe category group whose family to read. A group that was never fitted returns an empty envelope, not a 404.
level
enumquery`broad` (the default) is the family's top-level map; `specific` is its subdivisions.
parent
stringqueryOnly the children of this collection id. Implies `level=specific`. This is the contract's `parentClusterId`.
snapshot
stringqueryRead a named snapshot run id instead of the ACTIVE one. An unknown id is a 400, never a silent fall back to the active snapshot.
limit
integerqueryCollections per page. Defaults to 24, ceiling 100. A non-integer is a 400.
cursor
stringqueryOpaque cursor from a previous page's `nextCursor`. Only valid for the query that produced it.

Response

200A page of visual collections, with the snapshot stamp.
status"experimental"

The contract's advertised stability; mirrors `x-status` on this operation.

snapshotobject

Which snapshot served the read — the reproducibility anchor.

runIdstring
statestring
corpusRunIdstring
corpusContentDigeststring
activatedAtstring| null
labelModelstring| null
motifModelstring| null
styleBriefVersionstring| null
scopeobject
kindenum
catalogbrandcategory_groupproduct_typebrand_type
keystring

Partition-directory key: `catalog`, a brand id, or `{brandId}:{productTypeId}`.

selectorstring

The canonical, operator-typable selector for the family.

labelstring

Resolved display name, e.g. `Marazzi · Tile`. Never an id.

levelenum
broadspecific
returnedCountinteger

Collections on this page — never a total.

totalMatchesinteger

Collections matching the filter across every page.

nextCursorstring| null
collectionsobject[]
idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/clusters/visual/category-group/{categoryGroupId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/clusters/visual/product-type/{productTypeId}mg:read

List one product type's visual collections

Automatic visual collections — clusters of catalog images a fitted snapshot found to be visually alike, then labelled, covered and measured. Nobody curated them: the grouping is what the corpus says looks the same, across every brand at once. Scope is part of the PATH, not a filter, because which family a read addresses is its identity rather than a narrowing of one collection: this operation serves the visual collections one product type forms across every brand — the read that says which looks a type actually has, rather than which brands sell it. Narrow the read with `level=specific`, with one broad collection's children (`parent=`), or by pinning a named `snapshot=` — those are modifiers on one family and stay in the query string. Each collection carries its cover, its six published exemplars and its diagnostics (mean similarity, exemplar floor, stability, brand concentration, product-type diversity, image-role mix) — never its memberships, which run to hundreds of thousands of rows. A family that exists but was never fitted returns a well-formed envelope with an empty `collections` array; a read that cannot be answered at all (a foreign cursor, an unknown snapshot) is a 400, and a scope key passed as a query parameter is a 400 naming it rather than a value that silently contradicts the URL. Every response stamps the snapshot it was served from. EXPERIMENTAL — see `x-status`. Snapshots are immutable, so success responses carry `Cache-Control: private, max-age=60` (`private` because this is a metered bearer surface). Requires the `mg:read` bearer scope.

Parameters

productTypeId
stringpathThe product type whose family to read. A type that was never fitted returns an empty envelope, not a 404.
level
enumquery`broad` (the default) is the family's top-level map; `specific` is its subdivisions.
parent
stringqueryOnly the children of this collection id. Implies `level=specific`. This is the contract's `parentClusterId`.
snapshot
stringqueryRead a named snapshot run id instead of the ACTIVE one. An unknown id is a 400, never a silent fall back to the active snapshot.
limit
integerqueryCollections per page. Defaults to 24, ceiling 100. A non-integer is a 400.
cursor
stringqueryOpaque cursor from a previous page's `nextCursor`. Only valid for the query that produced it.

Response

200A page of visual collections, with the snapshot stamp.
status"experimental"

The contract's advertised stability; mirrors `x-status` on this operation.

snapshotobject

Which snapshot served the read — the reproducibility anchor.

runIdstring
statestring
corpusRunIdstring
corpusContentDigeststring
activatedAtstring| null
labelModelstring| null
motifModelstring| null
styleBriefVersionstring| null
scopeobject
kindenum
catalogbrandcategory_groupproduct_typebrand_type
keystring

Partition-directory key: `catalog`, a brand id, or `{brandId}:{productTypeId}`.

selectorstring

The canonical, operator-typable selector for the family.

labelstring

Resolved display name, e.g. `Marazzi · Tile`. Never an id.

levelenum
broadspecific
returnedCountinteger

Collections on this page — never a total.

totalMatchesinteger

Collections matching the filter across every page.

nextCursorstring| null
collectionsobject[]
idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/clusters/visual/product-type/{productTypeId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/clusters/visual/brand/{brandId}mg:read

List one brand's visual collections

Automatic visual collections — clusters of catalog images a fitted snapshot found to be visually alike, then labelled, covered and measured. Nobody curated them: the grouping is what the corpus says looks the same, across every brand at once. Scope is part of the PATH, not a filter, because which family a read addresses is its identity rather than a narrowing of one collection: this operation serves one brand's own visual collections — the looks its range divides into when nothing but the imagery decides. Narrow the read with `level=specific`, with one broad collection's children (`parent=`), or by pinning a named `snapshot=` — those are modifiers on one family and stay in the query string. Each collection carries its cover, its six published exemplars and its diagnostics (mean similarity, exemplar floor, stability, brand concentration, product-type diversity, image-role mix) — never its memberships, which run to hundreds of thousands of rows. A family that exists but was never fitted returns a well-formed envelope with an empty `collections` array; a read that cannot be answered at all (a foreign cursor, an unknown snapshot) is a 400, and a scope key passed as a query parameter is a 400 naming it rather than a value that silently contradicts the URL. Every response stamps the snapshot it was served from. EXPERIMENTAL — see `x-status`. Snapshots are immutable, so success responses carry `Cache-Control: private, max-age=60` (`private` because this is a metered bearer surface). Requires the `mg:read` bearer scope.

Parameters

brandId
stringpathThe brand whose family to read. A brand that was never fitted returns an empty envelope, not a 404.
level
enumquery`broad` (the default) is the family's top-level map; `specific` is its subdivisions.
parent
stringqueryOnly the children of this collection id. Implies `level=specific`. This is the contract's `parentClusterId`.
snapshot
stringqueryRead a named snapshot run id instead of the ACTIVE one. An unknown id is a 400, never a silent fall back to the active snapshot.
limit
integerqueryCollections per page. Defaults to 24, ceiling 100. A non-integer is a 400.
cursor
stringqueryOpaque cursor from a previous page's `nextCursor`. Only valid for the query that produced it.

Response

200A page of visual collections, with the snapshot stamp.
status"experimental"

The contract's advertised stability; mirrors `x-status` on this operation.

snapshotobject

Which snapshot served the read — the reproducibility anchor.

runIdstring
statestring
corpusRunIdstring
corpusContentDigeststring
activatedAtstring| null
labelModelstring| null
motifModelstring| null
styleBriefVersionstring| null
scopeobject
kindenum
catalogbrandcategory_groupproduct_typebrand_type
keystring

Partition-directory key: `catalog`, a brand id, or `{brandId}:{productTypeId}`.

selectorstring

The canonical, operator-typable selector for the family.

labelstring

Resolved display name, e.g. `Marazzi · Tile`. Never an id.

levelenum
broadspecific
returnedCountinteger

Collections on this page — never a total.

totalMatchesinteger

Collections matching the filter across every page.

nextCursorstring| null
collectionsobject[]
idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/clusters/visual/brand/{brandId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/clusters/visual/brand/{brandId}/{productTypeId}mg:read

List one brand's visual collections within one product type

Automatic visual collections — clusters of catalog images a fitted snapshot found to be visually alike, then labelled, covered and measured. Nobody curated them: the grouping is what the corpus says looks the same, across every brand at once. Scope is part of the PATH, not a filter, because which family a read addresses is its identity rather than a narrowing of one collection: this operation serves the narrowest family a snapshot fits: one brand's slice of one product type. Both ids are path segments precisely because this family's key is compound — a pair carried in two optional query parameters can always be asked for half of itself. Narrow the read with `level=specific`, with one broad collection's children (`parent=`), or by pinning a named `snapshot=` — those are modifiers on one family and stay in the query string. Each collection carries its cover, its six published exemplars and its diagnostics (mean similarity, exemplar floor, stability, brand concentration, product-type diversity, image-role mix) — never its memberships, which run to hundreds of thousands of rows. A family that exists but was never fitted returns a well-formed envelope with an empty `collections` array; a read that cannot be answered at all (a foreign cursor, an unknown snapshot) is a 400, and a scope key passed as a query parameter is a 400 naming it rather than a value that silently contradicts the URL. Every response stamps the snapshot it was served from. EXPERIMENTAL — see `x-status`. Snapshots are immutable, so success responses carry `Cache-Control: private, max-age=60` (`private` because this is a metered bearer surface). Requires the `mg:read` bearer scope.

Parameters

brandId
stringpathThe brand half of the compound family key.
productTypeId
stringpathThe product-type half of the compound family key.
level
enumquery`broad` (the default) is the family's top-level map; `specific` is its subdivisions.
parent
stringqueryOnly the children of this collection id. Implies `level=specific`. This is the contract's `parentClusterId`.
snapshot
stringqueryRead a named snapshot run id instead of the ACTIVE one. An unknown id is a 400, never a silent fall back to the active snapshot.
limit
integerqueryCollections per page. Defaults to 24, ceiling 100. A non-integer is a 400.
cursor
stringqueryOpaque cursor from a previous page's `nextCursor`. Only valid for the query that produced it.

Response

200A page of visual collections, with the snapshot stamp.
status"experimental"

The contract's advertised stability; mirrors `x-status` on this operation.

snapshotobject

Which snapshot served the read — the reproducibility anchor.

runIdstring
statestring
corpusRunIdstring
corpusContentDigeststring
activatedAtstring| null
labelModelstring| null
motifModelstring| null
styleBriefVersionstring| null
scopeobject
kindenum
catalogbrandcategory_groupproduct_typebrand_type
keystring

Partition-directory key: `catalog`, a brand id, or `{brandId}:{productTypeId}`.

selectorstring

The canonical, operator-typable selector for the family.

labelstring

Resolved display name, e.g. `Marazzi · Tile`. Never an id.

levelenum
broadspecific
returnedCountinteger

Collections on this page — never a total.

totalMatchesinteger

Collections matching the filter across every page.

nextCursorstring| null
collectionsobject[]
idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/clusters/visual/brand/{brandId}/{productTypeId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/clusters/{clusterId}mg:read

Get one visual collection, with its parent and children

One automatic visual collection plus one hop of hierarchy: the broad collection it sits under (`parent`, null at the top) and the specific collections under it (`children`, as full summaries so a detail page renders in one call). Memberships are never embedded — a caller walks the hierarchy rather than downloading it, and the six published exemplars are what render the tile. The read sits directly under `/clusters` rather than under a basis segment because a cluster id is a cluster id whatever basis produced it. A cluster id is snapshot-scoped: with no `?snapshot=` it resolves against the ACTIVE snapshot, and an id that resolves in neither the active nor the named snapshot is a 404 (`visual-cluster-not-found`) rather than a silent landing on whatever cluster now holds that ordinal. A basis name in the id position — anything but `visual`, which is served by its own path — is also a 404, and says so: only visual clustering exists today. EXPERIMENTAL — see `x-status`. Snapshots are immutable, so success responses carry `Cache-Control: private, max-age=60`. Requires the `mg:read` bearer scope.

Parameters

clusterId
stringpathThe collection id, as returned on a list `collections[].id`. Snapshot-scoped: an id from a retired snapshot does not resolve against the active one.
snapshot
stringqueryRead a named snapshot run id instead of the ACTIVE one. An unknown id is a 400, never a silent fall back to the active snapshot.

Response

200The collection, its parent, and its children.
status"experimental"
snapshotobject
runIdstring
statestring
corpusRunIdstring
corpusContentDigeststring
activatedAtstring| null
labelModelstring| null
motifModelstring| null
styleBriefVersionstring| null
collectionobject
idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject
parentobject| null

The broad collection this sits under, if any.

idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject
childrenobject[]

The specific collections under this one — summaries, so a page renders in one call.

idstring
scopeobject
levelenum
broadspecific
ordinalinteger
parentClusterIdstring| null
memberCountinteger

Images in the cluster. Memberships are never embedded.

titlestring| null
blurbstring| null
coverobject| null
exemplarsobject[]

The published six — brief-sized and diverse.

diagnosticsobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/clusters/{clusterId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/clusters/{clusterId}/membersmg:read

List the materials in one visual collection

What a visual collection actually contains, one page at a time. The collection reads deliberately never EMBED memberships — a broad collection runs to hundreds of thousands of rows — so this is where they are reachable. Each member carries the join keys rather than only display names (`brandSlug`, `categoryKey`), because every real use of this ends in a join, and `similarity`, which is what turns a listing into a measurement: a member count says a collection holds 149 things and cannot say whether they form one tight family or a loose bag. `categoryKey: null` means the variant is UNTYPED, never that it is typed as nothing — do not read it as a zero in a ratio. Paging is on an opaque cursor over the frozen artifact's row ordinal, so a pinned snapshot pages identically every time; pass `nextCursor` back unmodified. A `limit` above the maximum is REFUSED rather than clamped, because a caller who asks for 5,000 and silently receives 500 believes it holds the whole collection. Add `include=image` for the member's image identity — out by default, since the usual consumer computes rather than browses. EXPERIMENTAL — see `x-status`. Snapshots are immutable, so success responses carry `Cache-Control: private, max-age=60`. Requires the `mg:read` bearer scope.

Parameters

clusterId
stringpathThe collection id, as returned on a list `collections[].id`. Snapshot-scoped: an id from a retired snapshot does not resolve against the active one.
snapshot
stringqueryRead a named snapshot run id instead of the ACTIVE one. An unknown id is a 400, never a silent fall back to the active snapshot.
cursor
stringqueryThe `nextCursor` from the previous page, unmodified. A cursor this endpoint did not issue is a 400.
limit
integerqueryMembers per page, 1–500. Default 100. Above 500 is refused, not clamped.
include
stringqueryComma-separated extras. The only supported value is `image`; anything else is a 400.

Response

200A page of the collection's members, with the snapshot stamp.
status"experimental"
snapshotobject
runIdstring
statestring
corpusRunIdstring
corpusContentDigeststring
activatedAtstring| null
labelModelstring| null
motifModelstring| null
styleBriefVersionstring| null
clusterIdstring
levelenum
broadspecific
returnedCountinteger
totalMatchesinteger

Members in the whole collection, not on this page.

nextCursorstring| null
membersVisualClusterMember[]
variantIdstring

The material. An opaque token — never parse it.

brandSlugstring| null

Brand join key, stable across display-name edits. Null when the variant's brand can no longer be resolved.

brandNamestring| null

Human-readable brand, for rendering. Never join on this.

categoryKeystring| null

Primary product-type join key, e.g. `natural_stone_surface`. Null means UNTYPED, never 'typed as nothing' — do not let it enter a denominator.

categoryNamestring| null

Human-readable product type, for rendering only.

similaritynumber

Cosine similarity of this member's image to the collection's centroid, at the level being read. Higher is tighter. This is what separates a coherent look from a loose bag: a member count cannot tell you whether 149 items are one family or many.

imageobject

Present only with `include=image`.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/clusters/{clusterId}/members' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalog/attributes/resolvemg:read

Resolve a spec phrase to canonical attribute ids

Resolves a free-text spec phrase ("double rubs", "how fire-resistant", "eco certifications") to the canonical registry attribute ids it most likely means, ranked by meaning (best first). Alias matching is included — it rides the same registry embeddings, so it never drifts from a hand-maintained alias table. Each candidate is flagged `facetable`, telling you which ids work with `search/specifications`. The phrase is embedded per request (voyage text encoder), so this is a dynamic, Node-only endpoint — and because the answer depends on the registry vocabulary rather than on the caller, and that vocabulary changes only when an ingest lands, success responses carry `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Parameters

q
stringqueryA free-text spec phrase to resolve, e.g. "double rubs".
limit
integerqueryMax candidates to return.
facetableOnly
booleanqueryWhen `true` (or `1`), drop non-facetable attributes and return only queryable spec facets.

Response

200The ranked attribute candidates for the phrase.
querystring

Echo of the resolved (trimmed) query phrase.

candidatesobject[]

Matching attributes, best (nearest) first.

attributeIdstring

Canonical registry attribute id (e.g. abrasion_classifications).

namestring

Human display name from the registry.

descriptionstring

Registry description of the attribute.

scorenumber

Semantic similarity to the query in [0,1] (1 = nearest). Higher is a better match.

facetableboolean

Whether this attribute is a queryable spec facet for search/specifications.

blockIdsstring[]

The spec blocks this attribute appears in.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalog/attributes/resolve?q=text' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalog/attributes/{attributeId}/values/resolvemg:read

Resolve a value phrase to a canonical term for one attribute

Resolves a free-text value phrase ("navy", "flat, no shine") to the canonical term id it most likely means within one dictionary-backed registry attribute's value dictionary, ranked by confidence with a `resolved` / `ambiguous` / `unresolved` verdict — fail-fast, so the caller must act on the verdict rather than receive a silent best guess. An `{attributeId}` of `colour_family` additionally runs a colorscope fallback (brand paint names, descriptive phrases the dictionary can't name) on top of the base term ladder; every other dictionary-backed attribute resolves through the plain lexical/semantic ladder alone. `{attributeId}` must name a dictionary-backed attribute (one with a value dictionary) — a 400 explains when it doesn't exist or is free-form. The resolution depends on the value dictionary rather than on the caller, and that dictionary changes only when an ingest lands, so success responses carry `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Parameters

attributeId
stringpathA dictionary-backed registry attribute id, e.g. "colour_family". See `catalog/attributes/resolve` to find it from a phrase.
q
stringqueryA free-text value phrase to resolve, e.g. "navy".
limit
integerqueryMax candidates to return.

Response

200The ranked term candidates for the phrase.
attributeobject
idstring

The resolved registry attribute id (echo of the path segment).

namestring

Human display name from the registry.

dictionaryIdstring

The attribute's value dictionary id.

resolutionobject
statusenum

Caller verdict: `resolved` (act on `best`), `ambiguous` (disambiguate), `unresolved` (stop).

resolvedambiguousunresolved
querystring

Echo of the resolved (trimmed) query phrase.

bestobject| null

The leading candidate, or null when nothing was found.

candidatesobject[]

Candidates ranked best-first, capped at `limit`.

recommendedActionenum

The action the resolver recommends the caller take.

use_bestchoose_from_candidatesask_usernone_found

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalog/attributes/{attributeId}/values/resolve?q=text' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalog/specifications/facetsmg:read

Discover the queryable specification vocabulary

Lists the facetable material-spec attributes that have live product-graph data, each with its top canonical values and the variants and brands behind them. This is the discovery companion to `search/specifications`: learn an `attribute:value` vocabulary here, then use it as a repeatable search criterion. A facet's `kind` says which shape it takes — `token` facets carry that vocabulary in `values`, while `numeric` facets carry no vocabulary at all and instead report `range` (the observed min/max span, the variants carrying a usable number, and the variants whose recorded value is not a number and is therefore excluded); filter those with `specRange=attribute:min..max`. Facets shift only when an ingest lands, so responses carry `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Response

200The live specification-facet collection.
mode"facets"

The complete live specification-facet collection.

countinteger

Number of facets returned.

facetsobject[]

Queryable specification facets, most-cross-brand first.

attributeIdstring

Canonical attribute id — query it in search/specifications as `attribute:value`, or as `specRange=attribute:min..max` when `kind` is `numeric`.

namestring

Human-readable attribute name.

isArrayboolean

Whether the attribute holds multiple values per variant.

kindenum

How this facet is queried. `token` — pick a value from `values` and pass `spec=attribute:value`. `numeric` — state inclusive bounds and pass `specRange=attribute:min..max`; `values` is empty and `range` carries the span.

tokennumeric
brandsinteger

Brands carrying any value for this attribute.

valuesobject[]

Top canonical values, most-cross-brand first. EMPTY for a `numeric` facet, which has no token vocabulary.

rangeobject

Observed span + coverage. Present ONLY on a `numeric` facet.

descriptionstring

How the facet is queried (set for virtual facets like certification programmes).

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalog/specifications/facets' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/catalog/attributes/{attributeId}/valuesmg:read

List the live values for one attribute

Lists one attribute's live product-graph value distribution, including induced vocabularies absent from the static registry. Values are ordered by cross-brand reach and include variant and brand counts. Attribute identity is carried by the path. The distribution shifts only when an ingest lands, so responses carry `Cache-Control: public, max-age=300, stale-while-revalidate=3600`. Requires the `mg:read` bearer scope.

Parameters

attributeId
stringpathThe canonical attribute id whose live values should be listed.

Response

200The attribute's live value distribution.
mode"distribution"

A single attribute's live value distribution.

attributeIdstring

The canonical attribute id named by the path.

countinteger

How many values are in THIS RESPONSE. Compare with `totalValues` — a page is not a corpus.

totalValuesinteger

How many distinct values the attribute holds in total, ignoring `limit`.

truncatedboolean

True when values were cut by `limit`. A consumer diffed a 40-value page against its own vocabulary, called it complete, and rejected two attributes on the difference — this field is what makes that impossible to do by accident.

valuesobject[]

The attribute's top values in the product graph, most-cross-brand first.

valuestring

A canonical value token queryable in search/specifications.

variantsinteger

Variants carrying this value.

brandsinteger

Distinct brands carrying this value.

labelstring

Human label for the value, when one is known (e.g. certification programmes).

Example

curl
curl 'https://beta.materialgraph.com/api/v1/catalog/attributes/{attributeId}/values' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/taxonomy/categoriesmg:read

Get the product-category tree

The MaterialGraph product-category tree, flattened into a single array of `{key, name, parentKey, path, level}` nodes projected purely from the schema registry (no database). Groups are top-level (`parentKey: null`); each product type points back at its group via `parentKey`, with a display-name `path` like "Tile Surfaces / Ceramic and Porcelain Tile". The taxonomy is projected from the registry, so it changes on DEPLOY rather than on an ingest; it still ships the shared vocabulary window used across the read surface, `Cache-Control: public, max-age=300, stale-while-revalidate=3600`, rather than a longer hand-picked one. Requires the `mg:read` bearer scope.

Response

200The flat category-node list.
countinteger

Total number of category nodes returned.

categoriesobject[]

Flat list of every group and product-type node.

keystring

Stable identifier for this category, e.g. "tile_surfaces" or "ceramic_porcelain_tile".

namestring

Human-readable display name, e.g. "Ceramic and Porcelain Tile".

parentKeystring| null

The key of this node's parent group, or null for a top-level group.

pathstring

Display-name path from the root, " / " separated, e.g. "Tile Surfaces / Ceramic and Porcelain Tile".

levelenum

Tier of this node: "group" for a top-level category, "type" for a product type within a group.

grouptype

Example

curl
curl 'https://beta.materialgraph.com/api/v1/taxonomy/categories' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/taxonomy/categories/{key}/specsmg:read

Get the specs a product type could carry

Abstract applicability — given a product type (`key` = its id or display name), the blocks + attributes the SCHEMA says COULD apply, each block graded required / recommended / optional / unusual and each attribute flagged when the type pins it, regardless of whether any variant carries data. The abstract complement to `catalog/specifications/facets` (which reports the values that actually exist). Pure and in-memory, so its real invalidation is a deploy; it ships the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600`, rather than the day-long one it used to claim — a 24-hour blind cache is not more truthful than a five-minute one that revalidates. 404 when `key` names no product type (including a category GROUP, whose problem `detail` hints to pick a specific type). Requires the `mg:read` bearer scope.

Parameters

key
stringpathA product-type id or display name (from `taxonomy/categories`).

Response

200The applicable blocks and attributes for the product type.
foundboolean
productTypeobject| null
idstring
namestring
blocksobject[]
blockIdstring
namestring
applicabilityenum
requiredrecommendedoptionalunusual
attributesobject[]
hintstring

Example

curl
curl 'https://beta.materialgraph.com/api/v1/taxonomy/categories/{key}/specs' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/glossary/{term}mg:read

Resolve a term to its meaning in the schema

The schema's own glossary — resolves `term` (an attribute id/alias, or a canonical dictionary value) to its MEANING: an attribute definition (kind / unit / block membership) or a dictionary value's definition, so an agent can interpret the attribute ids and value tokens it sees in responses. Pure and in-memory, so responses carry `Cache-Control: public, max-age=86400`. 404 when the term does not resolve, with a hint to discover the vocabulary via `catalog/specifications/facets`. Requires the `mg:read` bearer scope.

Parameters

term
stringpathAn attribute id/alias or a canonical dictionary value token.

Response

200The resolved term meaning.
foundboolean
termstring
matchoneOf| null
kind: attributeobject
kind"attribute"
idstring
namestring
descriptionstring
valueKindstring
unitstring
facetableboolean
comparableboolean
dictionaryIdstring
usedInBlocksstring[]
kind: valueobject
kind"value"
idstring
labelstring
descriptionstring
aliasesstring[]
dictionaryIdstring
dictionaryNamestring
attributeIdsstring[]
alternatesobject[]
kindenum
attributevalue
idstring
labelstring

Example

curl
curl 'https://beta.materialgraph.com/api/v1/glossary/{term}' \  -H "Authorization: Bearer $MG_API_KEY"

Schema

GET/api/v1/taxonomy/coveragemg:read

Read exact primary product-type status coverage

An exact, read-only census of current non-superseded variants grouped by their complete stored source category path. It reports accepted, proposed, other-status, and missing-primary assignments separately. `comparisonThreshold` is a reporting cutoff for proposed rows only; it never accepts or promotes a proposal. Proposed rows without confidence remain explicit in `proposedUnknownConfidence`. Live database truth carries `Cache-Control: private, no-store`. Requires the `mg:read` bearer scope.

Parameters

comparisonThreshold
numberqueryReporting cutoff for proposed assignment counts, from 0 to 1. Default 0.85; it never changes acceptance status.

Response

200Exact source-path-grained product-type status coverage.
computedAtstring<date-time>
basisobject
count"exact"
universe"non_superseded_variants"
assignment"variant_primary_only"
sourcePath"products.source_category_path"
comparisonThresholdnumber

A reporting cutoff, not an inferred acceptance rule. Proposed assignments at or above this cutoff remain proposed.

totalsobject
variantsinteger
acceptedinteger
proposedinteger
proposedBelowThresholdinteger
proposedUnknownConfidenceinteger
otherStatusinteger
missingPrimaryinteger
sourcePathsobject[]
variantsinteger
acceptedinteger
proposedinteger
proposedBelowThresholdinteger
proposedUnknownConfidenceinteger
otherStatusinteger
missingPrimaryinteger
sourcePathstring| null

The complete stored source category path, verbatim; compound leaves are not split or guessed. Null means no path is recorded.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/taxonomy/coverage' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/blocksmg:read

List canonical schema blocks

Every canonical BLOCK — the named groupings the graph organises attributes into (`identity`, `colour`, `acoustic`, `fire`, `sustainability`, …), each with its `attributeIds`. Read this first when you need the shape of the record rather than one field: it is the map from a human concern ("acoustics", "fire performance") to the attribute ids that express it, so you can go from a vague requirement to the exact ids `/api/v1/search/specifications` will accept. Blocks are also the unit of coverage — `/api/v1/materials/{variantId}/blocks` reports how populated each block is for one material. Items carry governance: `semanticId` is the stable global identifier (`mg:attribute.nrc_rating`) that survives renames, and `lifecycle` is `{ status: draft | active | deprecated, introducedIn, deprecatedIn?, supersededBy? }` — prefer `active` ids, and follow `supersededBy` rather than reusing a deprecated one. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List canonical schema blocks
kind"blocks"
schemaVersionstring
itemsobject[]
idstring
semanticIdstring
lifecycleobject
namestring
descriptionstring
attributeIdsstring[]
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/blocks' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/attributesmg:read

List canonical schema attributes

Every canonical ATTRIBUTE — the full field dictionary the graph normalises every brand's data into. This is the authoritative answer to "what can I ask about, and in what terms". Per item: `valueKind` (`string` | `number` | `boolean` | `enum` | `reference` | …) tells you what a value looks like; `quantityKind` + `canonicalUnitId` + `acceptedUnitIds` tell you the unit the graph stores numbers in and which source units it will accept; `valueDictionaryId` points at the closed vocabulary an `enum` draws from (resolve it via `/api/v1/schema/value-dictionaries`) and `enumValues` inlines those tokens when there is one; `searchFacet` and `comparable` say whether the attribute can be filtered on and whether two materials can be ranked by it; `usedInBlocks` places it on the record; `externalMappings` carries its IFC / bSDD / ETIM / Uniclass / schema.org equivalents. Use it to translate a free-text requirement into a criterion before calling `/api/v1/search/specifications` — criteria are canonical ids and tokens, never prose. Items carry governance: `semanticId` is the stable global identifier (`mg:attribute.nrc_rating`) that survives renames, and `lifecycle` is `{ status: draft | active | deprecated, introducedIn, deprecatedIn?, supersededBy? }` — prefer `active` ids, and follow `supersededBy` rather than reusing a deprecated one. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List canonical schema attributes
kind"attributes"
schemaVersionstring
itemsobject[]
idstring
semanticIdstring
lifecycleobject
namestring
descriptionstring
valueKindenum
stringtextintegernumberbooleanenum+2 more
quantityKindenum| null
lengthareavolumemasstimetemperature+14 more
canonicalUnitIdstring| null
acceptedUnitIdsstring[]
valueDictionaryIdstring| null
defaultSourceRequirementIdstring| null
externalMappingsobject[]
searchFacetboolean
comparableboolean
usedInBlocksstring[]
enumValuesstring[]| null
shapeIdstring| null
domainContractobject
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/attributes' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/verticalsmg:read

List schema verticals

The top-level VERTICALS the catalog is divided into — `materials` (finishes and construction materials), `ffe` (furniture, fixtures and equipment), `architectural` (systems, assemblies, sanitaryware, hardware) and `equipment` (lighting and technical systems). The smallest registry here and the cheapest orientation call: every product type belongs to exactly one vertical (`verticalId` on `/api/v1/schema/product-types`), so this is the coarse filter to reach for before walking the full product-type registry. Items are `{ id, displayName, description }` only — no governance metadata. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List schema verticals
kind"verticals"
schemaVersionstring
itemsobject[]
idstring
displayNamestring
descriptionstring
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/verticals' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/product-typesmg:read

List governed product types

Every governed PRODUCT TYPE — the category taxonomy the graph classifies products into (modular carpet, resilient sheet, task seating, …), and the definition of what each type is expected to carry. Per item: `verticalId` places it under a vertical; `parentProductTypeGroupId` gives the parent when types are grouped; `blocks` lists the blocks that apply with an `applicability` of `required` | `recommended` | `optional` | `unusual`, plus per-block `requiredAttributes` / `optionalAttributes` and a `completenessWeight`; `productTypeSpecificAttributes` names fields that exist only for this type. Use it to answer "what should a product of this kind have on it" — the applicability grades are what make a missing field either a real data gap or simply inapplicable. For the completeness scoring rules themselves see `/api/v1/schema/product-type-templates`; for the customer-facing browse taxonomy see `/api/v1/taxonomy/categories`. Items carry governance: `semanticId` is the stable global identifier (`mg:attribute.nrc_rating`) that survives renames, and `lifecycle` is `{ status: draft | active | deprecated, introducedIn, deprecatedIn?, supersededBy? }` — prefer `active` ids, and follow `supersededBy` rather than reusing a deprecated one. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List governed product types
kind"productTypes"
schemaVersionstring
itemsobject[]
idstring
namestring
descriptionstring
verticalIdstring
parentProductTypeGroupIdstring
blocksobject[]
productTypeSpecificAttributesstring[]
semanticIdstring
lifecycleobject
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/product-types' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/unitsmg:read

List canonical units

The canonical UNIT registry — one entry per unit the graph stores values in, with the conversions it accepts on the way in. Per item: `quantityKind` (`length`, `area`, `mass`, `emissions`, `luminous_flux`, `currency`, `ratio`, …) is the dimension; `symbol` is how it prints; `acceptedSourceUnits` are the spellings found in manufacturer source that map onto it; `conversionToCanonical` is the `{ factor, offset? }` applied to reach the canonical unit. Read it when you need to interpret or restate a numeric value correctly: a numeric attribute's `canonicalUnitId` names an id here, and every number the API returns for that attribute is already in the canonical unit — the conversion metadata explains what was done to the source figure, it is not something a caller needs to apply. Items carry governance: `semanticId` is the stable global identifier (`mg:attribute.nrc_rating`) that survives renames, and `lifecycle` is `{ status: draft | active | deprecated, introducedIn, deprecatedIn?, supersededBy? }` — prefer `active` ids, and follow `supersededBy` rather than reusing a deprecated one. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List canonical units
kind"units"
schemaVersionstring
itemsobject[]
idstring
namestring
symbolstring
quantityKindenum
lengthareavolumemasstimetemperature+14 more
acceptedSourceUnitsstring[]
conversionToCanonicalobject
notesstring
semanticIdstring
lifecycleobject
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/units' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/value-dictionariesmg:read

List canonical value dictionaries

The closed VOCABULARIES behind every `enum` attribute — the exact tokens a spec query may use, and the aliases each token absorbs. Per dictionary, `terms` carries `{ id, label, aliases?, broaderTermId?, externalMappings?, metadata? }`: `id` is the canonical token to send, `label` is the human name to show, `aliases` are the brand-specific spellings the graph folds into it (this is the normalisation, made inspectable), and `broaderTermId` links a narrow term to its parent so you can widen a query that returns too little. Reach for this when a criterion is rejected or returns nothing — the value is usually right in spirit and wrong in spelling. For a ranked lookup rather than the full dump, `/api/v1/catalog/vocabulary/search` searches the same terms. Items carry governance: `semanticId` is the stable global identifier (`mg:attribute.nrc_rating`) that survives renames, and `lifecycle` is `{ status: draft | active | deprecated, introducedIn, deprecatedIn?, supersededBy? }` — prefer `active` ids, and follow `supersededBy` rather than reusing a deprecated one. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List canonical value dictionaries
kind"valueDictionaries"
schemaVersionstring
itemsobject[]
idstring
namestring
descriptionstring
authorityoneOf
kind: taxonomyobject
kind: colorscopeobject
kind: materialgraphobject
kind: external-standardobject
termsobject[]
semanticIdstring
lifecycleobject
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/value-dictionaries' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/source-requirementsmg:read

List source requirements

The EVIDENCE POLICIES that govern what a value must be backed by before the graph will hold it — the machinery behind every confidence figure and source basis the API reports. Per policy: `allowedSourceTypes` (`technical_data_sheet`, `test_report`, `certificate`, `manufacturer_page`, …) and `allowedMethods` (`manual_review`, `agent_extract`, `pdf_extract`, `calculation`, …) bound what may establish a value; `minConfidence` is the floor it must clear; `reviewRequired` says a human must sign it off; `freshnessDays` is how long it stays current; `sourceBasis` distinguishes `declared` (the manufacturer says so), `derived` (MaterialGraph computed it) and `tested` (a report proves it). The smallest registry that most changes how you should report an answer: an attribute's `defaultSourceRequirementId` names the policy applied to it, and a `sourceBasis` of `derived` is a claim, not a manufacturer specification. Items carry governance: `semanticId` is the stable global identifier (`mg:attribute.nrc_rating`) that survives renames, and `lifecycle` is `{ status: draft | active | deprecated, introducedIn, deprecatedIn?, supersededBy? }` — prefer `active` ids, and follow `supersededBy` rather than reusing a deprecated one. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List source requirements
kind"sourceRequirements"
schemaVersionstring
itemsobject[]
idstring
namestring
descriptionstring
allowedSourceTypesenum[]
manufacturer_pagetechnical_data_sheetsafety_data_sheettest_reportcertificateimage+3 more
allowedMethodsenum[]
manual_reviewagent_extractpdf_extractimage_analysishtml_scrapestructured_feed+1 more
minConfidencenumber
reviewRequiredboolean
freshnessDaysnumber
sourceBasisenum
declaredderived
semanticIdstring
lifecycleobject
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/source-requirements' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/product-type-templatesmg:read

List product-type completeness templates

The COMPLETENESS TEMPLATES — one per product type (`productTypeId`), defining what a complete record of that type looks like and how partial ones are scored. Per block: `applicability`, `requiredAttributes` / `optionalAttributes`, a `sourceRequirementId` naming the evidence policy that block's values must satisfy, and a `completenessWeight` setting how much it contributes to the score. Use it to explain or predict a coverage figure — why one material reads as thin and another as complete — rather than to discover what exists: the applicable blocks themselves are on `/api/v1/schema/product-types`, and the measured per-block coverage for a specific material is on `/api/v1/materials/{variantId}/blocks`. Items carry governance: `semanticId` is the stable global identifier (`mg:attribute.nrc_rating`) that survives renames, and `lifecycle` is `{ status: draft | active | deprecated, introducedIn, deprecatedIn?, supersededBy? }` — prefer `active` ids, and follow `supersededBy` rather than reusing a deprecated one. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List product-type completeness templates
kind"productTypeTemplates"
schemaVersionstring
itemsobject[]
idstring
namestring
productTypeIdstring
blocksobject[]
defaultSourceRequirementIdstring
notesstring
semanticIdstring
lifecycleobject
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/product-type-templates' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schema/mappingsmg:read

List interoperability mappings

The INTEROPERABILITY crosswalk — every declared correspondence between a MaterialGraph concept and an external standard, flattened into one list. Per row: `target` is the standard (`schema_org`, `ifc_pset`, `bsdd`, `iso_23387`, `eclass`, `gs1_gdm`, `gdsn`, `etim`, `uniclass`); `subjectKind` + `subjectId` name what is being mapped (an attribute, block, product type, template, unit, value dictionary, or dictionary term); `canonicalPath` is its address in the graph (`blocks.colour.colour_name`) and `targetPath` its address in the standard (`Product.color`); `mappingType` grades the correspondence — `direct` (one-to-one), `transform` (unit or format change), `derived` (computed from other fields), `classification` (a taxonomy placement) and `semantic` (related in meaning, not interchangeable). Use it to export a record into another system's vocabulary, or to accept a query expressed in one. Treat anything other than `direct` as needing judgement, and note the crosswalk is a declared subset — absence here means unmapped, not incompatible. Envelope: `{ kind, schemaVersion, items, totalCount }` — `schemaVersion` is the registry's own version (not the OpenAPI spec version), and `totalCount` is the complete registry size, never a page (these reads are unpaginated). Deterministic: the registry is compiled into the deploy, so the same deploy always answers identically and responses carry the shared vocabulary window, `Cache-Control: public, max-age=300, stale-while-revalidate=3600` — a deploy invalidates it for free, so a five-minute floor with revalidation is more honest than the day-long literal this used to claim. Requires the `mg:read` bearer scope.

Response

200List interoperability mappings
kind"mappings"
schemaVersionstring
itemsobject[]
targetenum
schema_orgifc_psetbsddiso_23387eclassgs1_gdm+3 more
subjectKindenum
attributeblockproduct_typetemplateunitvalue_dictionary+1 more
subjectIdstring
canonicalPathstring
targetPathstring
mappingTypeenum
directtransformderivedclassificationsemantic
notesstring
totalCountinteger

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schema/mappings' \  -H "Authorization: Bearer $MG_API_KEY"

Brands

GET/api/v1/brandsmg:read

List registered brands

Lists canonical brand registry entries, including brands that do not yet have variants. Results are ordered deterministically by name, slug, and id and use offset pagination. Requires the `mg:read` bearer scope.

Parameters

q
stringqueryCase-insensitive brand-name filter.
limit
integerqueryMaximum brands to return.
offset
integerqueryZero-based number of ordered brands to skip.
corpusScope
enumqueryWhich corpus to answer from. samplizeCatalogue admits published US Samplize paint colours regardless of stock. ABSENT (or `all`) means EVERY brand MaterialGraph knows; absence is never a narrower default. Every scope narrows the brands listed AND counts `productCount` / `variantCount` over the same corpus, so a partly-orderable brand reports only the selected corpus rather than its whole catalogue. `materialBankNaOrderable`, `materialBankEuOrderable` and `designShopOrderable` are transaction-oriented; `materialBankNaAndCatalogue` is a governed discovery corpus and does not claim every member is buyable. All gate on governed CHANNEL membership: a brand appears exactly while at least one of its own materials belongs to the named distribution or editorial surface, because brand-level membership is a roll-up of the variants rather than a stored brand fact. `materialBankOrderable` is different — it omits the brands that carry no Material Bank PRESENCE, which is a provenance signal rather than a storefront, and nearly every brand in the feed carries it. The response echoes the applied scope under `corpusScope`. An unknown value is a 400 naming it.

Response

200The requested page of canonical brands.
itemsobject[]
idstring
slugstring
namestring
primaryMarketSegmentstring
homepageUrlstring| null
materialBankPresenceboolean| null

Whether this brand is ON RECORD as carrying Material Bank presence. `true` is the standing orderability tell — every brand holding it has Material Bank SKUs. `null` (or absent) means NOT ON RECORD, never false: the brand may still hold products, none of them orderable through Material Bank. `productCount` and `variantCount` are counted over the WHOLE catalogue unless `corpusScope` says otherwise, so a brand without this flag can still report thousands of variants.

productCountinteger
variantCountinteger
corpusScopeobject

The corpus this page AND its per-brand counts were drawn from. Present only when a narrowing scope applied — absence means the whole graph. Read it alongside `meta.total`: a short list under a scope is a small SCOPED corpus, not a truncated one.

scopeenum

Which corpus to list from. samplizeCatalogue admits published US paint colours regardless of stock. ABSENT (or 'all') means the WHOLE GRAPH — every brand MaterialGraph knows, orderable or not; absence is never a narrower default. Every scope narrows the BRANDS listed AND the corpus `productCount` / `variantCount` are counted over, both applied INSIDE the query: a page of `limit` fills with eligible brands rather than being shortened afterwards. 'materialBankNaOrderable', 'materialBankEuOrderable', 'materialBankNaAndCatalogue' and 'designShopOrderable' gate on governed CHANNEL membership. The token-specific receipt states whether that membership is a storefront or editorial inclusion; the union is not itself a transaction claim. 'materialBankOrderable' does not: it lists brands on record as carrying Material Bank presence, which is a provenance signal, and nearly every brand in the feed carries it. An unknown value is a 400 naming it, never a silent fall back to the whole graph.

allsamplizeCataloguematerialBankOrderablematerialBankNaOrderablematerialBankNaAndCataloguematerialBankEuOrderable+1 more
descriptionstring

What the scope admits, in one sentence, for a human reader.

metaobject
countinteger
totalinteger
limitinteger
offsetinteger
hasMoreboolean

Example

curl
curl 'https://beta.materialgraph.com/api/v1/brands' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/brands/{brandSlug}mg:read

Resolve a brand and return its profile

Brand identity (HQ, parent company, positioning, website) plus trade / specification signals (Material Bank presence, BIM library, sample program, trade program, CPD/CEU) — the spec-channel questions the per-material reads can't answer. `brandSlug` is the brand slug, name, or id; `?q=` supplies an alternate fuzzy query and wins over the path. Resolution is exact → slug/id → trigram fuzzy; on ambiguity the best match is returned and the rest ride `alternates`. Brand identity is effectively static, so responses carry `Cache-Control: public, max-age=300`. Requires the `mg:read` bearer scope.

Parameters

brandSlug
stringpathThe brand slug, name, or id.
q
stringqueryAlternate fuzzy query; takes precedence over the path when present.

Response

200The resolved brand profile.
foundboolean

Whether the name resolved to a brand.

querystring

The name that was looked up.

brandobject| null

Brand identity, or null when the name did not resolve.

idstring
slugstring
namestring
marketSegmentstring

The brand's primary market segment (e.g. textiles, tile).

headquartersCitystring| null

HQ city, when on record.

headquartersCountrystring| null

HQ country in canonical short form (US, UK, Italy), when on record.

parentBrandobject| null

Parent brand / holding company, when this brand has one.

positioningstring| null

Price/prestige tier (luxury, premium, mid-market, value, trade-only).

ownershipTypestring| null

Ownership structure (private, public, family, cooperative, subsidiary).

foundedYearinteger| null
descriptionstring| null
websitestring| null

The brand's primary website URL, when on record.

tradeobject

Trade / specification signals that ARE on record, keyed by canonical attribute id (material_bank_presence, bim_library_url, sample_program_url, trade_program_url, cpd_ceu_offerings). Absent keys mean we hold no value — do not infer false/absent as a claim.

linksobject[]

Labelled resource links on record (press kit, brand guidelines), when present.

labelstring
urlstring
alternatesobject[]

Other brands the name also matched, when ambiguous — the best match is in `brand`.

idstring
namestring

Example

curl
curl 'https://beta.materialgraph.com/api/v1/brands/{brandSlug}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/brands/{brandSlug}/coloursmg:read

A brand's range as a fan deck

A brand's colours grouped by colour FAMILY — creams together, then greys, then browns — and ordered light to dark within each. The order a person reads a paint wall in, which no stored scalar produces. Deliberately NOT paginated: family order is only correct over a COMPLETE range, because the greys cannot be placed relative to the browns while holding one page of each. The whole range returns at once and a range past the cap reports `truncatedAt` rather than being quietly cut. Each swatch carries the PUBLISHED hex where the maker has one (`declared`, else `catalog_feed`) and the palette's dominant colour otherwise. A hex colorscope cannot place in a family is dropped rather than guessed into one, so the deck can be shorter than the brand's variant count. Requires the `mg:read` bearer scope.

Parameters

brandSlug
stringpathBrand slug or display name, exactly as the catalogue carries it. An unknown brand is a 400 naming it, never a 200 with an empty deck.

Response

200The brand's colours in fan-deck order.
brandsstring[]

The brand slugs the deck resolved to.

swatchesobject[]
variantIdstring
brandstring
namestring| null
hexstring

`#rrggbb`. The maker's published hex where there is one.

familystring

colorscope's colour family id, e.g. `cream`. Never null — a hex that cannot be placed is dropped from the deck rather than guessed into a family.

lightnessnumber

OKLab lightness 0–1; the order within a family.

imageUrlstring| null
familiesobject[]

Family id and size, in the order the deck presents them.

familystring
countinteger
truncatedAtinteger

Present ONLY when the range exceeded the cap. ABSENT means this is the brand's whole range, not merely the part that fitted.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/brands/{brandSlug}/colours' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/brands/{brandSlug}/revit/librarymg:read

Download a brand's bulk Revit library (.zip)

Reads every variant of a brand in one round-trip and composes a bulk Revit library zip — a shared-parameters definition, one type-catalog row per material, a colours.csv reference, and a README. Leaner than the per-material package (no per-material classification). `?limit=` (default 1000, capped at 5000) bounds the export; when the brand has more materials than the cap, the library is truncated, the response carries `X-MG-Truncated: true`, AND the README states the truncation in band. Bytes are built fresh per request, so the response is `Cache-Control: no-store`. 404 when the brandSlug resolves to no exportable materials. Requires the `mg:read` bearer scope.

Parameters

brandSlug
stringpathThe brand slug, name, or id.
limit
integerqueryMax materials to export (default 1000, capped at 5000). When the brand has more, the library is truncated and `X-MG-Truncated: true` is set.

Response

200The bulk Revit library zip.
string<binary>

Example

curl
curl 'https://beta.materialgraph.com/api/v1/brands/{brandSlug}/revit/library' \  -H "Authorization: Bearer $MG_API_KEY"

Sources

GET/api/v1/citations/{citationId}mg:read

Resolve a verdict citation

Resolves a verdict cell's opaque citation handle to canonical claim-source identifiers and locator metadata. Excerpts, raw provider payloads, private URLs and storage credentials are never returned. Requires `mg:read`.

Parameters

citationId
stringpath

Response

200The canonical claim source.
citationIdstring
sourceTypestring
sourceobject
kindenum
source_recordsource_materialsource_facturlassetdownload+2 more
refstring
titlestring
urlstring<uri>
publicPathstring
sourceIdstring
sourceFactIdstring
sourceAssetIdstring
sourceDownloadIdstring
locatorobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/citations/{citationId}' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/source-materialmg:readmg:compose

Upload a source-material batch — a heterogeneous pile of files

Accepts one atomic multipart batch as a PILE of source files: a repeatable `files` field carrying any mix of PDF (≤30 MiB, ≤250 pages), plain-text/markdown (≤2 MiB), and images (JPEG, PNG, WebP, or AVIF, each ≤8 MiB) — at least one file, at most 16, ≤128 MiB aggregate. There is no need to pre-sort a brief from its references: the pipeline classifies each file and stamps its receipt `role` (`brief`, `reference_image`, `document`, `document_image`, `inspiration_image`, or `unclassified`). The legacy pre-classified shape — `brief` (a single PDF) plus a repeatable `references` field (images) — is still accepted and folded into the pile. At least one of `files`, `brief`, or `references` must be present. Bytes are validated strictly (media type, magic bytes, PDF structure and page count) before any write; any invalid file rejects the whole batch with per-file reasons in the problem `issues` member and nothing is stored. On success the bytes are stored content-addressed and a typed receipt is returned. Requires the `mg:compose` bearer scope (designer working data — a `compose` key suffices; broader ingest/write/admin keys also carry it).

Request body

filesstring<binary>[]

The heterogeneous pile: any mix of PDF, text/markdown, and images (≥1, ≤16 files across the whole batch).

briefstring<binary>

Legacy: the single PDF brief for the batch. Folded into `files`.

referencesstring<binary>[]

Legacy: up to 12 reference images (JPEG, PNG, WebP, or AVIF). Folded into `files`.

Response

201The stored batch receipt.
batchIdstring

The stored batch id (`smb_…`).

sourcesobject[]
sourceIdstring

The stored file id (`smf_…`).

roleenum

The pipeline-assigned role. `unclassified` is a legitimate terminal value when the file could not be placed; clients should tolerate new values.

briefreference_imagedocumentdocument_imageinspiration_imageunclassified
filenamestring
mediaTypestring
sha256string

Lowercase hex SHA-256 of the stored bytes.

bytesinteger
pagesinteger| null

PDF page count; null for images.

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/source-material' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "files": [    "…"  ],  "brief": "…"}'
GET/api/v1/source-material/{batchId}mg:readmg:compose

Read an uploaded source-material batch and its analyses

Returns one upload batch: its metadata, every file it holds with a fetchable `url`, and every analysis computed over each file's bytes inlined as `analyses`. `analyses` is always an array — `[]` means nothing has been computed yet, never a missing field — so the client contract does not change when preprocessing becomes automatic. Analyses are content-addressed by the file's SHA-256 rather than by upload, so re-uploading an image that has already been analysed returns that analysis immediately; this shares results across API keys by design and discloses nothing, since you only ever see analyses of images you hold yourself. A `failed` analysis is returned with its status rather than omitted. A batch belonging to a different principal is indistinguishable from one that does not exist: both answer 404, never 403, so batch ids cannot be probed for existence. Requires the `mg:compose` bearer scope — the same key gate as the upload.

Parameters

batchId
stringpathThe batch id (`smb_…`) returned by `POST /api/v1/source-material`.

Response

200The batch, its files, and their analyses.
batchIdstring

The stored batch id (`smb_…`).

createdAtstring

ISO-8601 timestamp of the upload.

sourcesobject[]
sourceIdstring

The stored file id (`smf_…`).

roleenum

The pipeline-assigned role. `unclassified` is a legitimate terminal value when the file could not be placed; clients should tolerate new values.

briefreference_imagedocumentdocument_imageinspiration_imageunclassified
filenamestring
mediaTypestring
sha256string

Lowercase hex SHA-256 of the stored bytes.

bytesinteger
pagesinteger| null

PDF page count; null for images.

urlstring

Fetchable URL for the stored bytes.

analysesSourceImageAnalysis[]

Every analysis computed over this file's bytes. ALWAYS an array: `[]` means nothing has been computed yet, never a missing field. Preprocessing is triggered manually today; when it becomes automatic only the trigger changes and this contract does not.

commentsSourceMaterialComment[]

The argument about this file - the thread of remarks people left on it, oldest root first, each carrying its replies. ALWAYS an array: `[]` means nobody has commented, which is a state to draw rather than an absence to guess at.

commentsTruncatedboolean

`true` when this file holds more than 500 comment rows, so `comments` is the oldest of them rather than all. Always present; `false` on a file whose thread you are seeing in full, including one nobody has commented on.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/source-material/{batchId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/source-material/{batchId}/sources/{sourceId}/commentsmg:readmg:compose

Read the comment thread on one source-material file

Returns the argument about one uploaded file: every remark left on it, oldest root first, each carrying the replies that answer it. Threads are one level deep, so a reply never carries replies of its own. `comments` is always an array — `[]` means the file is yours and nobody has said anything about it, which is a state to draw rather than an absence to guess at. Author names are REPORTED, not verified: each carries `author.attribution: "reported"` and the principal that recorded it in `recordedBy`, so render 'recorded as from X' rather than presenting the name as X's own signed statement. A read inlines at most 500 comment rows per file and says so with `truncated`. A file that is not in that batch, or a batch owned by a different principal, is indistinguishable from a file that does not exist: both answer 404, never 403, so upload ids cannot be probed for existence. The same thread is also inlined per file on `GET /api/v1/source-material/{batchId}`, so a client opening a batch needs one call, not one per file. Requires the `mg:compose` bearer scope — the same key gate as the upload.

Parameters

batchId
stringpathThe batch id (`smb_…`) returned by `POST /api/v1/source-material`.
sourceId
stringpathThe file id (`smf_…`) the thread hangs off, as returned in `sources[].sourceId`.

Response

200The file's thread, oldest root first.
sourceIdstring

The file the thread hangs off (`smf_…`).

commentsSourceMaterialComment[]

The thread, oldest root first. ALWAYS an array: `[]` means the file is yours and nobody has commented on it.

commentIdstring

The stored comment id (`smc_…`).

authorobject
recordedByobject
bodystring
createdAtstring

ISO-8601 timestamp of the posting.

repliesSourceMaterialCommentReply[]

The replies answering this comment, oldest first. ALWAYS an array: `[]` means nobody answered, never a missing field.

truncatedboolean

`true` when this file holds more than 500 comment rows and `comments` is therefore the oldest of them rather than all. Always present: whether you are looking at the whole thread is an answer, not something to infer from a count.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/source-material/{batchId}/sources/{sourceId}/comments' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/source-material/{batchId}/sources/{sourceId}/commentsmg:readmg:compose

Comment on one source-material file

Adds one remark to a file's thread. The author is who the comment is FROM — usually the client or a colleague, not the API principal, which is recorded separately for the audit trail — so `author.name` is required and `author.organisation` is nullable rather than optional. Nothing verifies that name: it is stored and read back as REPORTED attribution alongside the principal that recorded it, and a consumer must not present it as a signed statement. Pass `parentCommentId` to answer an existing remark; it must be a ROOT comment on the same file, because threads are one level deep and the database enforces it. A parent that is itself a reply, or one that belongs to another file, is a 400 naming which; a file that is not yours is the same 404 the read returns. Pass `idempotencyKey` so a retry after a network timeout returns the comment you already posted rather than a duplicate. The stored comment comes back discriminated on `kind`: a `root` carries an empty `replies`, a `reply` carries `parentCommentId` and no `replies` at all. Requires the `mg:compose` bearer scope.

Parameters

batchId
stringpathThe batch id (`smb_…`) returned by `POST /api/v1/source-material`.
sourceId
stringpathThe file id (`smf_…`) the comment hangs off, as returned in `sources[].sourceId`.

Request body

authorobject
namestring

Who the comment is reported to be from. Nothing authenticates this name — it is transcription, and it is read back labelled as such.

organisationstring| null

Their practice or team. Nullable, NOT optional: `null` is the answer for a note carrying no organisation, and the key is always present.

bodystring
parentCommentIdstring| null

The root comment being answered, or null/absent for a new remark. Must be a ROOT comment on the same file: replying to a reply is a 400, because threads are one level deep.

idempotencyKeystring| null

Your own token for this one intended remark. Send it and a retry after a network timeout — where you never learnt whether the first attempt landed — returns the comment that already exists instead of posting a duplicate. Scoped to the file: the same token on another file is another remark.

Response

201The stored comment.
sourceIdstring

The file the comment hangs off (`smf_…`).

commentoneOf
kind: rootobject
commentIdstring

The stored comment id (`smc_…`).

authorobject
recordedByobject
bodystring
createdAtstring

ISO-8601 timestamp of the posting.

repliesSourceMaterialCommentReply[]

The replies answering this comment, oldest first. ALWAYS an array: `[]` means nobody answered, never a missing field.

kind"root"
kind: replyobject
commentIdstring

The stored comment id (`smc_…`).

authorobject
recordedByobject
bodystring
createdAtstring

ISO-8601 timestamp of the posting.

kind"reply"
parentCommentIdstring

The root comment this one answers.

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/source-material/{batchId}/sources/{sourceId}/comments' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "author": {    "name": "…",    "organisation": "…"  },  "body": "…"}'

Runs

GET/api/v1/capabilitiesmg:read

List invocable agents, workflows, and tools

Returns the governed public capability catalogue, including live input/output schemas, effects, scopes, timeouts, and retry policy. Internal capabilities such as Web Browser Agent are excluded. Requires `mg:read`.

Response

200The public capability catalogue.
capabilitiesobject[]
publicIdstring
runtimeIdstring
kindenum
agenttoolworkflow
exposureenum
publicpartnerinternal
namestring
descriptionstring
schemasobject
sideEffectClassenum
read_onlybrowser_localexternal_readwrites_proposedwrites_persistedplanned_only
scopesenum[]
mg:readmg:composemg:writemg:ingestmg:admin
timeoutMsinteger
retryPolicyobject
tagsstring[]
linksobject

Example

curl
curl 'https://beta.materialgraph.com/api/v1/capabilities' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/runsmg:readmg:compose or mg:write

Start an asynchronous capability Run

Validates capability input and scope, transactionally records the Run and encrypted outbox command, then acknowledges without executing inline. Supply `Idempotency-Key` or `idempotencyKey` to make retries safe. Requires either the `mg:compose` or the `mg:write` scope. Starting a capability additionally requires whatever scope that capability declares.

Parameters

Idempotency-Key
stringheaderStable caller key for idempotent Run creation.

Request body

capabilityKindenum
agenttoolworkflow
capabilityIdstring
inputJsonunknown
idempotencyKeystring
clientReferencestring
scheduleIdstring

The Schedule this Run composes for. Create it with `POST /api/v1/schedules` and pass its id here; the Run then writes its composition into that Schedule as a new immutable revision. Omit for a Run that composes without a Schedule behind it. A Schedule that is not visible to the calling principal is refused with 404 AT CREATE TIME — a Schedule owned by another principal is indistinguishable from one that does not exist, and refusing here rather than at persist means a contradiction costs a request instead of a whole composition.

Response

200An idempotent replay of an existing Run.
acceptedboolean
replayedboolean
workflowRunIdstring
runPublicRun
idstring
capabilityKindenum
agenttoolworkflow
capabilityIdstring
clientReferencestring| null
statusenum
queuedclaimedrunningsuspendedsucceededfailed+1 more
redactedInputJsonobject
redactedOutputJsonobject| null
usageJsonobject| null
artifactRefsobject[]| null
failureJsonPublicRunFailure| null
suspensionPayloadobject| null

The current typed question payload while `status` is `suspended`; null otherwise. The same exact payload is retained on the `suspended` ledger event.

suspensionStepIdstring| null

The step awaiting input while `status` is `suspended` — pass it back as `stepId` when resuming, so a stale client cannot resume a step the run has already moved past. Read `suspensionPayload` for the current typed question. The same exact payload is retained on the `suspended` ledger event for event-stream consumers.

eventCursorinteger
attemptsinteger
startedAtstring<date-time>| null
cancellationRequestedAtstring<date-time>| null
completedAtstring<date-time>| null
createdAtstring<date-time>
updatedAtstring<date-time>

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/runs' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "capabilityKind": "agent",  "capabilityId": "…",  "inputJson": "…"}'
GET/api/v1/runsmg:read

List your own Runs

Enumerates the calling principal's own Runs, newest first (`createdAt` descending). This is the RECOVERY primitive: a client that still holds its API key but has lost a Run id — a reinstalled app, a crash before the id was persisted — finds its work again here, and `?status=suspended` finds the Run still waiting on an answer. Ownership is scoped exactly as `GET /api/v1/runs/{runId}` is: another principal's Runs are never selected, and there is no cross-principal or administrative listing on this surface. Each item is the same redacted projection the single-Run read returns. Paging is keyset, not offset, so a feed that grows mid-walk neither duplicates nor skips a Run: follow `nextCursor` until it is absent. Requires `mg:read`.

Parameters

status
enumqueryReturn only Runs in this lifecycle status. Omit for every status. A status outside the vocabulary is a `400`, never an empty list.
limit
integerqueryPage size.
cursor
stringqueryThe opaque `nextCursor` from the previous page. A cursor this API did not issue is a `400`, not a silent restart from the newest page.

Response

200One page of the caller's own Runs, newest first.
runsPublicRun[]
idstring
capabilityKindenum
agenttoolworkflow
capabilityIdstring
clientReferencestring| null
statusenum
queuedclaimedrunningsuspendedsucceededfailed+1 more
redactedInputJsonobject
redactedOutputJsonobject| null
usageJsonobject| null
artifactRefsobject[]| null
failureJsonPublicRunFailure| null
suspensionPayloadobject| null

The current typed question payload while `status` is `suspended`; null otherwise. The same exact payload is retained on the `suspended` ledger event.

suspensionStepIdstring| null

The step awaiting input while `status` is `suspended` — pass it back as `stepId` when resuming, so a stale client cannot resume a step the run has already moved past. Read `suspensionPayload` for the current typed question. The same exact payload is retained on the `suspended` ledger event for event-stream consumers.

eventCursorinteger
attemptsinteger
startedAtstring<date-time>| null
cancellationRequestedAtstring<date-time>| null
completedAtstring<date-time>| null
createdAtstring<date-time>
updatedAtstring<date-time>
nextCursorstring

Opaque continuation token. Present only when more Runs match; ABSENT means this is the last page. Round-trip it verbatim as `?cursor=` — never parse or construct one.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/runs' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/runs/{runId}mg:read

Get a Run's current state

Returns the caller-owned redacted Run ledger record. Prompts, raw model messages, reasoning, credentials, provider payloads, and source-document contents are never returned. Requires `mg:read`.

Parameters

runId
stringpathThe public Run identifier returned by `POST /api/v1/runs`.

Response

200The current Run state.
runPublicRun
idstring
capabilityKindenum
agenttoolworkflow
capabilityIdstring
clientReferencestring| null
statusenum
queuedclaimedrunningsuspendedsucceededfailed+1 more
redactedInputJsonobject
redactedOutputJsonobject| null
usageJsonobject| null
artifactRefsobject[]| null
failureJsonPublicRunFailure| null
suspensionPayloadobject| null

The current typed question payload while `status` is `suspended`; null otherwise. The same exact payload is retained on the `suspended` ledger event.

suspensionStepIdstring| null

The step awaiting input while `status` is `suspended` — pass it back as `stepId` when resuming, so a stale client cannot resume a step the run has already moved past. Read `suspensionPayload` for the current typed question. The same exact payload is retained on the `suspended` ledger event for event-stream consumers.

eventCursorinteger
attemptsinteger
startedAtstring<date-time>| null
cancellationRequestedAtstring<date-time>| null
completedAtstring<date-time>| null
createdAtstring<date-time>
updatedAtstring<date-time>

Example

curl
curl 'https://beta.materialgraph.com/api/v1/runs/{runId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/runs/{runId}/schedulemg:read

Export a composed scheme as a finish schedule

Projects one composed scheme from a completed compose Run into the shape a specifier issues: ONE ROW PER SLOT, each carrying the specified material's identity plus a fixed set of attribute cells. Every cell states its own verification: `value` with a `state`, `confidence`, and `sourceBasis`. The column set is IDENTICAL on every row — a material with no accepted value for a column exports a `missing` cell with a null value, never a blank that could be mistaken for a completed schedule. A `proposed` (pending-review) value is projected to `missing`, indistinguishable from a value that never existed. Cells the Run's criteria did not evaluate are backfilled from the catalogue and read `matched`/`missing` only: they assert presence-with-trust, not that the brief was met. Every row carries the reference a practice actually reads a schedule by — an L-code such as `L-101` under a numbered section such as `100 Floor` — allocated from the Run's PROGRAMME rather than its picks, so recomposing, varying, or accepting a substitution renumbers nothing. Rows are returned in READING order (space, then section, then line), not pick order. Slots holding a code with nothing specified against them are served separately as `openSlots`, because a schedule that omitted them would read as complete. A row may carry or-equal `alternates`, each stating what MaterialGraph claims about it and the basis for the claim; read them with `alternatesResolved`, since an empty list means either that nothing was looked up or that nothing was found. `?schemeId=` picks the scheme (default: the first). A Run that produced no composed schemes is a `400` problem, never an empty schedule. Requires the `mg:read` bearer scope, as the Run's owner.

Parameters

runId
stringpathThe public Run identifier returned by `POST /api/v1/runs`.
schemeId
stringqueryWhich of the Run's composed schemes to export. Defaults to the first scheme.

Response

200The finish schedule for one composed scheme.
scheduleFinishSchedule
runIdstring
schemeIdstring
titlestring
composeVersionstring
generatedAtstring<date-time>
columnsFinishScheduleColumn[]
sectionsFinishScheduleSection[]

The section table a renderer groups by, in schedule order. A section may carry no rows: three are reserved so their conventional numbers are held rather than reused, and any section can simply be unspecified in this scheme.

rowsFinishScheduleRow[]

In READING order — space (in programme order), then section, then line. Not pick order.

openSlotsFinishScheduleOpenSlot[]

Slots holding a code with nothing specified against them yet. Served explicitly because a schedule that simply omitted them would read as complete.

alternatesResolvedboolean

Whether the or-equal lookup RAN for this schedule. When false, no row's empty `alternates` may be read as `no alternate exists`.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/runs/{runId}/schedule' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/runs/{runId}/schedule.xlsxmg:read

Download a composed scheme's finish schedule (.xlsx)

The same projection as `GET /api/v1/runs/{runId}/schedule`, compiled to a single-sheet Excel workbook and streamed as an attachment. Columns: Code, Section, Line, Space, Element, Slot, Brand, Product, Variant, Anchored, Grade, one column per schedule attribute, Record URL, Notes, Or-equal basis. The honesty rule survives the flattening: an unknown attribute prints the literal `unknown` and a value that has not cleared its trust bar prints with an `(unverified)` suffix — no cell is ever blank. Every line declares its own kind in the `Line` column — `specified`, `or-equal`, `or-equal · unverified`, `declared successor`, or `open` — and an alternate repeats its parent's code, section and slot, so a reader who sorts or filters the sheet can never leave an alternate standing alone looking like a specification. An alternate's attribute cells read `not compared`, because the schedule's attribute set was never evaluated for it — only its equivalence signature was. Bytes are built fresh per request, so the response is `Cache-Control: no-store`. Requires the `mg:read` bearer scope, as the Run's owner.

Parameters

runId
stringpathThe public Run identifier returned by `POST /api/v1/runs`.
schemeId
stringqueryWhich of the Run's composed schemes to export. Defaults to the first scheme.

Response

200The finish-schedule workbook.
string<binary>

Example

curl
curl 'https://beta.materialgraph.com/api/v1/runs/{runId}/schedule.xlsx' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/runs/{runId}/eventsmg:read

Stream ordered Run events

Streams caller-owned, redacted Run events as server-sent events (SSE). Each frame uses the Run event sequence as `id`, the event type as `event`, and the serialized public Run event as `data`. Resume after the last observed sequence with the `after` query parameter or `Last-Event-ID` header; `after` takes precedence when both are supplied. The server polls for new events about once per second and returns at most 100 events per read. A terminal `succeeded`, `failed`, or `canceled` event closes the stream immediately. Non-terminal streams close after at most 25 seconds, so clients must reconnect with their cursor. The route emits no SSE heartbeat/comment frames or `retry` directive; the bounded connection lifetime is its reconnect and liveness boundary. Requires `mg:read`.

Parameters

runId
stringpathThe public Run identifier returned by `POST /api/v1/runs`.
after
integerqueryStream events whose sequence is greater than this non-negative cursor. Takes precedence over `Last-Event-ID` when both are supplied.
Last-Event-ID
integerheaderStandard SSE reconnect cursor. Stream events whose sequence is greater than this non-negative integer when `after` is absent.

Response

200An SSE stream of ordered public Run events. Reconnect after the bounded stream closes unless a terminal event was received.
string

SSE frames encoded as `id: <sequence>`, `event: <type>`, and `data: <PublicRunEvent JSON>`, followed by a blank line.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/runs/{runId}/events' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/runs/{runId}/resumemg:readmg:compose or mg:write

Resume a suspended Run

Transactionally moves a caller-owned suspended Run to claimed and records an encrypted resume command. Execution continues asynchronously through Inngest. Requires either the `mg:compose` or the `mg:write` scope.

Parameters

runId
stringpathThe public Run identifier returned by `POST /api/v1/runs`.

Request body

resumeDataunknown
stepIdstring

Optional. The suspension step the client believes is live. When supplied and it no longer matches the run's current suspension step, the resume is rejected with a 409 conflict so a stale answer cannot resume the wrong step. Omit to resume the current step unconditionally.

Response

202The claimed Run.
runPublicRun
idstring
capabilityKindenum
agenttoolworkflow
capabilityIdstring
clientReferencestring| null
statusenum
queuedclaimedrunningsuspendedsucceededfailed+1 more
redactedInputJsonobject
redactedOutputJsonobject| null
usageJsonobject| null
artifactRefsobject[]| null
failureJsonPublicRunFailure| null
suspensionPayloadobject| null

The current typed question payload while `status` is `suspended`; null otherwise. The same exact payload is retained on the `suspended` ledger event.

suspensionStepIdstring| null

The step awaiting input while `status` is `suspended` — pass it back as `stepId` when resuming, so a stale client cannot resume a step the run has already moved past. Read `suspensionPayload` for the current typed question. The same exact payload is retained on the `suspended` ledger event for event-stream consumers.

eventCursorinteger
attemptsinteger
startedAtstring<date-time>| null
cancellationRequestedAtstring<date-time>| null
completedAtstring<date-time>| null
createdAtstring<date-time>
updatedAtstring<date-time>

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/runs/{runId}/resume' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "resumeData": "…"}'
POST/api/v1/runs/{runId}/cancelmg:readmg:compose or mg:write

Request durable Run cancellation

For an active tool Run, records an encrypted cancellation command for the associated Inngest execution; cancellation completes asynchronously with a guarded state transition. Active agent and workflow Runs are not currently cancellable because their child executions cannot be correlated safely across processes, so they return `409 Conflict` without recording a cancellation command. Terminal Runs of any capability kind are returned unchanged with `202 Accepted` because no cancellation is required. Requires either the `mg:compose` or the `mg:write` scope.

Parameters

runId
stringpathThe public Run identifier returned by `POST /api/v1/runs`.

Response

202The Run after accepting the cancellation request.
runPublicRun
idstring
capabilityKindenum
agenttoolworkflow
capabilityIdstring
clientReferencestring| null
statusenum
queuedclaimedrunningsuspendedsucceededfailed+1 more
redactedInputJsonobject
redactedOutputJsonobject| null
usageJsonobject| null
artifactRefsobject[]| null
failureJsonPublicRunFailure| null
suspensionPayloadobject| null

The current typed question payload while `status` is `suspended`; null otherwise. The same exact payload is retained on the `suspended` ledger event.

suspensionStepIdstring| null

The step awaiting input while `status` is `suspended` — pass it back as `stepId` when resuming, so a stale client cannot resume a step the run has already moved past. Read `suspensionPayload` for the current typed question. The same exact payload is retained on the `suspended` ledger event for event-stream consumers.

eventCursorinteger
attemptsinteger
startedAtstring<date-time>| null
cancellationRequestedAtstring<date-time>| null
completedAtstring<date-time>| null
createdAtstring<date-time>
updatedAtstring<date-time>

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/runs/{runId}/cancel' \  -H "Authorization: Bearer $MG_API_KEY"

Projects

POST/api/v1/projectsmg:readmg:compose

Create a project

Creates a persistent Material Box project owned by the authenticated principal. An optional `name` defaults to "Untitled project". The response is the freshly assembled project state (an empty event log). Requires the `mg:compose` bearer scope (designer working data — a `compose` key suffices; broader ingest/write/admin keys also carry it).

Request body

namestring

Response

201The created project's assembled state.
projectIdstring
principalIdstring
namestring
createdAtstring<date-time>
updatedAtstring<date-time>
batchIdsstring[]

Attached source-material batch ids, first-seen order.

runIdsstring[]

Started capability Run ids, first-seen order.

answersobject[]
questionstring
answerstring
preferencesobject[]
leftIdstring
rightIdstring
chosenIdstring

Equals `leftId` or `rightId`.

chosenReferencesstring[]
adoptedSchemesstring[]
eventsProjectEvent[]

The full ordered event trail — the receipts.

eventIdstring

The stored event id (`pje_…`).

kindenum
batch-attachedrun-startedclarifying-answerpairwise-preferencereference-chosenscheme-adopted
payloadobject

Validated to the shape of `kind`.

createdAtstring<date-time>

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/projects' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "name": "…"}'
GET/api/v1/projectsmg:read

List your projects

Lists the authenticated principal's own projects, newest first, capped at 50. Summaries only — no event fold. Requires the `mg:read` bearer scope.

Response

200The caller's projects, newest first.
projectsProjectSummary[]
projectIdstring

The stored project id (`proj_…`).

principalIdstring
namestring
createdAtstring<date-time>
updatedAtstring<date-time>

Last-activity timestamp; bumped on each appended event.

Example

curl
curl 'https://beta.materialgraph.com/api/v1/projects' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/projects/{projectId}mg:read

Get a project's assembled state

Returns the caller-owned project's read model — its identity plus the collections folded from its append-only event log (attached batches, started Runs, clarifying answers, pairwise preferences, chosen references, adopted schemes) and the full ordered event trail. A project owned by another principal is indistinguishable from a missing one (404). Requires `mg:read`.

Parameters

projectId
stringpathThe project identifier returned by `POST /api/v1/projects` (`proj_…`).

Response

200The assembled project state.
projectIdstring
principalIdstring
namestring
createdAtstring<date-time>
updatedAtstring<date-time>
batchIdsstring[]

Attached source-material batch ids, first-seen order.

runIdsstring[]

Started capability Run ids, first-seen order.

answersobject[]
questionstring
answerstring
preferencesobject[]
leftIdstring
rightIdstring
chosenIdstring

Equals `leftId` or `rightId`.

chosenReferencesstring[]
adoptedSchemesstring[]
eventsProjectEvent[]

The full ordered event trail — the receipts.

eventIdstring

The stored event id (`pje_…`).

kindenum
batch-attachedrun-startedclarifying-answerpairwise-preferencereference-chosenscheme-adopted
payloadobject

Validated to the shape of `kind`.

createdAtstring<date-time>

Example

curl
curl 'https://beta.materialgraph.com/api/v1/projects/{projectId}' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/projects/{projectId}/eventsmg:readmg:compose

Append a project event

Appends one immutable event to the caller-owned project's process trail. The body is `{ kind, payload }`; `kind` must be one of `batch-attached`, `run-started`, `clarifying-answer`, `pairwise-preference`, `reference-chosen`, or `scheme-adopted`, and `payload` must satisfy that kind's schema (else 400 with per-issue detail). Appending against a project owned by another principal returns 404. Requires `mg:compose` (designer working data; broader ingest/write/admin keys also carry it).

Parameters

projectId
stringpathThe project identifier returned by `POST /api/v1/projects` (`proj_…`).

Request body

kindenum
batch-attachedrun-startedclarifying-answerpairwise-preferencereference-chosenscheme-adopted
payloadobject

The event payload; validated against the schema for `kind`.

Response

201The appended event receipt.
eventIdstring
projectIdstring
kindenum
batch-attachedrun-startedclarifying-answerpairwise-preferencereference-chosenscheme-adopted
createdAtstring<date-time>

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/projects/{projectId}/events' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "kind": "batch-attached",  "payload": {}}'

Schedules

GET/api/v1/schedulesmg:read

List the caller's Project-owned Schedules

Returns one bounded page of the authenticated principal's Schedules, ordered by `updatedAt` descending with opaque Schedule id tie-breaking. Each row carries only durable Schedule identity/status, exact supported content counts (`slots`, `specified`, `revisions`), the independently counted targeted Runs total, and the current active Run when one exists. A draft has exact zero content counts but may still have Runs. Unsupported counts are omitted rather than reported as zero. Run identity is provenance, never Schedule identity. Pass `nextCursor` back verbatim; a page is not the whole corpus. Requires the `mg:read` bearer scope.

Parameters

cursor
stringqueryOpaque cursor issued for this exact authenticated principal and environment. Pass it back verbatim.

Response

200One bounded Schedule page; an empty page is successful.
itemsobject[]
scheduleIdstring
projectIdstring
projectNamestring
namestring
statusenum
draftprogrammedcomposed
updatedAtstring<date-time>
countsobject
activeRunobject| null
nextCursorstring| null

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schedules' \  -H "Authorization: Bearer $MG_API_KEY"
POST/api/v1/schedulesmg:read

Create a Project-owned Schedule

Creates a persistent Schedule under one caller-owned Project. `requestId` is required and client-minted: the first canonical request returns 201, an exact replay returns 200 with the same Schedule, and reuse with a different name returns 409. A missing Project and one owned by another principal return the same 404. Requires the `mg:read` and `mg:compose` bearer scopes.

Request body

projectIdstring
namestring
requestIdstring

Response

200An exact replay of the existing Schedule.
scheduleIdstring
projectIdstring
namestring
environmentenum
developmentpreviewproductiontest
statusenum
draftprogrammedcomposed
revisionobject| null
revisionIdstring
revisionNumberinteger
sourceRunIdstring| null
sourceSchemeIdstring| null
contentHashstring
contentHashVersioninteger
adoptedAtstring<date-time>
adoptedByPrincipalIdstring
supersededAtstring<date-time>| null
supersededByRevisionNumberinteger| null
programmeobject| null
summarystring
contextstring| null
spacesobject[]
slotsobject[]
paletteobject[]
paletteDirectionsobject[]
paletteProvenanceenum
statedinferred
unresolvedLanguagestring[]
sustainabilityNotesstring[]
performanceNotesstring[]
sourceRefsobject[]
schemeobject| null
schemeIdstring
titlestring
picksobject[]
objectiveobject
decisionsobject[]
slotIdstring
variantIdstring
roleenum
basis-of-designcomparablesubstitution
clientenum
yesopendeclinednone
decidedAtstring<date-time>| null
decidedByPrincipalIdstring| null
notestring| null
comparablesobject[]
slotIdstring
entriesobject[]
createdAtstring<date-time>
updatedAtstring<date-time>
countsobject
slotsinteger
specifiedinteger
requiredinteger
fromCodeinteger| null
notCoveredinteger| null
preferredinteger
revisionsinteger
runsinteger
activeRunobject| null
runIdstring
statusenum
queuedclaimedrunningsuspended
startedAtstring<date-time>| null
suspensionStepIdstring| null
latestCompositionobject| null
runIdstring
statusenum
succeededfailedcanceled
createdAtstring<date-time>
completedAtstring<date-time>| null
composedRunobject| null
runIdstring
createdAtstring<date-time>
completedAtstring<date-time>| null
schemesobject[]
shortfallobject| null
warningsobject
runsobject
itemsobject[]
totalinteger
truncatedboolean
nextCursorstring| null

Example

curl
curl -X POST 'https://beta.materialgraph.com/api/v1/schedules' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "projectId": "…",  "name": "…",  "requestId": "…"}'
GET/api/v1/schedules/{scheduleId}mg:read

Get a Schedule's accepted state

Reads one persistent, Project-owned Schedule under the authenticated principal. The response is the current accepted revision, independent of whether a Run is active: a zero-revision draft is still a successful 200 with `revision`, `programme`, and `scheme` null, empty `decisions` and `comparables`, and zero Schedule-content counts (`slots`, `specified`, `required`, `preferred`, and `revisions`). Run provenance is independent: `activeRun` may be present and `counts.runs` may be nonzero even while the Schedule is a draft. `decisions` and `comparables` are plain top-level revision content; an absent comparable slot means no lookup was recorded, while a present slot with `entries: []` means a lookup ran and found none. Only `runs` is paginated: pass its opaque `nextCursor` back as `cursor`, without recomputing or sorting revision content. A missing Schedule and one owned by another principal return the same 404. Requires the `mg:read` bearer scope.

Parameters

scheduleId
stringpathThe opaque Schedule id. Preserve and return it verbatim; it has no public structure.
cursor
stringqueryOpaque cursor from `runs.nextCursor`. It is valid only for the same authenticated Schedule query that issued it. Pages only Run provenance; the accepted revision content does not change shape.

Response

200The current accepted Schedule state and one bounded page of its Runs.
scheduleIdstring
projectIdstring
namestring
environmentenum
developmentpreviewproductiontest
statusenum
draftprogrammedcomposed
revisionobject| null
revisionIdstring
revisionNumberinteger
sourceRunIdstring| null
sourceSchemeIdstring| null
contentHashstring
contentHashVersioninteger
adoptedAtstring<date-time>
adoptedByPrincipalIdstring
supersededAtstring<date-time>| null
supersededByRevisionNumberinteger| null
programmeobject| null
summarystring
contextstring| null
spacesobject[]
slotsobject[]
paletteobject[]
paletteDirectionsobject[]
paletteProvenanceenum
statedinferred
unresolvedLanguagestring[]
sustainabilityNotesstring[]
performanceNotesstring[]
sourceRefsobject[]
schemeobject| null
schemeIdstring
titlestring
picksobject[]
objectiveobject
decisionsobject[]
slotIdstring
variantIdstring
roleenum
basis-of-designcomparablesubstitution
clientenum
yesopendeclinednone
decidedAtstring<date-time>| null
decidedByPrincipalIdstring| null
notestring| null
comparablesobject[]
slotIdstring
entriesobject[]
createdAtstring<date-time>
updatedAtstring<date-time>
countsobject
slotsinteger
specifiedinteger
requiredinteger
fromCodeinteger| null
notCoveredinteger| null
preferredinteger
revisionsinteger
runsinteger
activeRunobject| null
runIdstring
statusenum
queuedclaimedrunningsuspended
startedAtstring<date-time>| null
suspensionStepIdstring| null
latestCompositionobject| null
runIdstring
statusenum
succeededfailedcanceled
createdAtstring<date-time>
completedAtstring<date-time>| null
composedRunobject| null
runIdstring
createdAtstring<date-time>
completedAtstring<date-time>| null
schemesobject[]
shortfallobject| null
warningsobject
runsobject
itemsobject[]
totalinteger
truncatedboolean
nextCursorstring| null

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schedules/{scheduleId}' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/schedules/{scheduleId}/referencesmg:read

Read saved Schedule reference preferences

Latest saved event per reference, including exact source Run and research frame provenance. Works before the first Programme or document revision. An empty items list means no decisions yet. Requires mg:read. Responses are private and never cached.

Parameters

scheduleId
stringpath

Response

200Current saved preferences.
itemsobject[]
eventIdstring
scheduleIdstring
referenceIdstring
versioninteger
requestIdstring
sourceRunIdstring
sourceEventSequenceinteger
scopeobject
sourceoneOf
kind: imageobject
kind: webobject
originalVerdictenum
keptset-aside
influenceenum
keptdiminished
decidedByPrincipalIdstring
decidedAtstring<date-time>

Example

curl
curl 'https://beta.materialgraph.com/api/v1/schedules/{scheduleId}/references' \  -H "Authorization: Bearer $MG_API_KEY"
PUT/api/v1/schedules/{scheduleId}/references/{referenceId}mg:read

Keep or diminish a Schedule research reference

Appends a preference without changing the document revision or starting provider work. Source, scope, actor, time and research provenance are server-derived. Use zero expectedFeedbackVersion for the first decision; subsequent decisions compare against the version last read. An exact retry returns its original immutable receipt without replacing newer preferences. Reusing requestId with different input or a stale reference version returns 409. Requires mg:read and mg:compose.

Parameters

scheduleId
stringpath
referenceId
stringpath

Request body

requestIdstring
expectedFeedbackVersioninteger
sourceRunIdstring
influenceenum
keptdiminished

Response

200Saved decision or original receipt for an exact replay.
eventIdstring
scheduleIdstring
referenceIdstring
versioninteger
requestIdstring
sourceRunIdstring
sourceEventSequenceinteger
scopeobject
kind"project"
sourceoneOf
kind: imageobject
kind"image"
laneIdstring
imageRefstring
sourceNamestring
sourceDomainstring
urlstring| null
captionstring
kind: webobject
kind"web"
domainstring
urlstring
statementstring
implicationstring
originalVerdictenum
keptset-aside
influenceenum
keptdiminished
decidedByPrincipalIdstring
decidedAtstring<date-time>

Example

curl
curl -X PUT 'https://beta.materialgraph.com/api/v1/schedules/{scheduleId}/references/{referenceId}' \  -H "Authorization: Bearer $MG_API_KEY" \  -H "Content-Type: application/json" \  -d '{  "requestId": "…",  "expectedFeedbackVersion": 0,  "sourceRunId": "…",  "influence": "kept"}'

Meta

GET/api/v1/whoamimg:read

Echo the authenticated principal and scopes

A sanity-check endpoint: fire it with your key to confirm the credential is accepted and see which principal and scopes the platform resolved it to, before spending a real query. Echoes the auth context verbatim — no database. Identity is per-request, so responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Response

200The resolved principal and scopes.
principalstring

The resolved principal for the credential (e.g. `apikey:read`).

scopesstring[]

The scopes granted to this credential (e.g. `mg:read`).

Example

curl
curl 'https://beta.materialgraph.com/api/v1/whoami' \  -H "Authorization: Bearer $MG_API_KEY"
GET/api/v1/keys/usagemg:read

Get this key's usage and spend

What the presented credential has spent, and on what. The path carries no key id and never will: a key reads its own usage and no other's. Reads a rollup at (day × operation class) grain — there are no raw per-call rows anywhere, so the answer is bounded and cheap, and a class the key never used produces no line rather than a zero one. Costs are carried in `costMicroDollars` (millionths of a dollar), which is the exact stored unit; cents would round a single search down to zero. Budgets are stated in cents because that is the unit an operator sets. Enforcement is two-tier and every `byOperation` entry says which tier it got and why. `hard` classes (`search_standard`, `search_pro`, `embed_image`, `embed_text`, `materials_read`, `revit_export`) are priced before the work runs, so an over-budget call is refused with 402 `budget_exceeded` and never executes. `soft` classes (`run_low`, `run_medium`, `run_high`, `live_verify`) have a true cost only once the work finishes, so they run and are booked afterwards — a key at its cap can exceed it by at most one such operation, and `budget.overshootPossible` says whether that exposure is live. Rates are placeholders and are not prices anyone has agreed. Spend is a live counter, so responses carry `Cache-Control: no-store`. Requires the `mg:read` bearer scope.

Parameters

from
string<date>queryInclusive first day of the window, `YYYY-MM-DD`. Defaults to the start of the current billing period.
to
string<date>queryEXCLUSIVE last day of the window, `YYYY-MM-DD`. Defaults to tomorrow, so today's partial usage is included. At most 62 days per call.

Response

200The key's usage rollup and the budget it is measured against.
fromstring

Inclusive first day of the window.

tostring

Exclusive last day of the window.

dailyobject[]

Every non-empty (day, operation class) cell, day then class order.

usageDatestring

The UTC day the operations were charged on, `YYYY-MM-DD`.

operationstring

The operation class, e.g. `search_standard`.

quantityinteger

Whole operations charged that day.

costMicroDollarsinteger

Cost in millionths of a dollar — the exact stored unit. Cents would round a single search to zero.

costDollarsnumber

The same figure in dollars, to six decimal places.

byOperationobject[]

The same money folded by class.

operationstring
quantityinteger
costMicroDollarsinteger
costDollarsnumber
enforcementenum

`hard` — the budget was a real pre-check and an over-budget call of this class was refused before it ran. `soft` — the true cost existed only after the work completed, so the call ran and was booked afterwards.

hardsoft
enforcementReasonstring

Why this class gets that tier. Stated, never implied.

totalCostMicroDollarsinteger
totalCostDollarsnumber
budgetobject

The cap this usage is measured against, and exactly how strongly it is enforced.

budgetCentsinteger| null

This key's cap for one billing period, in whole cents. `null` = unbudgeted.

periodStartstring

First day of the current billing period, `YYYY-MM-DD`.

spentCentsnumber| null

Spend booked in the current period, in cents. Fractional on purpose — half a cent is half a cent.

remainingCentsnumber| null

Cap minus spend. Negative after a soft-tier overshoot. `null` when unbudgeted.

softTierClassesstring[]

Operation classes whose cost is knowable only after the work runs. These are booked post-hoc and can take the key past its cap by at most one operation each.

overshootPossibleboolean

True while a budgeted key still has allowance exposed to that post-hoc overshoot. False when the key is unbudgeted (nothing to overshoot).

Example

curl
curl 'https://beta.materialgraph.com/api/v1/keys/usage' \  -H "Authorization: Bearer $MG_API_KEY"

Integration

Built to slot into your stack.

Every surface above maps onto a pattern production platforms already run.

Service auth

Internal services on most platforms authenticate to each other with a static shared Bearer key per service, compared in constant time.

MaterialGraph keys ride the same header and wire pattern — scoped and rotatable. Strictly stronger; zero new middleware.

Client generation

Platform teams auto-generate typed clients from API specs with openapi-generator and publish them to an internal registry.

MaterialGraph's spec ships valid JSON, a bearer scheme, and snake_case operationIds: a client is one more entry in that pipeline's config.

The entitlement seam

Storefronts already consume external search and catalog providers and apply entitlement themselves: per-user rule filters, anonymous traffic scoped to public data, graceful degradation on failure.

MaterialGraph returns candidates; your rules decide visibility. Same seam, second provider.

Wiring

A new service comes online the same way each time: a generated client package, a service-URL env var, one instantiation behind the existing auth and request middleware chain.

Adding MaterialGraph is exactly that shape — one generated package, one env var, one instantiation. No new request path, no new trust boundary.

Token endgame

User auth on modern platforms validates RS256 JWTs via JWKS.

The trajectory above terminates in exactly that verification path: an issuer and a JWKS URL, validated with machinery you already trust.

Transports

One primitive, four transports.

The same query and retrieval primitive, reachable however the caller reads — a REST client, an MCP-native agent, another party’s agent, or a shell pipe.

REST

/api/v1

The query primitive over plain, versioned REST — OpenAPI 3.1, one scope.

MCP

/mcp

Live stateless Streamable HTTP — claude mcp add --transport http materialgraph https://beta.materialgraph.com/mcp --header "Authorization: Bearer mg_…". 49 read tools.

A2A

/.well-known/agent-card.json

Other parties' agents discover the graph, with live capability counts.

CLI

mg

JSON out, NDJSON in pipes, schema introspection — everything, scriptable.

MCP

Every tool, listed from the registry.

47 read-only tools, generated from the same registry the wire serves — a tool added or renamed without regenerating this table fails the build. Typed tools publish an output schema.

browse-materials

Walk a category's materials ORDERED BY COLOUR, with filters and a cursor — browsing, not searching.

colour-coverage

typed

How much of the corpus has measured colour at all.

coordinate-materials

Given several materials already chosen, what completes the scheme.

describe-brand-revit-library

A whole brand's Revit library — every variant that ships a specification package, as one importable set.

describe-revit-package

typed

Describe the Revit specification package for one material variant.

explain-term

typed

What one attribute id (`primary_material`) or value token (`bfl_s1`) MEANS in the schema.

extract-palette

Read the palette OUT of an image — the colours actually present, with their proportions.

find-by-colour

typed

Find materials whose COLOUR is closest to a target colour, across every brand at once.

find-by-spec

typed

Find material variants matching a structured specification, across ALL brands at once.

find-related-materials

Start from ONE material you already have and get the materials related to it.

generate-palette

typed

Build a colour palette from a seed colour plus a harmony, or from a phrase ('coastal, weathered').

get-brand

Use this tool when you need the dossier for a specific brand: portfolio summary, posture, and recommendations.

get-material-demand

What is actually being SPECIFIED — the demand signal for one material or several compared.

get-material-record

The EVIDENCE behind a material: each value with its source, review state and freshness.

get-review

Use this tool when you need full detail on a specific review task, including source items and the associated product record context.

get-schema

Use this tool when you need to understand the MaterialGraph schema structure.

get-variant

Use this tool for the identity and coverage HEADLINE of a single product variant: brand, category, product context, and a per-block coverage summary with counts of images, downloads, and gaps.

get-variant-assets

Use this tool when you need the full list of images and downloadable files for a product variant.

get-variant-blocks

Use this tool when you need to understand the data completeness of a product variant, broken down by canonical block (identity, material_composition, sustainability, etc.).

get-variants

typed

Use this tool to read MANY variants by id in ONE round trip — the batch counterpart to get-variant.

get-visual-cluster

One visual cluster in full — its members, exemplars and diagnostics.

list-attribute-values

Every value one attribute actually holds across the corpus, with how many materials hold each.

list-brands

Use this tool when you need to see all brands in the MaterialGraph corpus with variant counts and category breakdowns.

list-categories

typed

The complete product-category tree — every group and product type, with keys and paths.

list-category-colours

typed

What colours a CATEGORY comes in — the profile of up to eight product types, ranked.

list-category-specs

typed

Which specifications a category's materials can carry.

list-code-contexts

typed

Discover the vocabulary for advisory building-code questions: which jurisdictions are seeded (and how thoroughly — `thorough` means that authority's own table was reviewed, `model_baseline` means unamended model IBC stood in), which IBC occupancy groups and finish locations the requirement registry keys on, the classification scales with their classes ordered BEST FIRST, and which room types correlate to which regulatory axes.

list-colour-facets

typed

Discover the colour vocabulary: the three canonical colour dictionaries — family, mood and undertone — each with its canonical term ids, aliases, and how many variants and brands carry every term.

list-declared-associations

What the MANUFACTURER states goes with a product: `finishes` — what can be applied to this chair; `products` — what this finish can be applied to.

list-reviews

Use this tool when you need to see outstanding review tasks in the MaterialGraph pipeline.

list-spec-facets

typed

Discover the specifications find-by-spec can query and what each admits: spec attributes (fire, material, colour, sustainability…) with data.

list-variants

Use this tool when you need to browse or search product variants in the MaterialGraph corpus.

list-visual-clusters

What the corpus LOOKS like in aggregate — materials grouped by visual likeness, each cluster with its exemplars.

match-material

Identify materials in a PHOTOGRAPH — room shot or sample close-up.

rank-by-colour-similarity

typed

Rank material variants you ALREADY HAVE by how close each one's colour is to a reference — 'of these, which is closest to X'.

resolve-attribute

A free-text property phrase (“double rubs”) → the canonical attribute ids it means, ranked by meaning, each flagged queryable-or-not.

resolve-attribute-value

A free-text value (“navy”) → the canonical term one attribute admits.

resolve-code-advisory

Evaluate materials against advisory building-code requirements for one space, or — with no variantIds — just report what that space requires.

resolve-colour

A colour reference a person used — a name, a standard code (`RAL 7048`), a brand colour, a hex — into a concrete colour the other colour tools accept.

resolve-phrases

Resolve a BATCH of your phrases to canonical vocabulary in one call, not a term at a time — search-vocabulary searches it, this resolves against it.

search-by-gradient

Materials carrying a colour TRANSITION — two to six ordered stops, where order is meaningful (an ombré weave, a graded stone).

search-by-palette

Materials that satisfy a whole PALETTE (up to eight colours, each weighted) rather than one colour.

search-products

Use this tool when you need to find product variants matching a text query.

search-vocabulary

typed

Search the controlled vocabularies (attributes + canonical term ids) by meaning or spelling — returns ranked candidates with scores.

suggest-colour-names

Autocomplete over the colour names the graph knows.

suggest-entities

Autocomplete over the graph's own entity NAMES — brands, products, collections.

whoami

typed

Which credential this session is authenticated as.

Outputs

Answers that leave as deliverables.

The graph’s answers don’t stop at JSON — a composed selection issues as a finish schedule, and a material record exports as a Revit specification package.

Finish schedule

GET /api/v1/runs/{runId}/schedule.xlsx

A run’s composed selection as a spreadsheet — the same projection the JSON schedule serves, in the format a project actually issues.

Revit specification package

GET /api/v1/materials/{variantId}/revit

Shared parameters, a type catalog, classification and a true-scale swatch — one material, ready to land in a model. A whole brand exports as a library.

Attribution

Join an order back to the search that produced it.

A list of SKUs cannot tell you which sample got ordered off which rail, or whether that was a colour answer, a look-alike answer or a text answer. Every ranked response carries an attribution receipt — a responseId (the same value as the X-Request-Id header), the ranking basis, and issuedAt — and every result carries its own resultId. Keep both and the question becomes a join in your own warehouse.

const response = await mg.search({
  colours: [{ hex: "9CAF88" }],
  limit: 12,
});

// One row per search. `basis` is what you group by later.
await db.rankings.insert({
  responseId: response.attribution.responseId,
  basis: response.attribution.basis, // "colour"
  issuedAt: response.attribution.issuedAt,
  query: "9CAF88",
});

// One row per thing the person then did with a result.
await db.sampleOrders.insert({
  orderId,
  variantId: chosen.variantId,
  materialBankId: chosen.materialBankId,
  resultId: chosen.resultId, // "<responseId>:<rank>"
  responseId: response.attribution.responseId,
});

basis is one vocabulary across both ranked routes: image, colour, text, lexical, entity-browse from /search (where it equals that response’s mode), and match from /search/match. Group on it directly.

Both ids are opaque — store them verbatim, never parse them. A resultId is unique across pages, because each page is its own response.

We keep no ledger of these ids. What you store is the record, so write the receipt down at the moment you show the results.

Honesty

Errors written for machines that retry.

Every error is an RFC 7807 problem document, served as application/problem+json with a matching X-Request-Id. The /api/v1 prefix is stable — changes within v1 are additive only — and the OpenAPI document is versioned 0.7.0 (pre-1.0), so treat unknown fields as forward-compatible.

401 problem+json
{
  "type": "https://beta.materialgraph.com/errors/unauthorized",
  "title": "Unauthorized",
  "status": 401,
  "detail": "No authentication credentials provided",
  "is_retriable": false,
  "trace_id": "3f9c2a1e-8b7d-4e2a-9c1f-2d6b8a4e5c30",
  "suggestions": [
    "Provide a valid API key via the Authorization header (Bearer <key>) or X-API-Key header.",
    "If using JWT authentication, ensure the token is not expired and was issued for the correct audience."
  ]
}

is_retriable and suggestions are written for agents — a machine can decide whether to retry and what to try next without a human in the loop.

Rate limits are enforced live. Every response carries real X-RateLimit-* headers, and exceeding the budget returns a 429 with Retry-After. Today the budget is keyed per scope-tier — every mg:read key shares one 60-request-per-minute bucket; per-key limits land with self-serve issuance.

Error types — the RFC 7807 type values a caller can meet.

400bad-requestBad requestdo not retry
404not-foundResource not founddo not retry
404material-not-foundMaterial not founddo not retry
429rate-limitedRate limit exceededretriable
503service-unavailableService unavailableretriable
500internal-errorInternal server errorretriable