Developer API

Bring ODIN into your stack.

The ODIN developer API is in partner development. Its staged sandbox reads the live economic health index, prevailing conditions, and economic data series, and runs deterministic what-if simulations — over REST or the MCP connection, on the same key. Natural-language reasoning remains in development for external access.

Partner development. Access is enabled per organization for controlled sandbox work. This public page cannot see whether your organization is enabled. To check, sign in, open Settings → API keys. If the key controls appear, an owner or admin can create a key (choose a lifetime from 30 to 365 days) and store it in your secret manager. If the page says access is not enabled, email info@odinos.ca to request access.

Authentication

Every request is authenticated with a bearer key passed in the Authorization header. Keys are org-scoped and secret — treat them like a password; they are shown once at issuance and stored only as a hash. Every response carries an X-Odin-Version header and Cache-Control: private, no-store.

Header

Authorization: Bearer odn_...

Getting a key. Sign in at app.odinos.ca and open Settings → API keys. Organization owners and admins can create keys (choose a lifetime from 30 to 365 days), see when each key was last used, and revoke instantly. Revocation takes effect within a minute.

Rotating a key. Zero-downtime rotation is by overlap: create the replacement key, move your integration to it, then revoke the old one. Your organization can hold up to five active keys, so old and new can run side by side for as long as the migration needs.

Quickstart

Start with the deterministic health endpoint. It confirms authentication, scopes, metering, and data access without waiting on a language model. The self-serve sandbox does not include natural-language reasoning yet.

/bin/bash -c '
set -euo pipefail
read -rsp "ODIN key: " ODIN_KEY; printf "\n"
printf "Authorization: Bearer %s\n" "$ODIN_KEY" | curl -sS https://api.odinos.ca/v1/pulse/health-index -H @-
unset ODIN_KEY
'

Paste this block into a terminal, then enter the key at the hidden prompt. Do not paste the confirmation dialog's Copy quickstart command into a normal shell: it contains the cleartext key, which the shell can retain in its history.

Commercial-lending starter set

A worked path for commercial lending and credit advisory teams — anyone whose day is deal intake, structuring, and timing conversations across sectors like construction, manufacturing, distribution, and professional services. ODIN reads the economy at the sector and province level; it never analyzes, scores, or names individual companies, and nothing here is borrower-level input. The catalog tracks over a thousand series; these are the sixteen a credit desk actually starts with, plus the five calls that turn them into a working read. Everything on this page is served by the current API surface — no capability below is promissory.

Scopes. A self-serve key covers everything in this section: dashboard reads, data series, and deterministic what-if simulations. Plain-English reasoning (/v1/reason) is not in the self-serve set — it remains in development for external access on a staged schedule. When you mint a key through the app's key endpoint rather than the UI, the create call takes an expires field with preset lifetimes 30d, 90d, 180d, or 365d; the response identifies the new key as key_id, and revocation addresses that same key UUID.

The sixteen series

Each identifier works in the series lookup and batch endpoints, and all but the market-implied spread direction also trace through the transmission endpoint. Each row states its geographic coverage and how often its data refreshes. Sector insolvency series are national monthly counts discovered from the regulator's current package manifest (official publication typically trails the reporting month by 5–6 weeks); province-level insolvency coverage is currently Ontario, and construction-cost coverage is currently Toronto.

IdentifierWhat it isWhy a credit desk reads itCoverageUpdates
slos_business_lendingBusiness lending conditions (senior loan officer survey)Bank of Canada's quarterly survey of bank lending officers on business credit. Positive readings mean banks are tightening; negative means easing. The most direct read on how hard it is to get a business loan approved.CanadaQuarterly
slos_lending_conditionsOverall lending conditions (survey composite)The same survey's composite across business, mortgage, and consumer lending — one number for overall bank posture.CanadaQuarterly
bos_credit_conditionsFirms reporting easier credit (business outlook survey)The borrower's side of the table: the Bank of Canada's quarterly survey of firms on whether credit got easier or harder to obtain.CanadaQuarterly
boc_overnightBank of Canada policy rateThe anchor for every floating-rate facility. Stored as a monthly series; the rate itself moves only on the eight scheduled decision dates.CanadaMonthly
prime_rateBig-bank prime business rateThe posted prime rate at the large chartered banks — the base most variable-rate business loans are priced from. Published daily; it steps only when the policy rate moves.CanadaBusiness-daily
ca_business_loansBusiness loans outstanding at chartered banksTotal business lending on bank balance sheets, in millions of dollars. Whether credit is actually flowing, not just what surveys say.CanadaMonthly
ca_ig_spread_directionCorporate credit spread direction (market-implied)Whether investment-grade corporate borrowing spreads are widening or narrowing, computed from freely quoted Canadian bond-fund prices. This is a direction, not a spread level — it is never quoted in basis points, and the level is index-arbitrary; the direction of change carries the signal.CanadaMonthly
insolvency_rate_constructionBusiness insolvencies — constructionBusiness bankruptcies plus proposals in construction, from the federal insolvency regulator's open data. A raw filing count, despite the identifier's name — not a rate.CanadaMonthly; official publication typically lags 5–6 weeks
insolvency_rate_manufacturingBusiness insolvencies — manufacturingSame measure for manufacturing — also a count, not a rate.CanadaMonthly; official publication typically lags 5–6 weeks
insolvency_rate_retailBusiness insolvencies — retail and distributionSame count for retail trade — the closest served proxy for distribution-facing stress.CanadaMonthly; official publication typically lags 5–6 weeks
insolvency_rate_professionalBusiness insolvencies — professional servicesSame count for professional services firms.CanadaMonthly; official publication typically lags 5–6 weeks
insolvency_rate_accommodationBusiness insolvencies — accommodation and foodSame count for accommodation and food service.CanadaMonthly; official publication typically lags 5–6 weeks
insolvency_filingsConsumer insolvency filings (Ontario)Monthly consumer bankruptcies plus proposals in Ontario — household-side stress in the largest provincial credit market. Ontario is the only province served today.OntarioMonthly
ccaa_filingsLarge-company creditor-protection filingsQuarterly count of filings under the federal large-company restructuring statute — the top end of the corporate distress spectrum.CanadaQuarterly
mortgage_arrearsMortgage arrears rateShare of residential mortgages 90+ days behind — the standard household credit-performance benchmark lenders watch.CanadaQuarterly
bcpi_toronto_highrise_compositeConstruction costs — Toronto high-rise compositeStatistics Canada's building construction price index for high-rise residential work in Toronto — input-cost pressure for construction borrowers. Toronto is the only market served today; use this exact identifier, as other identifiers sharing its prefix are national commodity prices, not construction costs.Toronto CMAQuarterly

The first five calls

1 — Prove the connection

Confirms your key, organization, and granted scopes without touching a data endpoint.

curl -s https://api.odinos.ca/v1/ping -H "Authorization: Bearer odn_your_key_here"

2 — The headline context read

The state-of-the-economy panel in three deterministic reads: the composite health score with its component pillars, the economic conditions currently prevailing, and the policy rates that anchor every facility — including the next scheduled rate decision date. Response shapes are documented under Endpoints.

curl -s https://api.odinos.ca/v1/pulse/health-index -H "Authorization: Bearer odn_your_key_here"
curl -s https://api.odinos.ca/v1/pulse/regime -H "Authorization: Bearer odn_your_key_here"
curl -s https://api.odinos.ca/v1/pulse/policy-rates -H "Authorization: Bearer odn_your_key_here"

3 — The credit-cycle batch

All sixteen series in one call, with history from 2024 onward — the input to a “should this borrower move now” view: bank posture from both sides of the table, the cost anchors, whether credit is flowing, and where sector stress is climbing. Each result carries its unit, its own freshness stamp, and a per-key status, so one unavailable series never fails the batch.

curl -s https://api.odinos.ca/v1/signals/batch -H "Authorization: Bearer odn_your_key_here" -H "Content-Type: application/json" -d '{"keys":["slos_business_lending","slos_lending_conditions","bos_credit_conditions","boc_overnight","prime_rate","ca_business_loans","ca_ig_spread_direction","insolvency_rate_construction","insolvency_rate_manufacturing","insolvency_rate_retail","insolvency_rate_professional","insolvency_rate_accommodation","insolvency_filings","ccaa_filings","mortgage_arrears","bcpi_toronto_highrise_composite"],"date_from":"2024-01-01"}'

4 — Borrowing-cost transmission

How a policy-rate move actually reaches loan pricing: the channels leading out of the policy rate, each with the historical strength of the relationship and its typical lag in days. This is structure, not a forecast — the timing half of “the cut is real, but it reaches your renewal in weeks, not days.”

curl -s https://api.odinos.ca/v1/transmission -H "Authorization: Bearer odn_your_key_here" -H "Content-Type: application/json" -d '{"signal":"boc_overnight","max_depth":3}'

5 — The rate scenario

A quarter-point cut to the policy rate, propagated through recorded economic relationships to a six-month horizon: which indicators move, in what direction, by how much, through how many steps. Deterministic — no language model involved — so the same shock always returns the same projection. Term-sheet timing conversations start here.

curl -s https://api.odinos.ca/v1/counterfactual -H "Authorization: Bearer odn_your_key_here" -H "Content-Type: application/json" -d '{"shock":{"node":"boc_overnight","delta":-0.25},"horizon_months":6}'
What this set deliberately is not. Sector conditions and timing — never borrower diagnostics, credit scoring, lender matching, or approval input; that side of the fence belongs to your own systems, and ODIN's output is research context, not advice. No individual company is analyzed or named, by design. And the plain-English reasoning surface stays in development for external access until its staged rollout — the sixteen series and five calls above need none of it.

Endpoints

Base URL https://api.odinos.ca (keys issued earlier may reference the previous base URL, which continues to work). All responses are JSON. Each endpoint documents the identifier type it accepts: counterfactual requests use identifiers defined for what-if simulations, while signal-series endpoints accept signal keys. The two identifier types are not interchangeable.

Economic responses carry a data_state receipt. Read freshness.observed_period for when a source fact applies, ingested_at for when ODIN received it, and computed_at for when a derived result was produced. A recent ingestion never makes an old observation current. Missing cadence evidence is unknown, while licence-restricted data is withheld. Aggregate responses also carry acoverage receipt naming eligible, served, withheld, missing, stale, unknown, and truncated cohorts. Stale classifies served data; unknown may classify a served or unresolved eligible item. Neither is an additional denominator bucket.

GET/v1/ping

Authenticated liveness check. Confirms your key, and returns your organization, granted scopes, and rate tier — nothing else.

Response

{
  "status": "ok",
  "organization": "Your Organization",
  "scopes": ["pulse", "signals", "counterfactual"],
  "rate_tier": "self_serve",
  "request_id": "req_09e19b5f9f534205a23a08a9a2819706"
}
GET/v1/pulse/health-index

The current health of the Canadian economy as a single score out of 100, with six component pillars (growth, monetary, consumer, labor, trade, systemic), the best and worst areas, and the 30-day trend. Recomputed nightly. This is the leading example because it runs with the standard self-serve pulse scope.

Response

{
  "composite_score": 54.9,
  "pillars": {
    "growth":                  { "score": 51.2, "label": "Growth" },
    "monetary_health":         { "score": 44.0, "label": "Monetary" },
    "consumer_balance_sheets": { "score": 47.1, "label": "Consumer" },
    "labor_market":            { "score": 55.4, "label": "Labor" },
    "trade_external":          { "score": 42.8, "label": "Trade" },
    "systemic_stability":      { "score": 49.6, "label": "Systemic" }
  },
  "worst_pillar": "trade_external",
  "best_pillar": "labor_market",
  "trend": "stable",
  "trend_delta_30d": -0.7,
  "computed_at": "2026-07-13T05:42:00Z",
  "run_id": "health-run-id",
  "pillar_count": 6,
  "oldest_input_period": "2026-06-01",
  "newest_input_period": "2026-08-01",
  "trend_baseline_at": "2026-06-13T05:42:00Z",
  "data_state": { "availability": "available", "freshness": { "status": "current" }, "quality": { "status": "ok", "flags": [] } },
  "coverage": { "status": "complete", "complete": true, "eligible": 6, "served": 6, "withheld": 0, "missing": 0, "stale": 0, "unknown": 0, "truncated": false }
}
POST/v1/reason

Ask any economic question in plain English. ODIN reads live data, traces transmission through the twin, and returns a reasoned answer. When ODIN uses live web sources, the answer lists them. Plain-English output — no system jargon. This capability remains in development for external access and is not included on self-serve sandbox keys.

Required scope: reason, granted separately for controlled partner testing. A standard self-serve key correctly returns a 403 missing-scope response here.

grounding_status is grounded, degraded, or not_used. A degraded answer also carries a confidence note; citations expose only a bounded public URL and title, never check names, arguments, raw results, or provider internals. The exact number of additional valid citations beyond the 20-item response cap is reported in citations_omitted. A private or malformed URL, or a title that is multi-line, control-bearing, or written in internal machine vocabulary, is omitted and makes the answer degraded even when another public URL survives. If ODIN reaches its analysis budget after tracing the highest-leverage paths, the response is marked truncated: true and degraded rather than being presented as exhaustive. Reasoning takes 1–2 minutes; set max_seconds and your client timeout accordingly.

Streaming is preview, off by default. When the server enablesRB_V1_REASON_STREAM, add "stream": true to the request body for text/event-stream. Otherwise the existing JSON response is unchanged. Each draft event contains answer text,provisional: true and an increasing sequence. Drafts have not passed grounding or numeric checks and can contain claims that the final answer removes or corrects. Do not use drafts for decisions. The final event contains the complete, screened JSON response shown below, including citations, grounding, confidence and completeness fields. Replace the entire draft with that answer; do not append it. Final means processing finished, not necessarily fully grounded or complete: inspect those fields. An error event or a disconnect means no final answer; discard the draft. Streaming HTTP status is 200; inspect terminal events for errors. No event IDs, reconnect replay or heartbeat tokens are supplied. Disconnect cancels work and records observed usage. Final-outcome quota headers are unavailable in streaming mode; query/v1/usage for updated usage.

MCP odin_reason on POST /mcp/v1 accepts the same optional stream argument. SSE message events carry JSON-RPC notifications/odin/reason_draft notifications whose params contain the originating RPC request_id, text, sequence and provisional marker. The terminal message is the unchanged JSON-RPC tool result (including isError on failure). Clients must opt in and support this preview notification. Tool preambles are never drafts; direct answers may remain buffered until routing completes. Streaming has no guaranteed first-token latency.

Request

POST /v1/reason
{
  "question": "How would a 50bp BoC cut transmit to housing?",
  "max_seconds": 120          // optional, default 120, max 150
}

Response

{
  "answer": "A 50bp cut lowers variable mortgage rates within weeks...",
  "confidence_notes": null,
  "grounding_status": "grounded",
  "citations": [
    { "url": "https://www.bankofcanada.ca/rates/interest-rates/canadian-interest-rates/",
      "title": "Policy interest rate" }
  ],
  "citations_omitted": 0,
  "elapsed_seconds": 91.4,
  "truncated": false,
  "request_id": "req_4948eba494704d4680b1a8a7fc0b014f",
  "evidence_receipts": [],
  "grounding_reasons": [],
  "analysis_status": "complete",
  "analysis_reasons": [],
  "computed_at": "2026-08-23T20:00:00Z",
  "data_state": { "availability": "available", "freshness": { "status": "current" }, "quality": { "status": "ok", "flags": [] } },
  "coverage": { "status": "complete", "complete": true, "eligible": 1, "served": 1, "withheld": 0, "missing": 0, "stale": 0, "unknown": 0, "truncated": false }
}

Identity and provenance questions are answered without reasoning and carry one extra trailing key, guard, with the value "identity".

GET/v1/pulse/regime

Which broad economic conditions currently prevail in Canada — each with a plain-English description and severity — plus the conditions that are currently dormant. An empty active_regimes list is a normal answer when all five current conditions are present under inactive_regimes: it means no monitored condition is dominant right now. The five rows must come from one detector run. The top-level as_of is that complete cohort's UTC timestamp and must be no more than 48 hours old; individual rows do not each carry the computation timestamp. When known and unknown conditions together cover all five, REST and MCP return HTTP 200 with regime: null, status: "not_computed", and an explicit unknown_regimes list. The known lists keep their existing shape; coverage states how many conditions have evidence. This partial response has as_of: null. Empty, errored, malformed, or invalid known cohorts still return 503 not_computed.

Response

{
  "active_regimes": [
    { "regime_key": "inflationary_regime", "label": "Inflationary Regime",
      "description": "Above-target inflation with...", "severity": "elevated" }
  ],
  "inactive_regimes": [
    { "regime_key": "liquidity_crisis_regime", "label": "Liquidity Crisis" },
    { "regime_key": "deflationary_regime", "label": "Deflationary Regime" },
    { "regime_key": "asset_bubble_regime", "label": "Asset Bubble Regime" },
    { "regime_key": "credit_cycle_regime", "label": "Credit Cycle" }
  ],
  "as_of": "2026-08-09T04:22:00Z",
  "run_id": "regime-run-id",
  "data_state": { "availability": "available", "freshness": { "status": "current" }, "quality": { "status": "ok", "flags": [] } },
  "coverage": { "status": "complete", "complete": true, "eligible": 5, "served": 5, "withheld": 0, "missing": 0, "stale": 0, "unknown": 0, "truncated": false }
}

HTTP 200 with unknown evidence

{
  "regime": null,
  "status": "not_computed",
  "active_regimes": [],
  "inactive_regimes": [{ "regime_key": "inflationary_regime", "label": "Inflationary Regime" }],
  "unknown_regimes": [
    { "regime_key": "liquidity_crisis_regime", "label": "Liquidity Crisis", "status": "not_computed" },
    { "regime_key": "deflationary_regime", "label": "Deflationary Regime", "status": "not_computed" },
    { "regime_key": "asset_bubble_regime", "label": "Asset Bubble Regime", "status": "not_computed" },
    { "regime_key": "credit_cycle_regime", "label": "Credit Cycle", "status": "not_computed" }
  ],
  "as_of": null,
  "run_id": "regime-run-id",
  "data_state": { "availability": "unavailable", "freshness": { "status": "unknown", "computed_at": null }, "quality": { "status": "unknown", "flags": ["incomplete_computation_cohort", "computation_time_unknown"] } },
  "coverage": { "status": "partial", "complete": false, "eligible": 5, "served": 1, "withheld": 0, "missing": 4, "stale": 0, "unknown": 4, "truncated": false }
}
POST/v1/counterfactual

Simulate a shock to one part of the economy and see the projected knock-on effects: which indicators move, in what direction, by how much, and through how many transmission steps. The shock must be a finite numeric change or an exact to:<finite number> target representable as an IEEE-754 number; nonzero targets too small to survive conversion are invalid. An absolute target also requires a current source value, otherwise ODIN returns 503 not_computed. Returned deltas are always finite. ODIN checks the canonical source's licence before reading its baseline or running the simulation: a restricted source returns 403 licensing_restricted, while a source that requires credit returns its note in top-level attribution. Every projected node and path hop passes the same licence check. Restricted projections are omitted, counted under coverage.withheld, and explained under licensing. Deterministic — no language model involved.

Request

POST /v1/counterfactual
{
  "shock": { "node": "ca_unemployment_rate", "delta": 0.5 }, // finite number or "to:<finite number>"
  "horizon_months": 6,        // optional, default 6, 1-24
  "max_depth": 3              // optional, default 3, 1-4
}

Response

{
  "shock": { "node": "ca_unemployment_rate", "label": "Unemployment Rate (Canada)",
             "delta": 0.5, "description": "Increase the selected economic measure by 0.5." },
  "horizon_months": 6,
  "impacts": [
    { "node": "mortgage_arrears", "label": "Mortgage Arrears",
      "direction": "up", "projected_delta": 0.1, "hops": 1 }
  ],
  "pillar_summary": {
    "credit": { "net_delta": 0.1, "magnitude": 0.1, "node_count": 1,
      "direction": "up", "examples": [
        { "target": "Mortgage Arrears", "delta": 0.1 }
      ] }
  },
  "assumptions": "The simulation follows recorded economic relationships, reduces effects at each transmission step and when expected timing extends beyond the selected horizon, and omits relationships below its minimum strength setting. Some included relationships describe association rather than a proven intervention, so results should be treated as directional scenarios.",
  "computed_at": "2026-08-23T20:00:00Z",
  "data_state": { "availability": "available", "freshness": { "status": "current", "observed_period": "2026-08-01", "ingested_at": null, "computed_at": "2026-08-23T20:00:00Z", "evaluated_at": "2026-08-23T20:00:00Z", "expected_frequency": "monthly", "reason_code": null, "age_seconds": 0, "stale_after_seconds": 300, "basis": "computation_time" }, "quality": { "status": "caution", "flags": ["modeled_current"] } },
  "source_data_state": { "availability": "available", "freshness": { "status": "current", "observed_period": "2026-08-01", "ingested_at": "2026-08-08T13:05:00Z", "computed_at": "2026-08-23T20:00:00Z", "evaluated_at": "2026-08-23T20:00:00Z", "expected_frequency": "monthly", "reason_code": null, "age_days": 22, "stale_after_days": 76, "basis": "declared_frequency" }, "quality": { "status": "ok", "flags": [] } },
  "coverage": { "status": "complete", "complete": true, "eligible": 1, "served": 1, "withheld": 0, "missing": 0, "stale": 0, "unknown": 0, "truncated": false },
  "attribution": null
}

Additional fields when a licence restriction or attribution applies

{
  "impacts": [
    { "node": "...", "label": "...", "direction": "up", "projected_delta": 0.1, "hops": 1,
      "attribution": "credit line required by the source of this projected measure or its path" }
  ],
  "licensing": {
    "restricted_projections_withheld": "integer — counted across the WHOLE simulated walk, not only the measures returned",
    "withholding_reasons": [
      { "code": "licensing_restricted", "message": "plain-English reason from the source", "withheld": "integer" }
    ],
    "pillar_summary_withheld": "boolean — the pillar summary is withheld whenever its totals would combine a withheld projection",
    "note": "plain-English explanation of what was withheld and why"
  }
}
POST/v1/counterfactual/evidence

Request typed, structured evidence ranges for a what-if. This capability is mounted only when the evidence contract is enabled. It reports examined and omitted targets, unit checks, unavailable reasons and limitations; it is not a probability forecast.

Request

POST /v1/counterfactual/evidence
{
  "shock": { "node_key": "boc_overnight", "mode": "absolute_delta",
             "value": -0.25, "unit": "percent" },
  "horizon_days": 180,
  "max_depth": 3
}

Response

{
  "contract_version": "counterfactual_evidence_v1",
  "as_of": "2026-08-23T20:00:00Z",
  "observation_selection_method": "latest_eligible_at_or_before_as_of_v1",
  "shock": { "node_key": "boc_overnight", "label": "Bank of Canada overnight rate",
             "mode": "absolute_delta", "value": -0.25, "unit": "percent",
             "unit_status": "exact", "baseline": null, "unit_receipt": {} },
  "impacts": [],
  "impact_coverage": { "reachable_targets_total": 0, "targets_examined": 0,
                       "targets_not_examined": 0, "impacts_returned": 0,
                       "examined_impacts_omitted": 0 },
  "pillar_summary": {},
  "methodology": { "id": "structured_evidence_range_v1",
                   "probability_distribution": false, "coverage_guarantee": false,
                   "statement": "The output should therefore be read as structured evidence within the map, not as a posterior probability over the whole Canadian economy." },
  "limitations": [],
  "data_state": {},
  "coverage": {}
}
GET/v1/signals/{key}

Look up one Canadian economic data series by identifier: description, unit, and up to five years of values. Optional date_from / date_to query parameters narrow the range. Series under restrictive data licenses are not served. For multi-geography series, the geography, values, and latest_ingested_at stamp all describe the same best-covered valid cohort inside that requested window. If the cohort exceeds 5,000 periods, ODIN returns its newest 5,000 in chronological order;series_total, series_returned, and series_omitted state the exact cohort denominator; the freshness stamp describes only those returned rows. Database-only non-finite, numeric-overflow and numeric-underflow values are excluded before cohort selection. When a series has a declared ODIN sanity envelope, out-of-envelope values are excluded by that same population before geography, counts and freshness are chosen; series without a declared envelope retain the finite, representable-number rule.

Response

{
  "signal_key": "ca_unemployment_rate",
  "display_name": "Unemployment Rate (Canada)",
  "unit": "percent",
  "frequency": "monthly",
  "geography_level": "national",
  "geography_level_label": "National",
  "geography": "CAN",
  "description": "...",
  "attribution": null,          // set only when the source license requires a credit line
  "series": [ { "period": "2026-06-01", "value": 6.5 } ],
  "truncated": false,
  "series_total": 1,
  "series_returned": 1,
  "series_omitted": 0,
  "latest_ingested_at": "2026-07-11T13:05:00Z",
  "data_state": {
    "availability": "partial",
    "freshness": { "status": "current", "observed_period": "2026-06-01", "ingested_at": "2026-07-11T13:05:00Z", "computed_at": null, "evaluated_at": "2026-07-23T20:00:00Z", "expected_frequency": "monthly", "reason_code": null, "age_days": 52, "cadence_days": null, "stale_after_days": 76, "basis": "declared_frequency" },
    "quality": { "status": "caution", "flags": ["insufficient_coverage"] }
  },
  "coverage": { "status": "partial", "requested_from": "2021-07-23", "requested_to": "2026-07-23", "available_from": "2026-06-01", "available_to": "2026-06-01", "requested_start_covered": false, "requested_end_covered": true, "internal_cadence_status": "insufficient_history", "observed_intervals": 0, "max_gap_days": null, "allowed_gap_days": 35, "internal_gaps_covered": false, "complete": false, "eligible": 1, "served": 1, "withheld": 0, "stale": 0, "unknown": 0 }
}
GET/v1/signals

Browse the catalog of available series. Filter with search, category or geography_level; page with cursor and limit (default 50). Every identifier returned here can be passed straight to the lookup, batch and transmission endpoints.

Response

{
  "items": [{
    "signal_key": "ca_unemployment_rate",
    "display_name": "Unemployment Rate (Canada)",
    "unit": "percent",
    "category": "labour",
    "category_label": "Labour",
    "domain": "employment",
    "domain_label": "Employment",
    "geography_level": "national",
    "geography_level_label": "National",
    "description": "...",
    "attribution": null,
    "retired": false,
    "retired_at": null
  }],
  "count": 1,
  "next_cursor": "Y2FfdW5lbXBsb3ltZW50X3JhdGU",
  "has_more": true,
  "filters": {
    "search": null, "category": "labour",
    "geography_level": null, "include_retired": false
  }
}
POST/v1/signals/batch

Look up many series in one call instead of looping the single-series endpoint — the efficient path when you are hydrating a dashboard or a model.

Request

POST /v1/signals/batch
{
  "keys": ["ca_unemployment_rate", "unknown_key"],  // 1-25, alias-aware
  "date_from": "2025-01-01",                        // optional, inclusive
  "date_to": null                                    // optional, inclusive
}

Response

{
  "requested": 2,
  "succeeded": 1,
  "failed": 1,
  "data_state": { "availability": "partial", "freshness": { "status": "unknown", "observed_period": "2026-08-01", "ingested_at": "2026-08-08T13:05:00Z", "computed_at": null, "evaluated_at": "2026-08-23T20:00:00Z", "expected_frequency": "mixed", "reason_code": "child_state_incomplete_or_unknown", "age_days": null, "stale_after_days": null, "basis": "child_states" }, "quality": { "status": "unknown", "flags": ["insufficient_coverage", "source_schedule_unknown", "not_computed"] } },
  "coverage": { "status": "partial", "complete": false, "eligible": 2, "served": 1, "withheld": 0, "missing": 1, "stale": 0, "unknown": 1, "truncated": true },
  "results": [
    {
      "requested_key": "ca_unemployment_rate", "status": "ok",
      "signal_key": "ca_unemployment_rate",
      "display_name": "Unemployment Rate (Canada)",
      "unit": "percent", "frequency": "monthly",
      "geography_level": "national", "geography_level_label": "National",
      "geography": "CAN",
      "description": "...", "attribution": null,
      "series": [{ "period": "2026-08-01", "value": 6.5 }],
      "truncated": false,
      "series_total": 1, "series_returned": 1, "series_omitted": 0,
      "latest_ingested_at": "2026-08-08T13:05:00Z",
      "data_state": { "availability": "partial", "freshness": { "status": "current", "observed_period": "2026-08-01", "ingested_at": "2026-08-08T13:05:00Z", "computed_at": null, "evaluated_at": "2026-08-23T20:00:00Z", "expected_frequency": "monthly", "reason_code": null, "age_days": 22, "cadence_days": null, "stale_after_days": 76, "basis": "declared_frequency" }, "quality": { "status": "caution", "flags": ["insufficient_coverage"] } },
      "coverage": { "status": "partial", "requested_from": "2025-01-01", "requested_to": "2026-08-23", "available_from": "2026-08-01", "available_to": "2026-08-01", "requested_start_covered": false, "requested_end_covered": true, "internal_cadence_status": "insufficient_history", "observed_intervals": 0, "max_gap_days": null, "allowed_gap_days": 35, "internal_gaps_covered": false, "complete": false, "eligible": 1, "served": 1, "withheld": 0, "stale": 0, "unknown": 0 }
    },
    {
      "requested_key": "unknown_key", "status": "error",
      "error": { "code": "not_found", "message": "No signal with that identifier exists." },
      "data_state": { "availability": "unavailable", "freshness": { "status": "unknown", "observed_period": null, "ingested_at": null, "computed_at": null, "evaluated_at": "2026-08-23T20:00:00Z", "expected_frequency": null, "reason_code": "not_found", "age_days": null, "stale_after_days": null, "basis": "insufficient_history" }, "quality": { "status": "unknown", "flags": ["source_schedule_unknown", "not_computed"] } }
    }
  ]
}
GET/v1/pulse/policy-rates

The policy rates that anchor Canadian conditions — the Bank of Canada overnight rate and its peers — plus buyer purchasing-power context, in one call. Every item carries its own latest official print date; unavailable tracked series are named.

Response

{
  "as_of": "2026-07-15",
  "rates": [{
    "signal_key": "boc_overnight", "label": "BoC Overnight Rate",
    "value": 2.25, "unit": "percent", "period": "2026-07-15",
    "ingested_at": "2026-07-15T14:05:00Z"
  }, {
    "signal_key": "buyer_purchasing_power", "label": "Buyer Purchasing Power",
    "value": 612400.0, "unit": "dollars", "period": "2026-07-01",
    "ingested_at": "2026-07-02T05:10:00Z"
  }],
  "unavailable": ["goc_2yr"],  // present only when a tracked series is unavailable
  "data_state": { "availability": "partial", "freshness": { "status": "unknown", "expected_frequency": "mixed", "reason_code": "child_state_incomplete_or_unknown", "basis": "child_states" }, "quality": { "status": "unknown", "flags": ["mixed_observation_periods", "insufficient_coverage", "source_schedule_unknown"] } },
  "coverage": { "status": "partial", "complete": false, "eligible": 8, "served": 7, "withheld": 0, "missing": 1, "stale": 0, "unknown": 0, "truncated": false },
  "decisions": {
    "boc": { "last_decision": "2026-07-15", "next_scheduled_decision": "2026-09-02" },
    "fed": { "last_decision": "2026-07-29", "next_scheduled_decision": "2026-09-16" }
  },
  "note": "Each rate shows its own latest official print date in 'period'..."
}
GET/v1/pulse/headlines

The latest nightly checked story lines, ready for a human dashboard or briefing header. Every line is plain English, every number was machine-verified before publication, and only audit-passed lines from the most recent line_date are served. If the latest night has zero passed lines, the response is a 200 with an empty headlines array and an explicit note rather than silently serving an older date. Each served line includes its fixed interpretation identifier and list fingerprint, the time the line passed its nightly audit (null for lines audited before ODIN began recording that instant — which is every line until the first nightly audit after this field ships), source series and source dates, and its own current, stale, or unknown status — unknown covers a missing date and the rarer case of a date that is somehow ahead of the most recent expected nightly run. Licensing on this surface is controlled by the deny-list and source licence tags, not by retirement: a retired-but-untagged series, or a cited key with no row in the series catalogue at all, can still be served — bounded to the fixed set of series the nine widget inputs can ever cite.

Response

{
  "as_of": "2026-09-02",
  "age_days": 0,
  "methodology": "every number machine-verified against source data before publication",
  "headlines": [
  {
    "text": "Big Six avg — Cet1 Ratio registered 13.62%. This matters.",
    "context": "Big Six bank stress",
    "line_date": "2026-09-02",
    "checked": true,
    "interpretation_id": 1,
    "list_version": "v1",
    "list_sha256": "02e44d798a7cc107751fc138a5807e2624d11bb3b84385f82ed3a53996cc5b31",
    "verified_at": null,
    "source_series": [
      { "key": "big_six_avg_cet1_ratio", "as_of": "2026-01-31" }
    ],
    "staleness": { "status": "current", "age_days": 0 }
  },
  {
    "text": "CPI Inflation rose to 3%. This is notable.",
    "context": "CPI breakdown",
    "line_date": "2026-09-02",
    "checked": true,
    "interpretation_id": 3,
    "list_version": "v1",
    "list_sha256": "02e44d798a7cc107751fc138a5807e2624d11bb3b84385f82ed3a53996cc5b31",
    "verified_at": null,
    "source_series": [
      { "key": "ca_cpi_yoy", "as_of": "2026-07-01" },
      { "key": "cpi_core_trim_yoy", "as_of": "2026-07-01" },
      { "key": "cpi_core_median_yoy", "as_of": "2026-07-01" }
    ],
    "staleness": { "status": "current", "age_days": 0 }
  },
  {
    "text": "GDP Monthly (All Industries) rose by 6,253 to 2,369,309. This still matters.",
    "context": "Monthly GDP",
    "line_date": "2026-09-02",
    "checked": true,
    "interpretation_id": 2,
    "list_version": "v1",
    "list_sha256": "02e44d798a7cc107751fc138a5807e2624d11bb3b84385f82ed3a53996cc5b31",
    "verified_at": null,
    "source_series": [
      { "key": "gdp_monthly", "as_of": "2026-06-01" }
    ],
    "staleness": { "status": "current", "age_days": 0 }
  },
  {
    "text": "CPI Inflation rose to 3%. This remains notable.",
    "context": "Inflation measures",
    "line_date": "2026-09-02",
    "checked": true,
    "interpretation_id": 4,
    "list_version": "v1",
    "list_sha256": "02e44d798a7cc107751fc138a5807e2624d11bb3b84385f82ed3a53996cc5b31",
    "verified_at": null,
    "source_series": [
      { "key": "ca_cpi_yoy", "as_of": "2026-07-01" },
      { "key": "cpi_core_trim_yoy", "as_of": "2026-07-01" },
      { "key": "boc_cpi_common", "as_of": "2026-07-01" },
      { "key": "cpi_ex_food_energy_yoy", "as_of": "2026-07-01" }
    ],
    "staleness": { "status": "current", "age_days": 0 }
  },
  {
    "text": "GDP Monthly (All Industries) rose by 6,253 to 2,369,309. This matters.",
    "context": "Current GDP estimate",
    "line_date": "2026-09-02",
    "checked": true,
    "interpretation_id": 1,
    "list_version": "v1",
    "list_sha256": "02e44d798a7cc107751fc138a5807e2624d11bb3b84385f82ed3a53996cc5b31",
    "verified_at": null,
    "source_series": [
      { "key": "gdp_monthly", "as_of": "2026-06-01" }
    ],
    "staleness": { "status": "current", "age_days": 0 }
  }
  ],
  "omitted_count": 0,
  "note": null
}
POST/v1/transmission

Trace how a shock travels. Given a starting series, this enumerates the causal channels leading out of it — which series are reached, through how many steps, how strong the historical relationship is, and the typical lag in days. Distinct from the counterfactual endpoint: that one propagates a numeric shock and returns magnitudes; this one maps the structure itself.

Request

POST /v1/transmission
{
  "signal": "wti_oil",        // series to trace FROM (alias-aware)
  "max_depth": 3,             // optional, default 3
  "min_strength": 0.0         // optional, drop weak paths
}

Response

{
  "trigger": { "signal_key": "wti_oil", "label": "WTI Crude Oil" },
  "max_depth": 3,
  "min_strength": 0.0,
  "paths": [{
    "signal_key": "cpi_inflation", "label": "CPI Inflation",
    "depth": 2, "order": "2nd order",
    "cumulative_strength": 0.41, "transmission_lag_days": 62,
    "mechanism": "Energy prices feed goods and transport costs...",
    "chain": [
      { "signal_key": "wti_oil", "label": "WTI Crude Oil" },
      { "signal_key": "gas_price_proxy", "label": "Weekly Retail Gas Price — Toronto" },
      { "signal_key": "cpi_inflation", "label": "CPI Inflation" }
    ],
    "current_value": 3.1, "unit": "percent", "value_withheld": null
  }],
  "path_count": 14,
  "truncated": false,
  "as_of": "2026-07-30",
  "computed_at": "2026-07-30T12:00:00Z",
  "data_state": { "availability": "available", "freshness": { "status": "current" }, "quality": { "status": "ok", "flags": [] } },
  "coverage": { "status": "complete", "complete": true, "eligible": 14, "served": 14, "withheld": 0, "missing": 0, "stale": 0, "unknown": 0, "truncated": false },
  "note": "Structural transmission channels with historical strength and typical lag — not a numeric forecast. For a what-if with magnitudes, use the counterfactual endpoint."
}
GET/v1/predictions/track-record

Building track record — not yet validated against naive baselines. Confidence calibration is not validated. Stored confidence is not a validated probability of being right. ODIN’s graded forecasting record for the scoped public set. Rows are written to a ledger with a deadline and graded against what actually happened. Licensing can withhold rows from the listing, and paired or otherwise non-independent rows are disclosed separately rather than blended into published rates. The endpoint applies the same filters as the rows (domain, target, status, date range) applied to the summary, so the rate you read always describes the slice you asked for. Status accepts pending, validated, invalidated, inconclusive, expired or all; all removes the status filter.

Read the split before the headline. Calls that stated a direction are the falsifiable ones and run near a coin flip; calls that a value would stay above or below a level are easier by construction and run far higher. One number covering both overstates what reality has tested, so the payload names them separately — and names the credits deducted for not clearing their own bar rather than quietly folding them in.

The response also lists source_disclosures and keeps paired or otherwise non-independent rows in disclosed_non_independent. Those counts sit next to the rates they affect and are not blended into the headline, directional or held-at-a-level percentages. No percentage is published for a cohort smaller than 30 calls. This page intentionally carries no current track-record figures; read the live track-record endpoint with your API key for the current as-of date, counts and rates.

Response

{
  "forecast_validation": { "status": "building track record", "baseline_status": "not yet validated", "calibration_status": "not yet validated", "note": "Building track record — not yet validated against naive baselines. Stored confidence is not a validated probability.", "assessment_date": "2026-09-26", "assessment_scope": "Council scorecard rows 1 and 2; not a new evaluation of this response cohort.", "correction_label": "Correction: forecast validation disclosure" },
  "as_of": "date from the live response",
  "record_scope": "forward_only",
  "methodology_version": "v2-forward-only",
  "effective_at": "2026-08-04T12:06:55Z",
  "licensing": { "restricted_rows_withheld_from_listing": "integer",
                 "withheld_tallies": { "correct": "integer", "incorrect": "integer",
                                       "pending": "integer", "inconclusive_or_expired": "integer" },
                 "note": "Why these rows are withheld and where they remain counted." },
  "filters": { "domain": null, "target": null, "status": null,
               "date_from": null, "date_to": null },
  "data_state": { "availability": "available, partial or unavailable",
                  "freshness": {}, "quality": {} },
  "coverage": { "status": "complete or partial", "complete": "boolean",
                "eligible": "integer", "served": "integer", "withheld": "integer",
                "missing": "integer", "stale": "integer", "unknown": "integer",
                "truncated": "boolean" },
  "source_disclosures": {
    "belief_engine": {
      "honest_short_form": "Paired band-stability events are not independent forecasts.",
      "replacement_language": "The live response carries the full source-specific disclosure.",
      "counts_toward_directional_proof": false,
      "counts_toward_first_print_proof": false,
      "counts_toward_governed_proof": false
    }
  },
  "disclosed_non_independent": {
    "decided": "excluded decided-row count",
    "correct": "excluded correct-row count",
    "incorrect": "excluded incorrect-row count",
    "directional": { "decided": "integer", "correct": "integer", "incorrect": "integer" },
    "held_at_a_level": { "decided": "integer", "correct": "integer", "incorrect": "integer" },
    "paired_events": "independent paired-event count",
    "derivation": "how the excluded set is chosen — read live from the ledger, never a frozen list",
    "as_of": "timestamp the excluded set was read",
    "sealed_snapshot_decided_2026_08_29": "integer, the sealed reference count",
    "decided_since_the_sealed_snapshot": "integer, how far the live set has moved past it",
    "treatment": "Why these visible rows are excluded from every published rate."
  },
  "summary": {
    "total": "integer", "pending": "integer", "resolved": "integer",
    "decided": "independent decided count", "correct": "independent correct count",
    "corrected_incorrect": "decided minus corrected correct; unavailable when figures are unavailable",
    "decided_including_disclosed_non_independent": "integer — add this to inconclusive and expired to close against resolved",
    "disclosed_non_independent_decided": "integer, the excluded count printed beside the rates",
    "reconciliation": "how the netted decided count closes against total and resolved",
    "exclusion_direction": "whether the exclusion raises, lowers or does not move the published rates",
    "rates_withheld_because_the_exclusion_would_raise_them": "list of rate field names; an exclusion that would RAISE a rate withholds it instead",
    "incorrect": "independent incorrect count", "inconclusive": "integer", "expired": "integer",
    "credited_without_clearing_their_bar": "excluded credit count",
    "credited_on_a_broken_comparison": "excluded credit count",
    "decided_hit_rate_pct": "number when decided is at least 30; otherwise null",
    "directional": {
      "decided": "independent count", "correct": "independent count",
      "hit_rate_pct": "number when decided is at least 30; otherwise null",
      "what_this_is": "calls that stated a direction — the falsifiable ones. Treat this as ODIN's forecasting rate."
    },
    "held_at_a_level": {
      "decided": "independent count", "correct": "independent count",
      "hit_rate_pct": "number when decided is at least 30; otherwise null",
      "what_this_is": "calls that a value would remain above or below a level. Easier by construction; read the directional rate before relying on the headline."
    },
    "record_span_start": "date or null", "record_span_end": "date or null"
  },
  "rows": [{
    "prediction_id": "...", "title": "...", "status": "validated",
    "category": "rates", "direction_called": "down", "confidence": 0.63,
    "target": { "signal_key": "boc_overnight", "label": "BoC Overnight Rate",
                "unit": "percent", "domain": "monetary", "domain_label": "Monetary" },
    "registered_on": "2026-06-02", "deadline": "2026-07-15",
    "resolved_on": "2026-07-15",
    "predicted_value": 2.25, "actual_value": 2.25
  }],
  "row_count": "integer",
  "truncated": "boolean"
}
GET/v1/predictions

The open book — forward-looking calls that have been registered but not yet graded, each with its deadline. Defaults to pending; filter by domain, target or status. Status accepts pending, validated, invalidated, inconclusive, expired or all; all removes the status filter.

Response

{
  "forecast_validation": { "status": "building track record", "baseline_status": "not yet validated", "calibration_status": "not yet validated", "note": "Building track record — not yet validated against naive baselines. Stored confidence is not a validated probability.", "assessment_date": "2026-09-26", "assessment_scope": "Council scorecard rows 1 and 2; not a new evaluation of this response cohort.", "correction_label": "Correction: forecast validation disclosure" },
  "as_of": "2026-08-12",
  "record_scope": "forward_only",
  "methodology_version": "v2-forward-only",
  "effective_at": "2026-08-04T12:06:55Z",
  "filters": { "domain": null, "target": null, "status": "pending" },
  "data_state": { "availability": "partial", "freshness": { "status": "current", "computed_at": "2026-08-23T06:00:00Z", "expected_frequency": "event_driven" }, "quality": { "status": "caution", "flags": ["insufficient_coverage"] } },
  "coverage": { "status": "partial", "complete": false, "eligible": 13, "served": 1, "withheld": 12, "missing": 0, "stale": 0, "unknown": 0, "truncated": false },
  "licensing": { "restricted_rows_withheld_from_listing": 12, "note": "Some calls target data series whose licenses do not permit redistribution; those rows are withheld from this listing. Track-record rates on this API count them." },
  "rows": [{
    "prediction_id": "...", "title": "...", "status": "pending",
    "category": "rates", "direction_called": "down",
    "predicted_value": 2.5, "confidence": 0.63,
    "target": {
      "signal_key": "boc_overnight", "label": "BoC Overnight Rate",
      "unit": "percent", "domain": "monetary", "domain_label": "Monetary"
    },
    "registered_on": "2026-07-21", "deadline": "2026-09-02"
  }],
  "row_count": 1,
  "truncated": false,
  "boc_next_decision_call": {
    "status": "abstain",
    "reason": "No rate-decision call is currently on the record..."
  }
}

Bank of Canada rate context

When ODIN has the rate-context block switched on, the response also carries boc_rate_context: Bank of Canada rate context for rate widgets. It is additive. boc_next_decision_call and every field shown above keep their shape, and a parser that ignores unknown keys needs no change. The block is not an ODIN call: is_odin_call is always false, boc_next_decision_call keeps abstaining, and the block contains no meeting probabilities at any horizon, no most_likely and no cut/hold/hike probability map. Replace an abstention message with its suggested_copy.

Each section has its own status: available, partial, unavailable (with a reason_code) or disabled. Show a section only when it is available or partial; one missing input never blanks the others. Branch on status, not on the reason code, which can gain new values. Each rate, yield, survey figure, implied change, dead band, meeting count, hit rate and Brier score is an object with value, unit and provenance. Two kinds of number sit beside a value instead of in their own object, and share that score's provenance: in the scorecard, hits and meetings are bare integers next to the value of a hit_rate or brier, and each confidence_interval carries bare level, lower, upper and n. The provenance names source, series_id and observed_date, and may add source_url, retrieved_on and terms_url. Show nothing for a missing value, and never show zero in its place.

A section that is not available carries only the fields shown in the second example below: its label and provenance are absent and its data fields are empty. market_implied_3m is the exception: it keeps every field in every state other than a build failure, with no display or change_bp unless it is available. If a section fails to build, it is exactly status unavailable and reason_code section_build_failed, with no other field.

market_implied_3m is ODIN's cash-market estimate from Bank of Canada T-bill and CORRA data: not OIS or futures pricing, and not an ODIN forecast. Render display as it is; it is the number shown under the display rules below, and no change_bp number is given when display reads "no clear move priced". The figure is a total for the window ending on window_end_date; the block gives no per-meeting split, because the timing between the Bank's meetings in that window is not reliable. Always show label, which says this in plain words for the current reading, or a shortened form that keeps "(ODIN cash-market estimate; not OIS or futures pricing)", and keep disclaimers and attribution with the figure. The Bank of Canada attribution, including its no-endorsement line, is a condition of the Bank's terms. Show bill_spread_note with the market reading whenever it is present: it reads "Most of this reading comes from the 6-month T-bill: with the 6-month yield equal to the 3-month yield, the estimate would be less than half the size of the number shown. Part of the gap between the two yields can be bill supply or term premium rather than expected rate moves." when the 6-month bill drives the reading, and otherwise, for an unusual spread, "The 6-month T-bill yield is unusually far above the 3-month yield (the gap shown is more than 15 bp). Part of that gap can be bill supply or term premium rather than expected rate moves."

Display rules for market_implied_3m, stated once in the code (RATE_CONTEXT_DISPLAY_RULES) and repeated here verbatim: No number is shown when the unrounded estimate is under 10 bp in size; the display then reads 'no clear move priced'. The estimate is computed exactly from the three quotes, each taken to 4 decimal places, half away from zero. A shown number is the estimate rounded to the nearest 5 bp, and an exact half (such as 12.5 or 17.5) rounds away from zero. six_month_bill_driven is true only when a number is shown and the same estimate with the 6-month yield set equal to the 3-month yield, rounded to a whole basis point, is zero or has the same sign as the number shown and is less than half its size. bill_spread_unusual is true when the shown 6M-3M spread, bill_spread_6m_3m.value, is above 15 bp. Without a reading (status disabled or unavailable), label is the fixed no-reading text; a section that failed to build carries only status unavailable and reason_code section_build_failed, with no label.

survey_expected_path gives the 25th percentile, median and 75th percentile of responses to question 2.1 of the Bank of Canada Market Participants Survey, by horizon. These are quartiles of respondents' views, not probabilities and not an ODIN forecast. Show edition_label, which names the survey quarter and its publication date, so readers can see how old the figures are. Show only horizons where horizon_passed is false. The survey is quarterly; the section reads unavailable once the most recent edition ODIN has recorded was published more than 120 days before as_of, and partial when that edition left some horizons blank.

decision_scorecard grades ODIN's internal (unpublished) Bank of Canada decision calls, frozen before each decision and graded on the announced outcome. It carries only past grades, never an open call. live_record and backtest_record are separate: never add them together, and label any backtest figure as backtest with its disclosure. Each hit_rate and brier carries a 95% confidence_interval and n, the number of graded meetings behind it. A record with fewer than 8 graded meetings has small_sample true and a sample_notice, such as "Only 1 graded meeting, fewer than 8 (one year of Bank of Canada decisions). That is too few to judge skill: show the 95% interval beside any figure, or do not show the figure." When small_sample is true, show that notice, and show the interval beside any hit rate or Brier score, for example "100% (95% interval 21%–100%, 1 meeting)". When an interval has no bounds (its reason_code says why), do not show that figure. Show the persistence baseline beside any hit rate. With no graded meeting the notice reads "No graded meetings yet, so there is no hit rate or Brier score to show."

us_context gives the FOMC target range, the effective federal funds rate and the U.S. Treasury 2-year and 10-year par yields, read only from the Federal Reserve Board, the New York Fed and the U.S. Treasury. When effr is present, show attribution_notice. ODIN does not yet run a scheduled job for these direct sources, so expect this section to read unavailable.

{
  "boc_rate_context": {
    "schema_version": "v1",
    "as_of": "date, the Toronto calendar day the block was assembled",
    "is_odin_call": false,
    "note": "Context only. ODIN does not publish its own Bank of Canada rate call yet, and nothing in this block is an ODIN forecast. No meeting probabilities are published here.",
    "suggested_copy": "ODIN doesn't publish its own call yet; here's what markets and the Bank's survey show.",
    "policy_rate": {
      "status": "available or unavailable",
      "reason_code": "string, present when unavailable",
      "label": "Bank of Canada target for the overnight rate",
      "rate": { "value": "number", "unit": "percent", "provenance": "object" }
    },
    "next_decision": {
      "status": "available or unavailable",
      "reason_code": "string, present when unavailable",
      "label": "Next scheduled Bank of Canada rate announcement",
      "date": "date",
      "provenance": "object"
    },
    "market_implied_3m": {
      "status": "available, unavailable or disabled; change_bp is null unless available and outside the dead band",
      "reason_code": "string or null",
      "is_odin_forecast": false,
      "headline": "What money markets imply for the next ~3 months",
      "label": "text: a plain sentence for this reading, such as Cash markets price about a quarter-point hike by late December, driven mostly by the 6-month T-bill. The timing between the Oct 28 and Dec 9 meetings is not reliable. (ODIN cash-market estimate; not OIS or futures pricing)",
      "estimate_label": "ODIN cash-market estimate from Bank of Canada T-bill and CORRA data; not OIS or futures pricing, and not an ODIN forecast",
      "display": "text such as +30 bp, or no clear move priced; null unless status is available",
      "change_bp": { "value": "integer, rounded to the nearest 5", "unit": "basis_points", "provenance": "object" },
      "dead_band": {
        "value": 10,
        "unit": "basis_points",
        "text": "no clear move priced",
        "rule": "The estimate is computed exactly from the three quotes, each taken to 4 decimal places, half away from zero. A shown number is the estimate rounded to the nearest 5 bp, and an exact half (such as 12.5 or 17.5) rounds away from zero. No number is shown when the unrounded estimate is under 10 bp in size; the display then reads 'no clear move priced'.",
        "provenance": "object"
      },
      "six_month_bill_driven": "boolean: true only when a number is shown and, on the numbers shown, the 6-month T-bill accounts for more than half of it; null unless status is available",
      "bill_spread_unusual": "boolean: true when the shown 6M-3M spread, bill_spread_6m_3m.value, is above 15 bp; null unless status is available",
      "bill_spread_note": "text matching the two flags above; null when both are false or status is not available",
      "bill_spread_6m_3m": { "value": "number, 6-month minus 3-month T-bill yield", "unit": "basis_points", "provenance": "object" },
      "window_end_date": "date the ~3-month window ends (quote_date plus 90 days); null unless status is available",
      "quote_date": "date or null",
      "next_meeting_date": "date or null",
      "note": "emergency moves out of scope; not an ODIN forecast",
      "known_tilt": "text: the estimate's known historical tilt, which the figure is not adjusted for",
      "disclaimers": ["text"],
      "attribution": {
        "source": "Bank of Canada",
        "licence_url": "https://www.bankofcanada.ca/terms/",
        "paid_service_notice": "Source data are available free of charge from the Bank of Canada website.",
        "changes": "ODIN parses the survey and derives the market approximation.",
        "endorsement": "The Bank of Canada does not endorse ODIN or these calculations."
      }
    },
    "survey_expected_path": {
      "status": "available, partial or unavailable",
      "reason_code": "string, present when unavailable",
      "survey": "text, the survey edition's title",
      "survey_quarter": "text, the edition's quarter, such as Q2 2026",
      "edition_label": "text naming the quarter and publication date, such as Q2 2026 Market Participants Survey, published July 27, 2026. Responses were collected before publication, so they do not reflect market moves since then.",
      "fieldwork": "text, when the survey was conducted",
      "published_on": "date the Bank published the edition",
      "basis": "25th percentile, median and 75th percentile of survey responses. These describe respondents' views; they are not probabilities and not an ODIN forecast.",
      "missing_horizons": { "value": "integer, horizons the edition left blank", "unit": "horizons", "provenance": "object" },
      "path": [{
        "horizon": "YYYY-MM or YYYY-Qn",
        "horizon_type": "month or quarter",
        "horizon_passed": "boolean, true once that month or quarter is over",
        "p25": { "value": "number", "unit": "percent", "provenance": "object" },
        "median": { "value": "number", "unit": "percent", "provenance": "object" },
        "p75": { "value": "number", "unit": "percent", "provenance": "object" },
        "responses": { "value": "integer", "unit": "respondents", "provenance": "object" }
      }],
      "attribution": {
        "source": "Bank of Canada",
        "licence_url": "https://www.bankofcanada.ca/terms/",
        "paid_service_notice": "Source data are available free of charge from the Bank of Canada website.",
        "changes": "ODIN parses the survey and derives the market approximation.",
        "endorsement": "The Bank of Canada does not endorse ODIN or these calculations."
      }
    },
    "decision_scorecard": {
      "status": "available, unavailable or disabled",
      "reason_code": "string, present when not available",
      "label": "Bank of Canada decision scorecard (cut / hold / hike)",
      "live_record": {
        "record_type": "LIVE",
        "label": "Live forward record",
        "disclosure": "Grades ODIN's internal (unpublished) BoC decision calls, frozen before each decision and graded on the announced outcome. These are past calls scored after the fact, not a forecast.",
        "meetings_graded": { "value": "integer", "unit": "meetings", "provenance": "object" },
        "small_sample": "boolean, true when fewer than 8 meetings are graded",
        "sample_notice": "text when small_sample is true; otherwise null",
        "hit_rate": {
          "value": "number from 0 to 1; null when no meeting is graded",
          "unit": "fraction",
          "hits": "integer",
          "meetings": "integer",
          "confidence_interval": {
            "level": 0.95,
            "lower": "number or null",
            "upper": "number or null",
            "method": "Wilson score interval; null with reason_code interval_not_computed",
            "n": "integer, graded meetings behind the score",
            "reason_code": "present only when there are no bounds: no_graded_meetings, at_least_two_meetings_required or interval_not_computed"
          },
          "provenance": "object"
        },
        "brier": {
          "value": "number from 0 to 2, lower is better; null when no meeting is graded",
          "unit": "brier_score_lower_is_better",
          "meetings": "integer",
          "confidence_interval": "same fields as hit_rate.confidence_interval; method is normal approximation for a mean",
          "provenance": "object"
        },
        "persistence_baseline": { "hit_rate": "same fields as hit_rate", "brier": "same fields as brier" },
        "base_rate_baseline": { "hit_rate": "same fields as hit_rate", "brier": "same fields as brier" }
      },
      "backtest_record": "same fields as live_record, with record_type BACKTEST, label BACKTEST (retrospective replay, not live) and its own disclosure",
      "separation_notice": "Live and BACKTEST records are intentionally not combined."
    },
    "us_context": {
      "status": "available, partial or unavailable; when partial, each missing one of fomc_target_range, effr, treasury_2y and treasury_10y is null",
      "reason_code": "string, present when unavailable",
      "fomc_target_range": {
        "label": "FOMC target range for the federal funds rate",
        "decision_date": "date",
        "lower": { "value": "number", "unit": "percent", "provenance": "object" },
        "upper": { "value": "number", "unit": "percent", "provenance": "object" }
      },
      "effr": { "value": "number", "unit": "percent", "label": "Effective federal funds rate", "provenance": "object" },
      "treasury_2y": { "value": "number", "unit": "percent", "label": "U.S. Treasury 2-year par yield", "provenance": "object" },
      "treasury_10y": { "value": "number", "unit": "percent", "label": "U.S. Treasury 10-year par yield", "provenance": "object" },
      "attribution_notice": "text required by the New York Fed's terms when effr is present; otherwise null"
    }
  }
}

Sections that are not available (reason codes vary; these are examples)

{
  "policy_rate": { "status": "unavailable", "reason_code": "no_daily_target_rate", "rate": null },
  "next_decision": { "status": "unavailable", "reason_code": "no_published_next_decision", "date": null },
  "survey_expected_path": { "status": "unavailable", "reason_code": "no_recorded_survey", "path": [] },
  "decision_scorecard": { "status": "disabled", "reason_code": "scorecard_not_enabled" },
  "us_context": { "status": "unavailable", "reason_code": "no_direct_producer_rows",
                  "fomc_target_range": null, "effr": null, "treasury_2y": null, "treasury_10y": null }
}
GET/v1/usage

Your own consumption read-back — requests and spend against the limits below. Call it as often as you like; it costs a read.

Response

{
  "organization": "Your Organization",
  "period": { "day": "2026-08-03", "month": "2026-08" },
  "requests": { "day": 18, "day_reason": 3, "day_reason_quota": 50, "month": 241 },
  "tokens": { "month_reason": 184220, "month_quota": 5000000 },
  "estimated_cost_usd": { "day": 0.42, "month": 8.17, "day_ceiling": 5.0 },
  "warnings": [],
  "resets": { "day_seconds": 12840, "month_seconds": 2430040 }
}

Errors

Errors return the matching HTTP status and a stable envelope. The code is the field to branch on; the message is plain English and safe to surface; request_id can be quoted to support.

{
  "error": {
    "code": "quota_exceeded",
    "message": "Daily request quota reached. Resets at 00:00 UTC.",
    "request_id": "req_..."
  }
}

During the final 14 days before a key expires, every authenticated response carries X-Key-Expires-In-Days and a plain-English X-Key-Expiry-Warning naming the exact expiry time and the rotation remedy. Expiry does not change: at that instant the response is the distinct 401 body below. Rotate the key in Settings → API keys, then update the integration with the newly issued key.

{
  "error": {
    "code": "key_expired",
    "message": "This API key expired on 2026-08-18T19:34:00Z. Rotate it in the app, then update this integration with the new key.",
    "request_id": "req_..."
  }
}
CodeHTTPMeaning
unauthorized401Missing or invalid key.
key_revoked401Key has been revoked.
key_expired401Key reached its stated expiry; rotate it in the app and update the integration.
org_not_enabled403Organization not enabled for API access.
scope_missing403Key lacks the scope for this endpoint.
licensing_restricted403Series is under a restrictive data license.
not_found404Unknown identifier.
signal_retired410Series has been retired.
invalid_request405 / 422Unsupported HTTP method or malformed request body.
personal_data_rejected422Obvious personal identifiers are not accepted by the reasoning boundary.
rate_limited429Per-minute burst exceeded.
trial_exhausted429Every question in a trial bundle has been used.
trial_expired429The trial bundle’s window has ended.
quota_exceeded429Daily request or monthly token quota reached.
metering_unavailable503Usage metering could not safely admit or settle the request; retry after the stated delay.
database_busy503Database capacity is temporarily occupied; retry after the stated delay.
cost_ceiling429Daily spend circuit breaker tripped.
client_disconnected499Preview stream cancelled; observed usage is recorded. No final answer is delivered.
reasoning_timeout504Question needed more time than allowed — retry or raise max_seconds.
not_computed503Underlying value not yet computed.
internal_error500Unexpected error — quote the request id.

Rate limits & quotas

Daily and monthly quotas are enforced per organization. Per-minute burst controls are currently worker-local and therefore best-effort across a multi-worker deployment; an organization-global burst contract is still in development. The reasoning endpoint is the token-heavy one and has its own tighter budget; the read endpoints are generous. Rate headers are outcome-scoped, not universal: authentication and organization failures, missing-scope failures, request validation errors, unknown paths or methods, and failures before quota evaluation do not carry guaranteed rate headers.

Once an authenticated, in-scope request completes daily quota evaluation, its response carries X-RateLimit-Limit-Day, X-RateLimit-Remaining-Day, and X-RateLimit-Reset-Day, including later handler errors. A burst block carries the matching -Minute headers and Retry-After. A daily quota, monthly-token, or cost block carries the day headers and Retry-After. Remaining values are ledger snapshots, not concurrency reservations.

A trial key is metered as a bundle of questions rather than a daily allowance. Once quota evaluation completes it carries X-Questions-Limit, X-Questions-Remaining and X-Questions-Reset — the moment the bundle ends, as a UTC timestamp. A spent bundle returns trial_exhausted and a bundle past its window returns trial_expired, both 429 and both with the headers above. Once any allowance passes 80% — daily requests, monthly reasoning tokens, or trial questions — the response also carries X-Usage-Warning, a semicolon-separated list of the allowances that crossed it. It is advisory: nothing is refused because of it.

The outcome ledger still records every attributed attempt for operations and support. Daily request quota counts successful calls, deliberate authorization or safety-policy rejections. It does not count 429 burst, quota, or cost-ceiling denials, invalid_request validation failures, key_expired failures, or any 5xx response. This keeps ODIN’s contract and server failures off a partner’s allowance without making valid-but-forbidden traffic free or charging rejected work.

OutcomeGuaranteed rate headers
Quota evaluation completedX-RateLimit-Limit-Day, X-RateLimit-Remaining-Day, X-RateLimit-Reset-Day
Burst 429X-RateLimit-Limit-Minute, X-RateLimit-Remaining-Minute, X-RateLimit-Reset-Minute, Retry-After
Quota or cost 429Day headers above, plus Retry-After
Trial-bundle key, once quota evaluation completesX-Questions-Limit, X-Questions-Remaining, X-Questions-Reset, alongside the day headers
Any of the above, once a soft threshold is passedX-Usage-Warning
Pre-auth, scope, validation, unknown route/method, pre-evaluation failureNone guaranteed
ClassBurstDaily
reason5 / minPer-key request quota (default 50) + monthly token budget + daily spend ceiling
shared simulation pool: counterfactual + transmission10 / min combined200 / day combined
reads (health-index, regime, signals, policy-rates, predictions, predictions/track-record, ping, usage)30 / min2,000 / day per class

Counterfactual and transmission calls consume the same shared pool: every call to either endpoint counts toward the combined 200-per-day organization limit. Call GET /v1/usage any time for your own consumption read-back.

Privacy & retention

The declared retention period for api_v1_requests metadata is 24 months. The ledger records organization/key attribution, endpoint class, outcome, latency and usage. It has no request-body or response-body column; for /v1/reason, it stores only the question length and a SHA-256 fingerprint, not the question text.

Current enforcement status: not active. The ledger is append-only and database triggers block updates and physical removal. A default-off monthly scheduler stub performs no database work and removes no rows. The operator must separately approve an auditable, append-only-compatible expiry mechanism before the 24-month policy is physically enforced; until then, rows are retained beyond the policy target.

MCP server

ODIN’s native Model Context Protocol connection is live for organizations with API access enabled. Point your client at https://api.odinos.ca/mcp/v1 with the same bearer key — no separate credential, nothing extra to integrate. It speaks JSON-RPC 2.0 (protocol 2025-06-18, stateless). For a staged self-serve key, tools/list discovers four tools: odin_economy_health, odin_regime, odin_counterfactual, and odin_signal. A controlled-partner key with the separately granted reason scope also discovers odin_reason. The list always reflects the scopes on your key.

POST /mcp/v1
{
  "jsonrpc": "2.0", "id": 1, "method": "tools/call",
  "params": {
    "name": "odin_economy_health",
    "arguments": {}
  }
}

Embed the conditions widget (free)

A live, ODIN-branded snapshot of Canadian economic conditions — the composite health score, the conditions currently prevailing, and key indicators — that any site can drop in with one iframe. No key, no signup; the widget fetches fresh data itself. Append ?theme=dark for a dark version.

Snippet

<iframe src="https://app.odinos.ca/embed/conditions" width="380" height="460" frameborder="0" title="ODIN — Canadian economic conditions"></iframe>

Keyed widget embeds are coming.

A note on what you get back.

ODIN answers in plain English and shows its work — when the evidence isn’t there, it says so rather than fabricating. Where a figure comes from outside ODIN’s own data, the answer carries the source it was retrieved from.

Interpretation is what this engine is for: reading conditions, tracing transmission, and telling you which channels are live. Its forward-looking calls are published and graded in the open — pull /v1/predictions/track-record and judge them yourself before you lean on them. Output is informational, not investment advice.