Data Studio Developer Portal

Consumer reference

This is the detail behind Getting started with the Data Studio. Start there: it covers which host to use, connecting over MCP, the lean surface and the consumer lane, finding skills, your first property calls, prices, retries, reading the answer, and the error codes. This page picks up where it stops.

It is written for anyone holding a consumer key. An author key can consume too, and everything here applies to it; if you will create or update skills, Authoring skills is the companion.


§2 Who sees which skills: the three gates

Every key’s catalog is filtered through three gates, in order:

  1. Publish state. Only skills with status='published' reach MCP. Drafts, skills in review and archived skills stay invisible. If a skill you expect is missing, the most common cause is that it is not published yet.
  2. Skill category. Internal platform skills (category='system', such as tag validators and skill builders) are never served to external keys, whatever their status or your tier. You cannot unlock this boundary.
  3. Per-key access. If your key has roles, the roles alone decide: each role’s tag_allow, minus its tag_deny, is matched against each skill’s tags. If your key has no roles, your tier decides, and a consumer-tier key sees only the skills marked mcp_visibility='consumer'.

Roles outrank the visibility column. A consumer-tier key with a role allowing Ontario-tagged skills sees those skills even where they are marked mcp_visibility='admin'.

Role decides data scope; tier decides capability shape. Your tier decides which auxiliary tools register at all (the read-side surface: discovery, dictionary and feedback tools). Your roles decide which skills you see among them.

Two colleagues can therefore see different catalogs from the same ecosystem. A change to your key’s roles can take up to 30 minutes to show in tools/list, because each key’s tool list is cached. tip-key-introspect describes your key as it is right now.


§5 The dictionary tools

The lean surface serves the dictionary tools listed in Getting started. Their detail:

portal-browse-dictionary: what terms are canonical in this ecosystem?

// tools/call portal-browse-dictionary
{ }

Returns the ecosystem’s harmonized vocabulary: terms, definitions, types, and which sources provide each term. Use it when you join data across sources (a provincial assessor, a city’s open data, a land registry) and need to know which fields canonically mean “owner name” rather than “registered owner” or “title holder”. Filter by geography or tags.

portal-dictionary-release: pinned versions, the latest pointer, and the naming standard

The dictionary is also published as versions you can pin (portal-dictionary-release, or GET /api/portal/<ecosystem>/dictionary/releases/<version>).

find-skills-for-concept: who touches this concept, and which way round?

Concept-anchored discovery. Pass a term from portal-browse-dictionary (either as a dictionary/property path or as the dotted termName that tool returns) and, optionally, relationType to pick the direction:

// "I have a PID: what can you get me with it?"
{ "dictionarySlug": "<the dictionary slug portal-browse-dictionary returns>", "propertySlug": "pid", "relationType": "requires" }

// "who can produce an AVM band for me?"
{ "concept": "Custom/avm-estimate-min", "relationType": "outputs" }

Both directions are read from the live catalog at call time: a skill that gains an input or a field mapping shows up on its next publish. A parameter the ecosystem dictionary has no term for produces no edge at all, so an empty result means “nothing declares this concept”, never “someone forgot to annotate it”.

query, category and domain narrow and rank results within the concept match. They don’t replace it: the concept filter is required.

On the full surface only

portal-browse-skills (catalog browsing with category, q, tier and jurisdiction filters) and portal-browse-catalog (the data sources behind the skills) register only on the full surface, without surface=lean. portal-browse-catalog’s geography narrows by any of three levels: a country (US), a province or state (CA-BC), or a municipality or county slug from availableMunicipalities (vancouver, pierce-county). A municipality filter returns the sources about that place; a province-wide source does not match a city inside it. The REST twin, GET /api/portal/<ecosystem>/catalog?geography=, takes the same values and lists them as municipalities.


§5b Coverage: which of this source’s fields carry a dictionary term?

portal-browse-dictionary tells you what a term means. Coverage tells you which published sources actually carry it, and, the question you will have the moment you hold a real envelope, what each of a source’s other fields is instead.

// tools/call portal-dictionary-coverage
{ "slug": "bc-address-geocoder" }

REST, if you would rather not go through MCP:

# British Columbia: one source, field by field
curl -s "https://api.dev.trustinfrastructure.com/api/portal/real-estate/dictionary/coverage/bc-address-geocoder" \
  -H "Authorization: Bearer $TIP_API_KEY"

# Vancouver: same shape
curl -s "https://api.dev.trustinfrastructure.com/api/portal/real-estate/dictionary/coverage/vancouver-property-tax-report" \
  -H "Authorization: Bearer $TIP_API_KEY"

Drop the slug for a page of every published source with its counts only (?cursor= and ?limit=, up to 200; the MCP tool pages at 50).

If you call skills rather than sources, ask about the skill. Sources and skills have separate slug spaces, so a skill slug passed as a source is simply not found:

curl -s "https://api.dev.trustinfrastructure.com/api/portal/real-estate/dictionary/coverage/skills/british-columbia-property" \
  -H "Authorization: Bearer $TIP_API_KEY"
// tools/call portal-dictionary-coverage
{ "skill": "vancouver-property" }

You get every data source the skill’s orchestration dispatches, each in exactly the shape above, plus an aggregate roll-up per direction. Every response states which kind of thing it describes in kind ("source" or "skill"), so the two can never be confused.

Two things the skill response is careful about. A source the skill names that the ecosystem does not publish is listed under unpublishedSources and left out of the aggregate: it has no declared fields to count, and counting it would read as a coverage gap rather than as the publishing gap it is. And a published skill whose orchestration names no source at all answers namesNoSources: true rather than 404. It exists, and “nothing to measure here” is the true answer, usually because it is a computation skill or reaches its data through skills it dispatches, each of which has its own coverage under its own slug.

Each field arrives in exactly one of four states, and the states are the contract:

statewhat it meansalign to it?
termlinked to a dictionary term; carries the term name and its envelope path, e.g. Valuation.AssessedTotalValueyes
unmapped_by_designsomebody ruled that no term applies; carries the class saying why: system_column, geometry, transport_param, open_data_column, duplicate_of_linked, deprecated_upstream, containerno
opennobody has ruled on it yet: the absence of a decision, not a decisionno
term_unresolvedlinked to a dictionary entry that serves no envelope path today (superseded, retired, or awaiting a section)no

If you join on a term’s sources[], page it

The release overlay (?include=coverage) gives each term a sources[]: the sources that can feed it. That array is a page, not the whole list. sourceCount is the whole:

sourceCount = every source naming this term
sources[]   = one page of them, ordered by slug, default 50, max 200

Both lanes carry this overlay, and each names its own parameters. They are not the same words, so check which lane you are on:

RESTMCP (portal-dictionary-release)
ask for the overlay?include=coverageincludeCoverage: true
next page&cursor=<nextCursor>sourceCursor: "<nextCursor>"
page size&limit= (max 200)sourceLimit: (max 200)

The REST route also accepts the MCP spellings as aliases (includeCoverage, sourceCursor, sourceLimit), so a call written from an MCP response works here unchanged. The REST names above stay canonical.

curl -s "https://api.dev.trustinfrastructure.com/api/portal/real-estate/dictionary/releases/live?include=coverage&terms=Address.UnparsedAddress&cursor=carberry-property-assessments" \
  -H "Authorization: Bearer $TIP_API_KEY"

Page until sourcePage.nextCursor is null. A term with more sources than one page is not a term missing sources: before paging existed, Address.UnparsedAddress named 339 sources and showed the alphabetically-first 50, and a consumer joining on the array concluded that vancouver-issued-building-permits did not populate a term it genuinely populates.

Two ways to get the answer, and they answer different questions. Page sources[] when you want which sources can feed this term. Read the coverage export per source (GET …/dictionary/coverage/<slug>) when you want what will arrive under which term from this source: it is keyed per declared field with exactly one winner, so it is the one a cache should follow.

Every provider entry says how it reaches the term and whether its link still resolves. direction is output (the source can return the value), input (it accepts the term as a parameter) or both. resolved: true means at least one of that source’s links to the term points at a declaration that direction actually has: an output link at a declared field, an input link at an input_schema parameter. resolved: false means none do. declaresNoFields: true is the third state: the source describes nothing of that kind at all, which is an absence of evidence rather than a broken link. The same keys appear on portal-browse-dictionary’s providers[].

Read direction before you act on a provider: a source that accepts a term as an input is not a source that can give you its value.

linked is the whole linked population; termUnresolved is the part of it you cannot use. Both counts ride every per-direction block, on the page and on the per-field response:

linked = (fields carrying a usable term) + termUnresolved

Subtract to get the number you can actually align to.

A non-harmonized field is never served with a candidate term. The class is the answer, because it says why no term applies. A best guess would read as a promise the harmonizer never made.

Two things these numbers do not say. They describe what is harmonized, not what is populated: a field linked to a term arrives under that term when the source returns it, and whether the source returns it for your property is a different question. And a direction that declares nothing has no completeness: measurable is false and completeness is null, rather than 0% or 100%, because a source nobody has described is neither perfectly harmonized nor perfectly unharmonized.

The page envelope also carries corpus: seven ecosystem-wide totals, measured by the same query the platform’s own scheduled corpus check asserts them from, so a number you read here and a number your operator reads there cannot disagree. Pass includeCorpus: false to skip it.

Nothing here is invoked, no data source is contacted, no invocation is recorded and nothing is billed.


A proposal is an ask against the ecosystem dictionary, with a status you poll. Sending one invokes nothing, contacts no upstream and bills nothing, and it does not change the dictionary by itself. The rules below are what the tool enforces. They are also served with every proposal, as statements.

LaneCall
MCPportal-dictionary-propose: submit; read your own with id; withdraw your own with withdraw: true
RESTPOST /api/portal/<ecosystem>/dictionary/proposals (submit) · GET …/proposals and GET …/proposals/:id (read your own) · POST …/proposals/:id/withdraw (withdraw your own)

Your ecosystem and your app (the proposer) come from your key, never from the body. A proposal lives on the host you send it to (dev or staging), and is read, withdrawn and applied there.

Which host this is true on (2 October 2026). Sending a link by envelopePath, the four envelope_path_* refusals, the marker, withdrawal, applied, and the apply step itself are on dev from the deploy that carries them. They are not on staging until the Data Studio team deploys them there, which waits on a note to you at least one day before. Until then staging serves the earlier surface: a link by field only, no marker, no withdrawal, and an accepted link is a ruling that writes no link. Everything else in this section is true on both hosts.

Added 6 October 2026: on dev that day, on staging since 7 October 2026, 13:15 UTC. A term sent as its release id is read as the same term as its Section.Name, the marker’s alsoLinkedTo, and the refusal of a count over MCP (it was dropped before).

ArgumentRequiredMeaning
kindyeslink
termyesThe dictionary term, by name (BedroomsTotal), as Section.Name (Property.BedroomsTotal), or by its id in a dictionary release (property.prop-bedroomstotal)
sourceyesThe data source slug
field or envelopePathone of themfield: the upstream column name, as the source declares it. envelopePath: the key as you read it on the envelope (Section.Term; an array section may carry []). The Data Studio resolves the column. Never both.
skillnoWith envelopePath only: the skill that served the key. It narrows the resolution, but no answer depends on it today.
confirmedBy (MCP) / confirmation: { by, at? } (REST)to be acceptedWho on your side confirmed the link. A link is accepted on this confirmation.
notenoAnything a reviewer should read

A term is identified by its name. On the envelope a key is Section.Term, but a skill can serve a term under another section than the term’s own: Buildings.BedroomsTotal is the term BedroomsTotal, whose entry lives in Property. So term: "Buildings.BedroomsTotal" and term: "Property.BedroomsTotal" name the same term. The section only chooses between entries when one name exists in several sections, and today no term name does. A term’s id in a dictionary release (the term of each entry portal-dictionary-release serves, such as property.prop-stories) names the same term as that entry’s path (Property.Stories); either spelling is read as the same term.

By envelope path. The Data Studio resolves the raw column on the host you send to, from the same mapping the envelope is built from. The answer records what it resolved:

// tools/call portal-dictionary-propose
{ "kind": "link", "source": "<source slug>", "envelopePath": "Property.BedroomsTotal",
  "term": "Property.BedroomsTotal", "confirmedBy": "network-admin" }
// → 201 (REST) / the proposal (MCP)
{ "status": "received",
  "proposal": { "field": "<the resolved column>", "envelopePath": "Property.BedroomsTotal",
                "resolution": { "via": "link", "envelopeKey": "Property.BedroomsTotal", "declared": true, "skillCount": 3 } },
  "marker": { "alreadyLinked": { "linkId": "…", "term": "Property.BedroomsTotal", "sourceFieldName": "<column>" } } }

resolution.declared: false means the column is created by a transform, not declared on the source. The link still works. resolution.skillCount is how many published skills serve the key from this source. resolution.alsoWrittenByNameCount is present when some of them write the same key by name, so the link does not change it there. You read counts, not skill names; the Data Studio team’s reviewers see which skills they are.

When no single column stands behind the key: four refusals

Only a link sent by envelopePath can be refused this way. Nothing is stored. REST answers 409 with the code in detail.refusal. MCP answers an error with the code in error. Both carry skillCount (how many published skills the answer is about) and name no skill.

CodeWhat it meansWhat to do
envelope_path_ambiguousSeveral raw columns feed the key; fields lists them. About 1% of keys, mostly case variants of one value.Resend with field naming the one your ruling is about, or send one proposal per column
envelope_path_written_by_nameA skill writes the key by name (fields names the raw field when there is one; skillCount how many skills), so a link would not change itSend it as a note; it is reviewed as a mapping change
envelope_path_derivedA transform computes the key (coordinates today), so no single raw column stands behind itNothing to link; send a note if the value is wrong
envelope_path_not_servedThis source serves no such key on this host through a link or a by-name write, or the source is not on this hostCheck the key and the source. If the envelope attributes the key to this source, a transform computes it; send a note

A column withheld from responses is never named in any of these answers.

A link is always recorded. When something is already known about it on this host, the answer and the stored proposal carry marker:

MarkerMeaning
alreadyLinked: { linkId, term, sourceFieldName }This column is already linked to this term
alsoLinkedTo: [{ linkId, term }]With alreadyLinked only: the column is also linked today to other terms. Shown for review; it does not make your proposal a correction
duplicateOf: "<proposal id>"You sent the same link before. Points at the earliest proposal that is not rejected or withdrawn
correctionOf: [{ linkId, term }]The column is linked today, only to other terms: your proposal is a correction

marker is null when none applies. It is computed on the resolved column when you send envelopePath.

States

received → validated → queued (in review) → accepted | rejected. Before a ruling a proposal can also be stalled, or withdrawn by you. An accepted new term or supersession moves to released when a dictionary release carries it. rejected, stalled and withdrawn always carry a reason. rejected, released and withdrawn are final. transitions on each proposal lists where it can go next. Every ruling (accept, reject, stall, release) is the Data Studio team’s.

Withdrawing your own proposal

curl -s -X POST "https://api.<env>.trustinfrastructure.com/api/portal/<ecosystem>/dictionary/proposals/<id>/withdraw" \
  -H "Authorization: Bearer $TIP_API_KEY" -H 'Content-Type: application/json' \
  -d '{"reason":"our network admin reversed the ruling"}'

On MCP, pass withdraw (true), id and reason: { "withdraw": true, "id": "<id>", "reason": "…" }. You can withdraw from received, validated, queued or stalled, never after a ruling. Only the app that sent a proposal can withdraw it. Any other app gets 404 (MCP: proposal_not_found), which does not confirm the proposal exists. Without a reason the answer is 409 reason_required. After a ruling it is 409 transition_not_allowed.

What acceptance changes, and when

Acceptance is a ruling. An apply step then writes the link on the host where the proposal lives, and records what it did on the proposal as applied:

"applied": { "environment": "dev", "appliedAt": "…", "outcome": "inserted", "linkId": "…",
             "section": "Property", "term": "BedroomsTotal", "sourceFieldName": "<column>" }

outcome is inserted, or already_held when the link was already held. The proposal stays accepted. applied is null until the apply step has run on that host.

The apply step checks the link against the host’s catalog again: the source, the declared column, a served term, and a column that is not withheld. A link that fails these checks is not applied: it stays accepted with applied null until a reviewer rules on it again.

Codes on this surface (closed)

WhereCodeAnswer
submitproposal_invalid (MCP) / 400 (REST)The body does not validate; every error is named. A body carrying a count is refused.
submit by envelopePathenvelope_path_ambiguous, envelope_path_written_by_name, envelope_path_derived, envelope_path_not_served409 / MCP error; nothing stored
read / withdrawproposal_not_found (MCP) / 404Not yours, or no such id
withdrawproposal_id_required (MCP), reason_required, transition_not_allowed409 on REST

These codes are this tool’s own. They are not invocation error_codes. Adding one is announced before it ships, and an existing one is never renamed.


§5d Pre-flight: property-scout (placement and price; the unit-versus-building verdict)

property-scout is a $0 skill with two modes. It never fetches findings, never bills, and calls no paid source.

Which host this is true on (5 October 2026). This section describes property-scout 1.0.1. It was published on dev first, and reaches staging only after a note to you at least one day before. Answers from 1.0.0 differ in the ways listed under “Changes in 1.0.1” below.

The two modes

ModeYou sendYou get backWhat it calls
Address modeaddress (one line, with its city and province or state), optionally unit, or jurisdiction when you already know itthe jurisdiction it resolves to; coverage (what is known about the place); pricing (the package that would answer, its price, and the per-call price of the sources that package names, from the catalog)a geocoder, to place the address. That call is never billed to you. No paid source.
Envelope modeenvelope (the data.envelope of a package answer you already hold, unchanged), optionally unitthe unit-versus-building verdict over that answer, with typed caveats, and the price of the package that produced itno upstream call at all

Send exactly one of address or envelope.

Where envelope mode places the envelope

The verdict is judged in a jurisdiction. In envelope mode it is the first of these that is present:

  1. the jurisdiction you send;
  2. the envelope’s Address.StateOrProvince (a state or province code, such as BC or NY);
  3. the envelope’s Provenance.Route.Jurisdiction (where property-open-data answers carry it);
  4. the envelope’s root resolved_code.

A route or code is taken only when it is a subdivision code such as ca-bc or us-ny; a bare country is not. When none is present, the answer says jurisdiction_unresolved.

What coverage carries

KeyMeaning
wireda package covers this place. In envelope mode, the envelope you sent proves one served it.
unitDrillfull, passthrough, flag_only or none: how far this place’s packages reach toward a unit
ownerDatapublic, redacted_statewide, commercial_gated, mixed or unknown: what owner data this place’s records carry
registryok, no_jurisdiction or unavailable: whether the catalog read for the place succeeded

What pricing carries

Everything in pricing is read from the catalog; nothing is called.

KeyMeaning
package{slug, cost, pricing}: the package that would answer the address, or that produced the envelope. pricing is priced, free or unpriced; unpriced means the catalog holds no price, never that the call is free.
sourcesFromwhere that package names its own sources: dag (the steps it runs), price_provenance (the sources its price is built on, for a package that picks its sources per address), or null when it names none
sources[{slug, costPerCall, pricing}]: the per-call price of the sources that package names. Omitted when sourcesFrom is null: the scout does not guess which sources a package that dispatches per address would call for this place.

For the BC Legislature (501 Belleville Street, Victoria), address mode names victoria-property, free, with sourcesFrom dag and six sources at $0. An envelope from property-open-data for the same place names that package with sourcesFrom null and no sources.

The verdict

resolution.scope is one of unit, building, parcel or unresolved: the scope the answer actually reached. resolution.askedScope is unit when you sent unit (or a unit inside the address) and property otherwise, and resolution.scopeValid says whether the asked scope was reached. A scope that collapsed (a unit asked, a building answered) is stated by scopeValid and the caveats, not by the grade. The caveats this skill can emit, all professional-facing:

CaveatMeaning
building_scope_onlythe answer is for the building, not a unit
unit_not_resolveda unit was asked and the answer did not reach it
aggregate_lot_zero_valuesa condominium or co-op billing lot carrying zero values, not a unit’s
unit_drill_unavailablea unit was asked and this place has no unit-level descent
scope_signals_limitedthis jurisdiction’s packages carry no unit-level signals, so the verdict can tell a parcel from a building but cannot confirm a unit. Today only New York City and New Jersey carry them.
ambiguous_addressthe address resolved ambiguously across jurisdictions
jurisdiction_unwiredno package covers this jurisdiction yet (coverage.wired is false)
jurisdiction_unresolvedthe address (or the envelope) could not be placed in a jurisdiction; a fuller address may resolve. A served $0 answer, not a refusal.

next_actions name the next call: fetch_full_envelope names the package that would answer (priced), and drill_to_unit names it with the unit written into the address, where unit descent exists.

The grade

The run’s completeness and the answer’s provenance.completeness are the same grade, always:

GradeWhen
fullthe place was matched to a package that covers it (whatever the scope verdict)
emptyit was not: unresolved, ambiguous, out of coverage, or the geocoder unavailable. Still status: success, a served $0 answer.
partiala step of the scout itself failed, for example the catalog read behind pricing; status is partial too

Changes in 1.0.1

Refusals

Every refusal is input_invalid, costs $0, and its message names the defect:

DefectWhen
neitherneither address nor envelope was sent
bothboth were sent
not_an_objectenvelope is not a JSON object
error_bodyenvelope is an error response, not a package answer
no_provenanceenvelope carries no Provenance block, so it is not a package answer as the Data Studio returns it

An argument the skill does not declare is refused at the boundary, as for every skill.


§6 Feedback: the field shapes

Getting started §10 introduces the two feedback tools. Their shapes:

submit-invocation-feedback: feedback on one call

Every call’s _meta carries an invocationId. Rate the call against it:

// tools/call submit-invocation-feedback
{
  "invocation_id": "9ff4cb60-c4d0-4a79-b732-b919b51b5352",
  "status": "accepted",
  "reason": "Returned the right parcel for the Empire State Building."
}

invocation_id and status are required. status is accepted or rejected, a closed set. reason is optional, and most useful on a rejection. It is a two-step flow: call the skill first, then rate it by its id.

submit-repo-feedback: anything not tied to one call

For “this skill exists but is broken”, “I wish there were a skill for X”, “the docs say X but the API does Y”:

// tools/call submit-repo-feedback
{
  "title": "Address-only lookups pick the wrong borough for 350 5th Ave",
  "body": "Tested 350 5th Ave with no borough hint; came back as a Brooklyn parcel, not the Empire State Building.",
  "category": "bug_report"
}

title and body are required. The Data Studio team triages every entry.


§7 Re-key and revoke

Keys are issued by your ecosystem’s operator, through your Data Studio contact.

The key’s prefix names its environment (tip_dev_ for dev, tip_stg_ for staging, tip_ for production), so a leaked key is identifiable by environment. The prefix is checked before anything else, so a key works only on the environment that issued it.


§8 Is my key wired right?

When something looks off (an empty catalog, fewer skills than you were told to expect, a tool that should register and doesn’t), check in this order:

  1. The health check. GET https://api.<env>.trustinfrastructure.com/api/mcp/<ecosystem>/health?lane=consumer&surface=lean with your key. {"ok":true,…} means the key authenticates on this host and the consumer lane accepts it.
  2. tip-key-introspect. It describes the key making the call: its family (consumer, author or admin), its role slugs, the write capabilities it holds, and whether the consumer lane accepts it. Compare it with what you were told your key would be.
  3. The catalog. get-skills-manifest with summary=true lists what your key can call. If a skill you expect is missing, §2’s three gates say why: it is not published, it is internal, or your roles don’t reach its tags.

If all three look right and the answer is still wrong, send submit-repo-feedback with what you expected and what you got, or ask your Data Studio contact.


§9 Common 401, 403 and empty-result causes

The full table of error codes is in Getting started §9. These are the three failures partners meet most, with what to check.

1. 401 Unauthorized: the key and the host don’t match

Symptom. Any MCP call returns:

HTTP 401
{"jsonrpc":"2.0","error":{"code":-32001,"message":"Unauthorized — missing, invalid, or revoked API key"},"id":null}

The message is the same for a missing header, a mistyped key, a revoked key and a key sent to the wrong environment.

Common causes, most likely first.

  1. The wrong environment. The key’s prefix must match the host: tip_dev_ on api.dev.trustinfrastructure.com, tip_stg_ on api.staging.trustinfrastructure.com, tip_ on api.trustinfrastructure.com.
  2. A missing Authorization header. The value must be Bearer <full key>. Some HTTP libraries drop the Bearer prefix; check the raw header.
  3. A revoked key. Ask your Data Studio contact.
  4. A mistyped key. One transposed character is enough. Copy it again from the secure channel it came through.

2. 403 on the consumer lane

Symptom. HTTP 403 with key_family_not_accepted (JSON-RPC -32004).

Cause. You sent lane=consumer with a key that can write (an author or admin key). The lane accepts read-only keys only, and refuses before anything runs, at $0. Connect without lane=consumer, or use a consumer key. tip-key-introspect, called without lane=consumer, shows which family your key is.

3. An empty or unexpectedly small catalog

Symptom. get-skills-manifest or tools/list shows far fewer skills than you were told to expect.

Common causes, most likely first.

  1. No role, where you expected one. A key with no roles falls back to its tier, and a consumer key then sees only the skills marked consumer-visible, not the role-scoped catalog.
  2. A role narrower than your data. The role allows geography:CA-BC, and the skills you want are tagged Ontario.
  3. A role change still in the cache. A change takes up to 30 minutes to show (§2).
  4. Write tools missing on an author key. The four tip-skill-* tools register only when a role grants capability:skill-write; see Authoring skills.

tip-key-introspect shows your key’s roles; your Data Studio contact can change them.


See also