{"openapi":"3.1.0","info":{"title":"Opportunity Exchange public API","version":"1","description":"Keyless, read-only JSON for Saskatchewan labour-market data: occupations, sectors, postings, communities, cost-of-living outcomes, housing, trade, and measured national wages and rents. Free, no account, CORS-open. Generous per-client rate limit on /api/v1, /data and /mcp: every limited response carries RateLimit-Limit, RateLimit-Remaining and RateLimit-Reset, and a 429 carries Retry-After. All responses share one envelope: { meta: { platform, canonical, retrievedAt, dataStatus, evidence, provenance?, citeAs }, data }. `meta.evidence` is the compact machine contract: truthState, recordType, isActionable, status, synthetic flag, confidence, source kinds, as-of date, qualification, and assumptions. `truthState` uses a closed vocabulary — preview (synthetic demonstration records, never actionable), measured_aggregate (statistical survey estimates, not browsable postings), snapshot (authoritative published data for a stated period, not real-time), live (current actionable provider-backed records; the only state permitting live language), mixed (derived results inherit their weakest material input), metadata (platform self-description). `dataStatus` is its human-readable explanation. Derived resources also include full structured `provenance`: status (`derived` or `placeholder`), weakest confidence tier, conservative observation date, method, assumptions, and contributing sources. `citeAs` is a ready-made attribution string carrying the observation date where one exists; URLs and the ids inside them are permanent, so citations stay valid. Bulk one-fetch snapshots (same envelope) live outside this spec at /data/catalog.json, /data/occupations.json, /data/sectors.json, /data/municipalities.json, /data/jobs.json, /data/trade.json, /data/wages.json, /data/rents.json, /data/real-estate.json and /data/sources.json. Markdown twins of the human pages are indexed at https://veilpoint.ca/llms.txt."},"servers":[{"url":"https://veilpoint.ca/"}],"paths":{"/.well-known/agent-capabilities.json":{"get":{"summary":"Agent capability manifest: discovery links, supported interfaces, MCP configuration and tools, authentication, evidence, citation, and privacy policies.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/catalog":{"get":{"summary":"Machine-facing dataset catalogue: identifiers, entity relationships, methodologies, versioning, bulk files, and operational endpoints.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/roles":{"get":{"summary":"Occupation catalogue: id, title, NOC, sector, wage, demand.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/roles/{id}":{"get":{"summary":"One role in full: market, requirements, working conditions, training programs, and per-community outcomes (net and discretionary monthly income at the median wage).","parameters":[{"name":"id","in":"path","required":true,"description":"Role id from /api/v1/roles.","schema":{"type":"string"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/sectors":{"get":{"summary":"Sector rollups: vacancies, available talent, tightness, median wage, demand.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/sectors/{slug}":{"get":{"summary":"One sector: occupations, vacancies by community, notable employers.","parameters":[{"name":"slug","in":"path","required":true,"description":"Sector id from /api/v1/sectors.","schema":{"type":"string"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/jobs":{"get":{"summary":"Canonical opportunity search resource. Advertised postings, newest first. Rows with synthetic=true are placeholder corpus, not real vacancies. Invalid filter values are a 400, never silently ignored.","parameters":[{"name":"q","in":"query","description":"Free-text search over title, employer and description.","schema":{"type":"string"}},{"name":"region","in":"query","description":"Exact region name.","schema":{"type":"string"}},{"name":"municipality","in":"query","description":"Exact municipality name.","schema":{"type":"string"}},{"name":"occupation","in":"query","description":"Role id from the occupation catalogue.","schema":{"type":"string"}},{"name":"sector","in":"query","description":"Sector id from the sector list.","schema":{"type":"string"}},{"name":"type","in":"query","description":"Employment type.","schema":{"type":"string","enum":["full-time","part-time","seasonal","casual","contract"]}},{"name":"minWage","in":"query","description":"Minimum hourly wage, CAD.","schema":{"type":"number"}},{"name":"postedWithinDays","in":"query","description":"Only postings newer than this many days.","schema":{"type":"integer"}},{"name":"limit","in":"query","description":"Page size, 1-200 (default 50).","schema":{"type":"integer"}},{"name":"offset","in":"query","description":"Pagination offset (default 0).","schema":{"type":"integer"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/municipalities":{"get":{"summary":"The 20-municipality reference set: real census names, coordinates, populations.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/municipalities/{id}":{"get":{"summary":"One community: census reference plus its business directory.","parameters":[{"name":"id","in":"path","required":true,"description":"Municipality id from /api/v1/municipalities.","schema":{"type":"string"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/outcomes":{"get":{"summary":"The wage-vs-cost join: what an hourly wage leaves over each month, community by community, after tax, housing, utilities and commuting. Pass exactly one of occupation or wage.","parameters":[{"name":"occupation","in":"query","description":"Role id — models that role's median wage, with posting counts (truth state in meta.evidence).","schema":{"type":"string"}},{"name":"wage","in":"query","description":"Explicit hourly wage, CAD.","schema":{"type":"number"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/communities/rank":{"get":{"summary":"Communities ranked by monthly discretionary income. Pass exactly one of occupation or wage.","parameters":[{"name":"occupation","in":"query","description":"Role id whose median wage should be modelled.","schema":{"type":"string"}},{"name":"wage","in":"query","description":"Explicit positive hourly wage in CAD.","schema":{"type":"number","exclusiveMinimum":0}},{"name":"limit","in":"query","description":"Number of ranked communities, 1-20 (default 10).","schema":{"type":"integer","minimum":1,"maximum":20}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/employers/search":{"get":{"summary":"Search the public employer directory by name, sector, municipality and current hiring status.","parameters":[{"name":"q","in":"query","description":"Case-insensitive text search over employer and sector names.","schema":{"type":"string"}},{"name":"municipality","in":"query","description":"Municipality id from /api/v1/municipalities.","schema":{"type":"string"}},{"name":"sector","in":"query","description":"Sector-group id from /api/v1/sectors.","schema":{"type":"string"}},{"name":"hiring","in":"query","description":"true to return only employers currently hiring.","schema":{"type":"boolean"}},{"name":"limit","in":"query","description":"Result limit, 1-100 (default 25).","schema":{"type":"integer","minimum":1,"maximum":100}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/insights/credential-bottlenecks":{"get":{"summary":"Aggregate credential bottlenecks: vacancies gated, current and lapsed holders, suppression flags, and null-safe pressure semantics.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/insights/sector-tightness":{"get":{"summary":"Aggregate sector vacancies against available talent, with suppression flags and null-safe ratio semantics.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/trade":{"get":{"summary":"Provincial exports by NAPCS section and producer grain deliveries by delivery point. Real published data.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/wages":{"get":{"summary":"Measured wages and vacancies on the NOC spine (Statistics Canada, real published data): LFS median/average hourly wages by occupation group × province, JVWS vacancies and offered wages by NOC 2021 unit group × geography. No parameters: national overview. Wage medians are group-level; each per-NOC estimate names the published grouping that answered. Suppressed vacancy cells are absent, never zero.","parameters":[{"name":"noc","in":"query","description":"Five-digit NOC 2021 unit-group code from /api/v1/noc — that group across geographies.","schema":{"type":"string"}},{"name":"province","in":"query","description":"SGC 2021 code, postal abbreviation (e.g. SK), or \"canada\" — that geography's full wage table and top vacancies.","schema":{"type":"string"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/rents":{"get":{"summary":"Measured rental markets (CMHC Rental Market Survey via Statistics Canada, real published data): average rents by unit type and rental vacancy rates for every surveyed centre in Canada, October survey. No parameters: national overview with Saskatchewan detail and national extremes. Suppressed cells are null and unsurveyed centres absent, never zero.","parameters":[{"name":"province","in":"query","description":"SGC 2021 code or postal abbreviation (e.g. SK) — every surveyed centre in that province.","schema":{"type":"string"}},{"name":"centre","in":"query","description":"CMHC/StatCan geography code (cmhcCode) — one centre.","schema":{"type":"string"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/real-estate":{"get":{"summary":"Housing market by community: median price, listings, velocity, and the own-vs-rent monthly cost on the local wage.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/real-estate/{id}":{"get":{"summary":"One community housing market in full: price trend, home types, and the complete ownership model.","parameters":[{"name":"id","in":"path","required":true,"description":"Municipality id from /api/v1/real-estate.","schema":{"type":"string"}}],"responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/noc":{"get":{"summary":"The complete NOC 2021 classification: 516 unit groups, bilingual titles, TEER tiers, platform coverage flags. Real published data (Statistics Canada Open Licence).","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/v1/evaluate":{"post":{"summary":"POST-only decision endpoint for stated credentials, education, experience, driver licence, relocation radius or destination allowlist, housing and household needs, savings, and retraining time. Returns three-state feasibility (meets, does not meet, needs review), ranked or one target in depth. Constraints stay in the request body rather than access-log-visible URLs; never cached or stored.","requestBody":{"required":true,"content":{"application/json":{"schema":{"type":"object","properties":{"credentials":{"type":"array","maxItems":30,"items":{"type":"string"}},"educationLevel":{"type":"string","enum":["Secondary school","Secondary plus short certificate","Secondary plus trade certificate","Apprenticeship or journeyperson","Diploma","Bachelor degree"]},"experienceAreas":{"type":"array","maxItems":30,"items":{"type":"string","enum":["mechanical_systems","safety_systems","field_operations","process_monitoring","electrical_basics","coordination_admin","care_delivery","customer_service","digital_systems","logistics_handling","physical_operations"]}},"licenceClass":{"type":"string","enum":["None","Class 5","Class 3","Class 1A"]},"currentMunicipalityId":{"type":"string","enum":["saskatoon","regina","prince-albert","moose-jaw","swift-current","yorkton","north-battleford","weyburn","estevan","lloydminster","warman","martensville","humboldt","melfort","meadow-lake","melville","kindersley","nipawin","rosetown","la-ronge"]},"allowedMunicipalityIds":{"type":"array","maxItems":30,"uniqueItems":true,"items":{"type":"string","enum":["saskatoon","regina","prince-albert","moose-jaw","swift-current","yorkton","north-battleford","weyburn","estevan","lloydminster","warman","martensville","humboldt","melfort","meadow-lake","melville","kindersley","nipawin","rosetown","la-ronge"]}},"maximumRelocationKm":{"type":"number","minimum":0,"maximum":2500},"housingBudgetMonthly":{"type":"number","minimum":1,"maximum":50000},"bedroomsRequired":{"type":"string","enum":["bachelor","oneBedroom","twoBedroom","threeBedroomPlus"]},"incomeEarners":{"type":"integer","minimum":1,"maximum":8},"vehicleAvailable":{"type":"boolean"},"savingsDollars":{"type":"number","minimum":0,"maximum":1000000},"maximumRetrainingWeeks":{"type":"number","minimum":0,"maximum":520},"targetOccupationId":{"type":"string"},"limit":{"type":"integer","minimum":1,"maximum":20}},"additionalProperties":false}}}},"responses":{"200":{"description":"A private, no-store pathway evaluation envelope."},"400":{"description":"Invalid constraint; no value is silently discarded."},"404":{"description":"Unknown target occupation id."}}}},"/api/v1/sources":{"get":{"summary":"Provenance and serving mode of every data source: placeholder versus curated/live, confidence tier, licence, freshness, validation. The machine twin of /about/data.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/health":{"get":{"summary":"Liveness: infrastructure is responding. This is not a public-launch approval.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/readiness":{"get":{"summary":"Traffic readiness: the configured database and providers can serve their declared contract, including clearly-labelled preview data.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}},"/api/public-launch-status":{"get":{"summary":"Public-launch promotion gate: data, environment, moderation, and production-provider blockers. A 503 can mean intentionally not launch-approved while liveness and traffic readiness remain healthy.","responses":{"200":{"description":"The envelope described above."},"304":{"description":"Not modified (If-None-Match matched the data ETag)."},"400":{"description":"Invalid parameter value, with an error naming the parameter."},"404":{"description":"Unknown id or missing required parameter, with an error hint."},"429":{"description":"Rate limited. Retry-After and the RateLimit-Limit / RateLimit-Remaining / RateLimit-Reset headers state when to resume."}}}}}}