# Sedona Strata API: the documentation as one text file The same pages, for a person, are at https://sedonalabs.si/docs. This file is for a script or an AI assistant, which cannot run the site's pages. - Base address: https://sedonalabs.si/api/public/strata/v1 - Authentication: send the key in the X-API-Key header. Every endpoint is a GET request and answers JSON. The API is read-only. - OpenAPI 3 description, no key needed: https://sedonalabs.si/api/public/strata/v1/openapi.json - Every dataset a key can read, as JSON, no key needed: https://sedonalabs.si/api/public/strata/reference - One dataset in full (what its times mean, its entities and events, every metric with its meaning and unit, its limits), as JSON, no key needed: https://sedonalabs.si/api/public/strata/reference/ - With a key, GET /datasets and GET /datasets/{id} return the same descriptions for the datasets that key can read. Links below that start with /docs are pages of https://sedonalabs.si. --- # Sedona Strata API Strata is Sedona's data API. It serves two kinds of data in one shape: the public record of sports and other events, and what Sedona's models said about each game, saved at the moment they said it. Every value carries the time it became known. You can ask for the data as it stood at any past moment, so a test on history sees only what was knowable then. ## What you can read | Kind | What it is | Who can read it | | --- | --- | --- | | **Signals** | Sedona's model outputs: each game simulated before it starts, each player's projection before the game, the chance of over at NBA prop lines, second-half projections, the NBA season played out, and Sedona's pick for each game with how it turned out. Each is stamped with when it was made. The game forecasts and the picks are stored beside the final result. | Partner keys | | **The record** | Results and official figures from public sources: baseball, soccer and cricket results, daily weather, US inflation and jobs reports, Federal Reserve decisions and rates, and US federal elections. | Every key | [Every dataset and metric](/docs/datasets) lists each one with its meaning, unit and coverage. ## The address All requests are HTTPS `GET` requests to one base address. Answers are JSON. ```text https://sedonalabs.si/api/public/strata/v1 ``` ## Your first request ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/datasets" ``` The answer lists the datasets your key can read. The [quickstart](/docs/quickstart) goes from there to a point-in-time read in five steps. ## How the pages are laid out - **Get started**: [quickstart](/docs/quickstart), [authentication](/docs/authentication), and [what each access level can read](/docs/access). - **Concepts**: [the data model](/docs/data-model) (datasets, entities, events, observations) and [point in time](/docs/point-in-time) (`ts`, `known_at`, vintages, `as_of`). - **Using the API**: [following updates](/docs/updates) as the models work, [pagination](/docs/pagination), [errors](/docs/errors), [rate limits](/docs/rate-limits) and [versioning](/docs/versioning). - **API reference**: [every endpoint](/docs/api) with its parameters and an example answer, including a [one-file download](/docs/api/export) and a [stream](/docs/api/stream). - **Datasets**: [one page per dataset](/docs/datasets). - **Guides**: [recipes](/docs/recipes) for common jobs and [a small Python client](/docs/python). A machine-readable description of the API (OpenAPI 3) is at `/openapi.json` under the base address, with a page to try calls at `/docs`. ## For a script or an AI assistant These pages need a browser. A program that fetches them gets an empty frame, so there are three plain addresses made for programs, none of which needs a key: | What | Address | | --- | --- | | Every guide and endpoint page, as one text file | `https://sedonalabs.si/docs/llms.txt` | | Every dataset a key can read, as JSON | `https://sedonalabs.si/api/public/strata/reference` | | One dataset in full, as JSON | `https://sedonalabs.si/api/public/strata/reference/{dataset id}` | To use the API from an AI assistant that can run code, give it your key and the first address, and ask for what you need. --- # Quickstart Five requests take you from a new key to a point-in-time read. Each example is a `curl` command; the same calls in Python are at the end. Put your key in an environment variable first, so it never appears in a command you paste somewhere: ```bash export STRATA_API_KEY="sk_strata_..." export STRATA="https://sedonalabs.si/api/public/strata/v1" ``` ## 1. Check the key ```bash curl -H "X-API-Key: $STRATA_API_KEY" "$STRATA/" ``` ```json { "service": "Sedona Strata", "version": "v1", "scope": "partner", "requests_per_minute": 60, "endpoints": ["/datasets", "/datasets/{id}", "/entities", "/events", "/observations", "/status"], "docs": "https://sedonalabs.si/docs", "openapi": "/api/public/strata/v1/openapi.json" } ``` `scope` is your access level: `partner` reads Signals and the record, `external` reads the record. See [access levels](/docs/access). ## 2. List the datasets ```bash curl -H "X-API-Key: $STRATA_API_KEY" "$STRATA/datasets" ``` Each item has the dataset's `id`, a description, how many values and events it holds, and the first and last time it covers. The `id` is what every other request takes as `dataset`. ## 3. Describe one dataset ```bash curl -H "X-API-Key: $STRATA_API_KEY" "$STRATA/datasets/signals.sim.game" ``` The answer explains what `ts` and `known_at` mean for this dataset, lists its entity kinds, and lists every metric with its meaning and unit. The same text is on the dataset's page here: [signals.sim.game](/docs/datasets/signals.sim.game). ## 4. Read values Read the home side's win chance for every game that starts on November 15, 2026 (UTC): ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "$STRATA/observations?dataset=signals.sim.game&metric=p_home_win&from=2026-11-15&to=2026-11-16" ``` ```json { "data": [ { "ts": "2026-11-15T21:25:00+00:00", "entity": {"kind": "team", "key": "nfl:DAL", "name": "Dallas Cowboys"}, "metric": "p_home_win", "value": 0.4091, "value_text": null, "unit": "probability", "vintage": 4, "known_at": "2026-11-14T12:37:02.558000+00:00", "loaded_at": "2026-11-14T13:58:27.630539+00:00", "event": {"domain": "nfl", "kind": "game", "key": "2026_10_SF_DAL"} } ], "has_more": false, "next": null, "as_of": null, "loaded_after": null, "vintage": "latest", "licences": {"signals.sim.game": {"licence": "model", "attribution": null}} } ``` One row is one value: one metric, for one entity, at one time `ts`. Here `ts` is the game's start, `known_at` is when this forecast was made, and `vintage` 4 means it is the fifth value held for this game. The answer is cut to one game, as read on November 14, 2026. A game's forecast is stored under its home team. ## 5. Read it as it stood at a past moment Add `as_of`. The answer holds only what was known at that moment: for each game, the newest forecast made by then. ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "$STRATA/observations?dataset=signals.sim.game&metric=p_home_win&from=2026-11-15&to=2026-11-16&as_of=2026-11-13T12:00:00Z" ``` A game whose first forecast was made after `as_of` is not in the answer. [Point in time](/docs/point-in-time) explains `as_of`, `known_at` and vintages in full. ## The same in Python ```python import os import requests BASE = "https://sedonalabs.si/api/public/strata/v1" http = requests.Session() http.headers["X-API-Key"] = os.environ["STRATA_API_KEY"] def get(path, **params): r = http.get(BASE + path, params=params, timeout=60) if r.status_code != 200: err = r.json()["error"] raise RuntimeError(f"{r.status_code} {err['code']}: {err['message']} ({r.headers['Request-Id']})") return r.json() print(get("/")["scope"]) for d in get("/datasets")["data"]: print(d["id"], d["observations"]) page = get("/observations", dataset="signals.sim.game", metric="p_home_win", **{"from": "2026-11-15", "to": "2026-11-16"}, as_of="2026-11-13T12:00:00Z") for row in page["data"]: print(row["ts"], row["entity"]["key"], row["value"], row["known_at"]) ``` `from` is a reserved word in Python, which is why it is passed through a dictionary. [A Python client](/docs/python) wraps paging, rate limits and errors. ## Next - Read more than one page: [pagination](/docs/pagination). - Keep pulling what is new as the models update: [following updates](/docs/updates). - Handle failures: [errors](/docs/errors) and [rate limits](/docs/rate-limits). - Find the dataset you need: [every dataset and metric](/docs/datasets). --- # Authentication Every request needs an API key. A key looks like `sk_strata_` followed by 43 letters, digits, `-` and `_`. ## Send the key Send it in the `X-API-Key` header: ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/datasets" ``` Or as a bearer token, if your HTTP client is built around that: ```bash curl -H "Authorization: Bearer $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/datasets" ``` The key is never accepted in the address. A key in a query string ends up in logs and browser history. ## What a key carries | Property | What it is | | --- | --- | | Access level | `external` or `partner`. It decides which datasets exist for the key. See [access levels](/docs/access). | | Rate limit | A number of requests a minute, 60 unless agreed otherwise. See [rate limits](/docs/rate-limits). | `GET /` returns both for the key you send. ## Keep it secret - Call the API from your servers, never from a web page or an app a user can open: anyone who can see the key can use it. - Keep it in an environment variable or a secrets store, not in source code. - Sedona stores only a hash of your key. If you lose it, we cannot show it to you again: we make a new one. ## Replace or revoke a key Ask your Sedona contact. A new key works at once; the old one stops working the moment it is revoked. To change keys without a gap, ask for the new key first, move your servers to it, then ask for the old one to be revoked. ## When authentication fails | Status | `code` | What happened | What to do | | --- | --- | --- | --- | | 401 | `missing_api_key` | No key was sent. | Send the `X-API-Key` header. | | 401 | `invalid_api_key` | The key is unknown or was revoked. | Check for a truncated or mistyped key; ask for a new one if it was revoked. | | 404 | `unknown_dataset` | The dataset does not exist for this key's access level. | `GET /datasets` lists what the key can read. | A dataset your key cannot read answers exactly as one that does not exist. The shape of an error is described in [errors](/docs/errors). --- # Access levels and licences A key has one access level. The level decides which datasets exist for it. Every dataset has one licence class, which says where the data comes from and travels with every answer. ## Access levels | Level | Reads | | --- | --- | | `external` | The record: datasets in the public domain or under an open licence. | | `partner` | Everything `external` reads, and Signals: Sedona's model outputs. | `GET /` returns your level as `scope`. [Every dataset and metric](/docs/datasets) marks which level reads each dataset. Sedona's play-by-play metrics for the NBA, NFL and college football are not served by the API at either level yet. ## Licence classes | `licence` | What it means | | --- | --- | | `public_domain` | Compiled from records in the public domain, such as US government publications. | | `open` | Compiled from a source that publishes under an open licence. The licence asks for attribution: show the dataset's `attribution` text where you show the data. | | `model` | Produced by Sedona's own models. Read with a partner key, under your agreement with Sedona. | Every dataset a key can read today has one of these three classes. Two more are defined for datasets that are not served yet: `derived`, for data Sedona builds from public-domain or open sources, and `own`, for data Sedona collects itself. A key with `external` access will read those too when they are served. ## Licences in every answer Every list answer ends with a `licences` object: one entry per dataset in the answer, with its licence class and its attribution text when it has one. ```json { "data": ["..."], "licences": { "mlb.games": { "licence": "open", "attribution": "The information used here was obtained free of charge from and is copyrighted by Retrosheet. Interested parties may contact Retrosheet at www.retrosheet.org." } } } ``` If you store or forward the data, keep the attribution with it. `GET /datasets` also returns each dataset's source name and address. ## What you may do with the data For `public_domain` and `open` datasets, the source's own terms apply, and for `open` datasets that includes showing the attribution. For `model` datasets, your agreement with Sedona says how you may use them and whether you may show them to your own customers. If your agreement does not answer a question, ask your Sedona contact before you build on an assumption. --- # The data model Everything in Strata fits four things: a **dataset** holds **observations**, each observation is a value about an **entity**, and many observations belong to an **event**. Every dataset uses the same shape, so one piece of client code reads all of them. ## Datasets A dataset is one body of data from one source, such as `econ.cpi` (US consumer prices) or `signals.sim.game` (Sedona's forecast of each game). Its `id` has two or three parts separated by dots: the area, then the subject. `GET /datasets` lists the ones your key can read. `GET /datasets/{id}` describes one in full. ## Entities An entity is what a value is about: a team, a player, a weather station, an economic indicator, a candidacy. | Field | What it is | | --- | --- | | `kind` | The type of entity: `team`, `player`, `station`, `indicator`, and so on. | | `key` | Its identifier, unique within its kind. This is what you pass as `entity` to filter. | | `name` | A name to show. Where no name is on file it is a short code or an id; the dataset's page says so. | | `attrs` | Extra facts about the entity, different for each kind. May be empty. | `GET /entities?dataset=...` lists a dataset's entities. ## Events An event is something that happens at a time and that values attach to: a game, a match, a data release, a Federal Reserve meeting, an election race, a station's day. | Field | What it is | | --- | --- | | `domain` | The area the event belongs to, such as `nba`, `nfl`, `econ`. | | `kind` | The type of event: `game`, `match`, `release`, `meeting`, `race`, `day`, `season`. | | `key` | Its identifier, unique within its domain and kind. | | `title` | A readable name, such as `SF @ DAL` or `CPI for January 1994`. | | `starts_at` | When it starts, or for a release, when it was published. | | `ends_at` | When it ends. Empty for most datasets today. | | `status` | Where it stands: for example `scheduled`, `final`, `released`, `observed`. The dataset's page lists the values it uses. | | `attrs` | Extra facts about the event, different for each kind. | | `entities` | The entities that take part, each with a `role` such as `home` or `away`. | `GET /events?dataset=...` lists a dataset's events. ## Observations An observation is one value. It is the unit everything else exists to describe. | Field | What it is | | --- | --- | | `ts` | The time the value is about: a game's start, the month an index describes, the day a temperature was measured. Each dataset's page says exactly what its `ts` is. | | `entity` | Who or what the value is about: `kind`, `key` and `name`. | | `metric` | What is measured, such as `p_home_win` or `tmax`. Names are lower case with underscores. | | `value` | The number. Empty when the value is text. | | `value_text` | The value as text, for values that are not numbers (a decision, a name, a list). Some datasets also use it for a note beside a number. | | `unit` | The unit of `value`, such as `probability`, `points`, `percent`. | | `vintage` | Which revision this is: 0 for the first value Strata held, then 1, 2, and so on. | | `known_at` | When this value became known. | | `loaded_at` | When the load that brought this value into Strata began. See [following updates](/docs/updates). | | `event` | The event the value belongs to (`domain`, `kind`, `key`), or empty. | A row is identified by its dataset, entity, metric, `ts` and vintage. `GET /observations?dataset=...` reads them. ### Probabilities A `value` with the unit `probability` runs from 0 to 1, so 0.62 is 62%. Every probability in a Signals dataset is the model's estimate. ### Metric names that carry a parameter Some metric names are built from parts, for example `p_over_pts_20.5`: the chance a player scores more than 20.5 points. The dataset's page lists each such family and the values its parts take. Filter with the full name. ### Times All times are ISO 8601 in UTC, such as `2026-11-15T18:00:00+00:00`. In a request you may write `2026-11-15T18:00:00Z`, or a date alone, `2026-11-15`, which means midnight UTC. ## How the four fit together For a game forecast in `signals.sim.game`: - the **event** is the game (`SF @ DAL`), with the two teams as its entities in the roles `home` and `away`; - each **observation** is one number of the forecast (`p_home_win`, `margin_median`, and so on), attached to the home team as its entity and to the game as its event; - `ts` is the game's start, `known_at` is when the forecast was made, and each new forecast of the same game is a new `vintage`. For US consumer prices in `econ.cpi`: - the **entity** is an indicator (one published price series); - the **event** is the monthly release; - `ts` is the month the index describes, `known_at` is the moment the figure was published, and a later revision of the same month is a new `vintage`. --- # Point in time Strata keeps every version of a value. When a number is revised, or a game is forecast again, the new value is stored beside the old one. Each value records when it became known. That lets you ask for the data exactly as it stood at any past moment. This is what makes a test on past data honest: a strategy tested with `as_of` sees only what it could have seen at the time. ## Three fields | Field | The question it answers | Example: US consumer prices for January 1994 | | --- | --- | --- | | `ts` | What time is this value about? | `1994-01-01`: the month the index describes. | | `known_at` | When did this value become known? | `1994-02-17T13:30:00Z`: the moment it was published. | | `vintage` | Which revision is it? | 0 for the first published figure, 1 for the first revision. | `ts` and `known_at` are different on purpose. A figure about January is not known in January. ## `as_of` `as_of` is a time. With it, the answer holds only values whose `known_at` is at or before that time. Without it, the answer is as of now. ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "$STRATA/observations?dataset=econ.jobs&metric=change&from=2026-01-01&as_of=2026-06-01" ``` This returns the monthly jobs changes from January 2026 as they were known on June 1, 2026: months published after that date are missing, and months revised after that date show their earlier figure. ## `vintage` `vintage` chooses which revision of each value you get. It works together with `as_of`. | `vintage=` | What you get for each entity, metric and `ts` | | --- | --- | | `latest` (default) | The newest value known at `as_of`. | | `first` | The first value Strata held. | | `all` | Every revision known at `as_of`, one row each. | Use `latest` with `as_of` to replay the past. Use `all` to study how a number or a forecast moved: each row carries its own `vintage` and `known_at`. ## What makes a new vintage It depends on the dataset, and each dataset's page says. Two common cases: - **Official figures** (`econ.cpi`, `econ.jobs`): a new vintage when the publisher revises a past period. - **Forecasts** (`signals.sim.game`): a new vintage when a later forecast of the same game, made before it starts, gives a different value from the one held. The forecast history of a game is part of the data. Many datasets are never revised. Their values are all vintage 0. ## Where point in time is exact, and where it is not `known_at` is only as good as what Strata could record. There are three cases. Each dataset's page says which one applies. **Exact.** The value was stamped when the world learned it, or when Sedona's model produced it. `as_of` replays the past faithfully. This holds for the economic releases, Federal Reserve decisions, and Sedona's live game forecasts. A live dataset that is stamped differently says so on its page. **Stamped when collected.** For long histories loaded in one pass (baseball, soccer and cricket results, daily weather before the collector began), `known_at` is the time Strata loaded the row, not the day of the game. An `as_of` before that load returns nothing for these datasets. A final score does not change, so use `ts` to bound these in a test: a game's result was known once the game ended. **Replayed seasons.** The history datasets of Sedona's models (their ids end in `_history`) hold forecasts for past seasons, each made from only what was known before that game. They were computed later, and `known_at` records when. Treat each row as known just before its `ts`; do not filter them with `as_of`. ## When stored values are repaired A revision by the publisher, or a new forecast, never replaces what was stored before it. One thing does change stored rows: a fault found later in how Sedona stamped or stored them. Such a fault is repaired in place, because leaving it would keep a wrong answer in every point-in-time read. Each repair is listed in the [changelog](/docs/versioning#changelog) with its date, the datasets it touched and what changed. If you hold a copy of a dataset from before a repair that names it, read that dataset again. ## A worked example: a forecast that changes A game's forecast changes four times in the days before it starts, so five values are held. `vintage=all` returns all five: ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "$STRATA/observations?dataset=signals.sim.game&metric=p_home_win&entity=nfl:DAL&from=2026-11-15&to=2026-11-16&vintage=all" ``` | `vintage` | `known_at` | `value` | | --- | --- | --- | | 0 | 2026-11-11T07:52Z | 0.4128 | | 1 | 2026-11-11T19:52Z | 0.4125 | | 2 | 2026-11-12T19:52Z | 0.4123 | | 3 | 2026-11-13T20:07Z | 0.4115 | | 4 | 2026-11-14T12:37Z | 0.4091 | The same request with `vintage=latest`: | `as_of` | Row returned | | --- | --- | | not given | vintage 4 | | `2026-11-13T12:00Z` | vintage 2 | | `2026-11-10T00:00Z` | no row: nothing was known yet | The values are this game's as read on November 14, 2026, before it was played. ## Rules for an honest test 1. Decide the moment you would have acted, and pass it as `as_of`. 2. Never join in a value whose `known_at` is later than that moment. Where a dataset stores an outcome beside its forecast (what a player did, a final score), the outcome carries its own later `known_at`, so `as_of` keeps it out for you. 3. For datasets stamped when collected, bound by `ts` instead, and assume a result is known only after its event ends. 4. Check a dataset's page for its limits before you rely on it. --- # Following updates Sedona's models keep working through the day: a game is forecast again as news arrives, and in-game projections are made while games are played. This page explains how soon a new value reaches the API and how to keep pulling only what is new. ## How fresh the data is | Data | Reaches the API | | --- | --- | | Live Signals: game forecasts, player projections, NBA prop chances, second-half projections, the NBA season simulation, picks and results | Within about five minutes of Sedona recording it. Each dataset's page says when that is. | | History Signals: the forecasts of past seasons | Twice a day, at 13:45 and 19:30 UTC. | | The record | On each source's own schedule, from every three hours (weather) to weekly. | Each dataset's page says exactly when its new values arrive, in the "New values" row. [`GET /status`](/docs/api/status) shows when each dataset was last loaded. ## Two times on every value | Field | What it is | Use it to | | --- | --- | --- | | `known_at` | When the value became known: when the model made the forecast, or when the figure was published. | Test on the past without look-ahead. See [point in time](/docs/point-in-time). | | `loaded_at` | When the load that brought this value into Strata began. | Follow updates: ask for what was loaded after the last value you have. | They differ on purpose. A forecast made at 12:01 is loaded a few minutes later. For a test, `known_at` is the honest time. For keeping a copy in step, `loaded_at` is the reliable one: it only moves forward. ## Ask for what is new Pass `loaded_after` to [`GET /observations`](/docs/api/observations). The answer holds only values loaded after that time. ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "$STRATA/observations?dataset=signals.sim.game&vintage=all&loaded_after=2026-11-15T17:00:00Z" ``` Use `vintage=all` when you follow updates. A new forecast of a game is a new vintage; with the default `vintage=latest` you would get each value's newest version but not the ones in between. When nothing was loaded since the time you pass, the answer is an empty list and costs almost nothing to produce, so polling is cheap for both sides. ## The loop 1. **First read.** Read the dataset once with `vintage=all`, page by page. Remember the largest `loaded_at` among the rows: this is your mark. 2. **Each later read.** Ask with `loaded_after` set to your mark. Store the rows, and move the mark to the largest `loaded_at` you now hold. 3. **Repeat** on a timer. ```python import time def follow(dataset, mark=None, every=120): """Yield each new row of a dataset, forever. `mark` is the largest loaded_at already held (None: read it all).""" while True: params = {"dataset": dataset, "vintage": "all"} if mark: params["loaded_after"] = mark for row in pages("/observations", **params): # pages() from the recipes yield row if row["loaded_at"] and (mark is None or row["loaded_at"] > mark): mark = row["loaded_at"] time.sleep(every) for row in follow("signals.sim.game"): print(row["loaded_at"], row["entity"]["key"], row["metric"], row["value"], "vintage", row["vintage"]) ``` The mark is exact. Values arrive in loads, a load becomes visible all at once, and every value of a load carries the same `loaded_at`. A read with `loaded_after` equal to your mark therefore never skips a value and never repeats one. Take the mark from the rows, not from your own clock. A few rows restored by a listed repair have no `loaded_at`; they arrive with a full read and never move the mark. ## How often to ask - **Live Signals** are loaded every five minutes. Asking once a minute or two per dataset keeps you within a few minutes of the model and well inside a 60-a-minute limit. - Asking more often than the data is loaded returns empty lists. It does no harm, but it spends your [rate limit](/docs/rate-limits). - For data loaded twice a day, ask a few times a day, or check `GET /status` first. ## Narrowing what you follow `loaded_after` combines with every other filter. To follow one metric, or one team, add `metric` or `entity`: ```text /observations?dataset=signals.sim.game&metric=p_home_win&vintage=all&loaded_after=2026-11-15T17:00:00Z ``` ## What a new row means | You receive | It means | | --- | --- | | A row with `vintage` 0 | A value that did not exist before: a new game, a new player line, a new period. | | A row with a higher `vintage` for a `ts`, entity and metric you already hold | The value changed. Keep both rows: the earlier one is what was known before. | | A metric you have not seen | A dataset gained a metric. See [versioning](/docs/versioning). | In normal running nothing is deleted or rewritten, so there is nothing to remove from your copy. The one exception is a repair listed in the [changelog](/docs/versioning#changelog): when it names a dataset you hold, read that dataset again in full. ## Having values pushed to you [`GET /stream`](/docs/api/stream) opens a connection that stays open and sends each new value as it is loaded, with a mark to resume from after every load. It carries the same values as the loop above, a little sooner and with no timer of your own. A key can hold two streams open; for more datasets than that, use the loop. ## The first load in one file [`GET /datasets/{id}/export`](/docs/api/export) returns a whole dataset as one CSV file. Download it with `vintage=all`, take the largest `loaded_at` in it as your mark, and follow from there. --- # Pagination List endpoints return one page at a time. Every list answer has the same shape, and every list is read the same way. ## The shape of a list ```json { "data": ["..."], "has_more": true, "next": "WzcsICJwX2hvbWVfd2luIiwgIjIwMjYtMTEtMTVUMTg6MDA6MDArMDA6MDAiLCAwXQ==", "licences": {"signals.sim.game": {"licence": "model", "attribution": null}} } ``` | Field | What it is | | --- | --- | | `data` | The rows of this page. | | `has_more` | `true` when there are rows after this page. | | `next` | The cursor for the next page, or `null` on the last page. | | `licences` | The licence and attribution of each dataset in the answer. See [access levels and licences](/docs/access). | ## Reading every page 1. Make the request without `cursor`. 2. If `has_more` is `true`, repeat the same request with `cursor` set to the answer's `next`. 3. Stop when `has_more` is `false`. Keep every other parameter the same from page to page. A cursor belongs to the request that produced it. ```python def pages(path, **params): while True: page = get(path, **params) # get() from the quickstart yield from page["data"] if not page["has_more"]: return params["cursor"] = page["next"] rows = list(pages("/observations", dataset="econ.cpi", metric="yoy_pct")) ``` ## Page size `limit` sets the rows a page, from 1 to 5,000. | Endpoint | Default `limit` | | --- | --- | | `GET /observations` | 1,000 | | `GET /entities` | 500 | | `GET /events` | 500 | A larger page means fewer requests against your [rate limit](/docs/rate-limits). ## Cursors A cursor is an opaque string. Pass it back unchanged; do not build or edit one. A cursor that is changed, or sent to a different endpoint, answers 400 with the code `invalid_cursor`. Cursors do not expire. Reading is stable while data is added: a row written after your first page appears later in the list or not at all, and no row is returned twice. ## Order | Endpoint | Rows are ordered by | | --- | --- | | `GET /observations` | Entity, then metric, then `ts`, then vintage. | | `GET /entities` | Kind, then key. | | `GET /events` | When Strata first held the event, oldest first. | Observations are grouped by entity, not sorted by time across entities. To read a time range, use `from` and `to`; to get one series in time order, filter by `entity` and `metric`. ## Endpoints that are not paged `GET /datasets` returns everything at once, in the list shape with `has_more` always `false`. `GET /status` returns everything at once in its own shape. --- # Errors The API uses ordinary HTTP status codes. Any status other than 200 is an error, and every error has the same JSON shape. ## The shape of an error ```json { "error": { "type": "invalid_request", "code": "unknown_parameter", "message": "Unknown parameter 'metrics'. This endpoint takes: dataset, metric, entity_kind, entity, from, to, as_of, loaded_after, vintage, limit, cursor.", "param": "metrics", "docs": "https://sedonalabs.si/docs/errors#unknown_parameter" }, "request_id": "req_3f9a1c0be4d27a65" } ``` | Field | What it is | | --- | --- | | `error.type` | The broad kind of error. One per status code; see the table below. | | `error.code` | The exact error. Stable: write your code against this, not against the message. | | `error.message` | A sentence for a person. It may be reworded at any time. | | `error.param` | The request parameter at fault, when there is one. | | `error.docs` | The address of the code's entry on this page. | | `request_id` | The identifier of this request. The same value is in the `Request-Id` header of every answer, errors and successes alike. | When you ask Sedona about a request, send its `request_id`. ## Status codes | Status | `type` | Meaning | Retry? | | --- | --- | --- | --- | | 200 | | The request worked. | | | 400 | `invalid_request` | The request is wrong: a missing, unknown or badly formed parameter. | No. Fix the request. | | 401 | `authentication_error` | The key is missing, unknown or revoked. | No. Fix the key. | | 404 | `not_found` | The dataset or the address does not exist for this key. | No. | | 405 | `invalid_request` | The method is not allowed. Every endpoint is `GET`. | No. | | 429 | `rate_limit` | The key has used its requests for this minute, or has a download or two streams running already. | Yes, after the `Retry-After` seconds. | | 500 | `api_error` | Something failed on Sedona's side. | Yes, with a pause. Report it if it repeats. | | 503 | `service_unavailable` | Strata's database cannot be reached. | Yes, with a pause. | | 504 | `timeout` | The query took too long. | Yes, with a narrower request. | ## Error codes ### missing_api_key {#missing_api_key} Status 401. No key was sent. Send it in the `X-API-Key` header. See [authentication](/docs/authentication). ### invalid_api_key {#invalid_api_key} Status 401. The key is not one Sedona issued, or it has been revoked. Check that the whole key was copied. If it was revoked, ask your Sedona contact for a new one. ### missing_parameter {#missing_parameter} Status 400. A required parameter was left out. `param` names it. `dataset` is required on `/entities`, `/events` and `/observations`. ### invalid_parameter {#invalid_parameter} Status 400. A parameter's value is not acceptable. `param` names it and the message says what is expected. Common causes: a time that is not ISO 8601, a `limit` outside 1 to 5,000, a `vintage` other than `latest`, `first` or `all`. ### unknown_parameter {#unknown_parameter} Status 400. The request carries a parameter the endpoint does not take. `param` names it and the message lists the ones it does take. The API refuses unknown parameters instead of ignoring them. A misspelt filter that was silently ignored would return far more data than you asked for, and you would not know. ### invalid_cursor {#invalid_cursor} Status 400. `cursor` is not the `next` value of this request's last page. Pass `next` back unchanged, to the same endpoint, with the same other parameters. See [pagination](/docs/pagination). ### unknown_dataset {#unknown_dataset} Status 404. There is no dataset by that id for this key. Either the id is mistyped, or the dataset is outside the key's access level; the two look the same. `GET /datasets` lists what the key can read. ### not_found {#not_found} Status 404. The address is not an endpoint of the API. See [all endpoints](/docs/api). ### method_not_allowed {#method_not_allowed} Status 405. The endpoint exists but not for this method. The API is read-only: every endpoint is `GET`. ### dataset_too_large {#dataset_too_large} Status 400. The dataset holds more values than one downloaded file carries (2,000,000). Read it page by page with `/observations`. See [the download](/docs/api/export). ### export_in_progress {#export_in_progress} Status 429. The key is already downloading a file. Start the next one when it has finished. ### too_many_streams {#too_many_streams} Status 429. The key already has two streams open. Close one, or follow the other datasets by asking `/observations` with `loaded_after`. See [the stream](/docs/api/stream). ### rate_limited {#rate_limited} Status 429. The key has made its allowed number of requests in the last minute. Wait for the number of seconds in the `Retry-After` header, then try again. See [rate limits](/docs/rate-limits). ### query_timeout {#query_timeout} Status 504. The query ran past 25 seconds and was stopped. Narrow the request: add `entity`, `metric`, or a `from` and `to` range, or lower `limit`. If a narrow request still times out, report it with its `request_id`. ### service_unavailable {#service_unavailable} Status 503. Strata's database could not be reached. Wait and retry. `GET /status` will answer again once it is back. ### internal_error {#internal_error} Status 500. A fault on Sedona's side. The message never carries details; the `request_id` lets us find them. Retry once after a pause, and report it if it happens again. ## Handling errors in code ```python import time import requests def get(http, url, params, tries=5): for attempt in range(tries): r = http.get(url, params=params, timeout=60) if r.status_code == 200: return r.json() code = r.json()["error"]["code"] if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", "5"))) elif r.status_code in (500, 503): time.sleep(2 ** attempt) else: # 400, 401, 404, 504: retrying the same request will not help raise RuntimeError(f"{r.status_code} {code} ({r.headers['Request-Id']}): {r.json()['error']['message']}") raise RuntimeError(f"gave up after {tries} tries ({r.headers['Request-Id']})") ``` --- # Rate limits Each key may make a set number of requests a minute. The limit is 60 unless your agreement says otherwise. `GET /` returns your key's limit as `requests_per_minute`. ## How it is counted The limit covers a rolling minute: at any moment, the requests your key made in the 60 seconds before it. Every answered request counts, including ones that failed with 400, 404, 500 or 504. A request refused with 429 does not count, so waiting always works. ## The headers Every answer to a request with a valid key carries three headers: | Header | What it is | | --- | --- | | `RateLimit-Limit` | Your key's requests a minute. | | `RateLimit-Remaining` | How many requests are left in the current minute, after this one. | | `RateLimit-Reset` | Seconds until the oldest request in the current minute stops counting. | ```http HTTP/2 200 request-id: req_3f9a1c0be4d27a65 ratelimit-limit: 60 ratelimit-remaining: 41 ratelimit-reset: 37 ``` ## When you pass the limit The answer is status 429 with the code `rate_limited` and a `Retry-After` header: the number of seconds to wait. ```json { "error": { "type": "rate_limit", "code": "rate_limited", "message": "This key allows 60 requests a minute. Try again in 23 s.", "docs": "https://sedonalabs.si/docs/errors#rate_limited" }, "request_id": "req_a81e44c07b02d913" } ``` Wait that long and repeat the request. Nothing is lost: a cursor from before the 429 still works. ## Staying under it - **Ask for bigger pages.** `limit` goes up to 5,000 rows. One request for 5,000 rows costs the same as one request for 10. - **Filter on the server.** Pass `metric`, `entity`, `from` and `to` instead of reading everything and discarding most of it. - **Read `RateLimit-Remaining`.** Slow down when it is low instead of running into 429. - **Do not poll faster than the data changes.** Each dataset's page says when new values arrive, and `GET /status` shows each dataset's last update. - **Keep a local copy.** Read a dataset once and then ask only for what is new with `loaded_after`. See [following updates](/docs/updates). If your use needs a higher limit, ask your Sedona contact. --- # Versioning and changes The version is part of the address: `/api/public/strata/v1`. Within a version, the API only changes in ways that do not break a working client. ## What can change without notice These are additions. Write your client so that it ignores what it does not know. - A new endpoint. - A new optional request parameter. - A new field in an answer. - A new dataset, a new metric in a dataset, a new entity kind, a new event kind or status. - A new error `code`. - The wording of an error `message` or of a description. - More history, or newer data, in a dataset. ## What will not change within a version - An endpoint's address or method. - The name, type or meaning of a field in an answer. - The meaning of a request parameter, or what happens when it is left out. - A dataset's id, a metric's name, a metric's unit, or what a metric measures. - The status code and `code` of an existing error. A change of that kind goes into a new version at a new address. The old version keeps working beside it, and key holders are told ahead of the date it will stop, with time to move. ## Corrections to data There are two kinds of correction. - **A value the source or the model changes** (a revised figure, a new forecast) is stored as a new vintage beside the old one. A read with `as_of` before the change still returns what was held then. - **A fault in how Sedona stamped or stored rows**, found later, is repaired in place: the stored rows are changed. A point-in-time read of those rows then answers differently from before the repair, which is the purpose of it. Every repair of the second kind is listed in the changelog below with its date, the datasets it touched and what changed. If you hold a copy of a dataset from before a repair that names it, read that dataset again in full. ## Changelog ### 2026-11-14: repairs to stored values Made between 21:16 and 21:36 UTC, before any customer key was issued. If you copied one of these datasets earlier, read it again in full. | Dataset | What was wrong | What changed | | --- | --- | --- | | `signals.nba.prop_chance` | Every chance and median was stamped when the game ended, so a read with `as_of` before a game returned nothing. | `known_at` of the chances and medians is now the time of the simulation they came from, before tip-off. What the player did keeps the time the game ended. | | `signals.more.game` | A game's result (`home_won`) was stamped with the forecast's time, so a read with `as_of` before the game returned who won. | `known_at` of each result is now the time it was graded, after the game. | | `signals.sim.game` | Only the newest forecast at each load was kept; forecasts made between two loads were missing. | The history was rebuilt to hold every forecast made before each game. Vintage numbers in this dataset changed, and the rows the rebuild restored have no `loaded_at`. | | NBA game events | A game's `starts_at` held the time of an in-game reading, and finished games stayed `scheduled`. | `starts_at` is the scheduled tip-off, and a finished game is `final`. | | NBA players, NBA and NFL teams | `name` was an id number or a team code. | `name` is the player's or team's name. Keys did not change. | ### 2026-11-14: version 1 The first version meant for customers. It replaces `v0`, the address used during testing. - **One error shape.** Every error is `{"error": {"type", "code", "message", "param", "docs"}, "request_id"}`. Before, errors came in three different shapes. - **Request ids.** Every answer carries a `Request-Id` header. - **Rate-limit headers.** `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset` on every answer, and `Retry-After` on a 429. - **Unknown parameters are refused** with `unknown_parameter`. Before, they were ignored. - **One way to page.** Every list has `has_more` and an opaque `next` cursor. `GET /entities` took `offset` before; it now takes `cursor`. - **Observations are ordered by entity, then metric, then time.** Before, they were ordered by time first, and a page of a large dataset could take over 30 seconds. A page now takes well under a second. - **A slow query answers 504** `query_timeout` with advice, not a bare 500. - **Datasets describe themselves.** `GET /datasets/{id}` returns what `ts` and `known_at` mean, the entities and events, each metric's meaning and unit, and the dataset's limits. - **`GET /events` takes `key`** to fetch one event. - **Following updates.** Every value has `loaded_at`, and `GET /observations` takes `loaded_after` to return only what was loaded since. Live Signals are loaded every five minutes; before, twice a day. - **Events are listed under every dataset that refers to them.** Before, a game shared by several datasets was listed under one. - **A shared event is no longer overwritten.** A game keeps its start time and does not go back to `scheduled` once it has another status. - **Empty and paused datasets are not listed.** - **A dataset in one file.** `GET /datasets/{id}/export` returns a whole dataset as CSV. - **A stream.** `GET /stream` keeps a connection open and sends each new value as it is loaded. - **New Signals datasets:** `signals.sim.player`, each player's projection before the game; `signals.pick.game`, Sedona's pick for each game and whether it won; and this season's final scores beside the game forecasts in `signals.sim.game`. `v0` still answers at its old address with the same behaviour as `v1`, for keys issued before this date. It will be removed once those keys have moved. --- # All endpoints The API has nine endpoints. All are `GET` and all need a key. Seven answer JSON; the download answers a CSV file and the stream answers server-sent events. ```text https://sedonalabs.si/api/public/strata/v1 ``` | Endpoint | What it returns | | --- | --- | | [`GET /`](/docs/api/whoami) | The service, and your key's access level and rate limit. | | [`GET /datasets`](/docs/api/datasets) | Every dataset your key can read, with how much it holds. | | [`GET /datasets/{id}`](/docs/api/dataset) | One dataset in full: what its fields mean and every metric. | | [`GET /datasets/{id}/export`](/docs/api/export) | A whole dataset as one CSV file. | | [`GET /entities`](/docs/api/entities) | The teams, players, stations and so on that a dataset is about. | | [`GET /events`](/docs/api/events) | The games, releases, meetings and so on that a dataset covers. | | [`GET /observations`](/docs/api/observations) | The values, point in time. This is the endpoint you will call most. | | [`GET /stream`](/docs/api/stream) | A connection that sends each new value as it is loaded. | | [`GET /status`](/docs/api/status) | How fresh each dataset is, and how its last update went. | ## What every endpoint shares - **Authentication.** Send your key in the `X-API-Key` header. See [authentication](/docs/authentication). - **Parameters.** All parameters go in the query string. A parameter the endpoint does not take is refused with `unknown_parameter`. - **Times.** ISO 8601, in UTC in answers. A date alone means midnight UTC. - **Lists.** `data`, `has_more`, `next` and `licences`. See [pagination](/docs/pagination). - **Errors.** One shape, with a stable `code`. See [errors](/docs/errors). - **Headers on every answer.** `Request-Id`, and with a valid key `RateLimit-Limit`, `RateLimit-Remaining` and `RateLimit-Reset`. See [rate limits](/docs/rate-limits). ## A machine-readable description The OpenAPI 3 description is at `/openapi.json` under the base address, and `/docs` under the base address is a page for trying calls from a browser. Both need no key to open; the calls you try there do. --- # GET / Returns the service and what the key you sent can do. Use it to check a key, and to read its access level and rate limit from code. ```text GET / ``` ## Parameters None. ## Example ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/" ``` ```json { "service": "Sedona Strata", "version": "v1", "scope": "partner", "requests_per_minute": 60, "endpoints": ["/datasets", "/datasets/{id}", "/entities", "/events", "/observations", "/status"], "docs": "https://sedonalabs.si/docs", "openapi": "/api/public/strata/v1/openapi.json" } ``` ## The answer | Field | Type | What it is | | --- | --- | --- | | `service` | text | Always `Sedona Strata`. | | `version` | text | The version of the API at this address. | | `scope` | text | The key's access level: `external` or `partner`. See [access levels](/docs/access). | | `requests_per_minute` | number | The key's rate limit. See [rate limits](/docs/rate-limits). | | `endpoints` | list of text | The endpoints of this version. | | `docs` | text | The address of these pages. | | `openapi` | text | The path of the OpenAPI description. | ## Errors `missing_api_key`, `invalid_api_key`, `rate_limited`. See [errors](/docs/errors). --- # GET /datasets Returns every dataset the key can read: what each one is, how much it holds, the time it covers, and how its last update went. ```text GET /datasets ``` ## Parameters None. The list is not paged: it always returns every dataset. ## Example ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/datasets" ``` ```json { "data": [ { "id": "econ.cpi", "domain": "econ", "title": "US consumer prices (CPI-U), point in time", "licence": "public_domain", "attribution": null, "description": "The US consumer price index as first published and as later revised.", "access": "external", "updates": "Once a month, on the day of the release.", "source": {"id": "bls", "name": "U.S. Bureau of Labor Statistics", "url": "https://www.bls.gov"}, "observations": 7530, "events": 407, "first_ts": "1994-01-01T00:00:00+00:00", "last_ts": "2026-10-01T00:00:00+00:00", "last_run": { "status": "written", "finished_at": "2026-11-14T13:46:02.118000+00:00", "rows_written": 0 } } ], "has_more": false, "next": null, "licences": {"econ.cpi": {"licence": "public_domain", "attribution": null}} } ``` The wording of `title`, `description` and `updates` in this example is illustrative; the live answer and [the dataset pages](/docs/datasets) carry the real text. ## The answer Each item of `data`: | Field | Type | What it is | | --- | --- | --- | | `id` | text | The dataset's identifier. Pass it as `dataset` to the other endpoints. | | `domain` | text | The area it belongs to: `sports`, `nba`, `nfl`, `econ`, `fed`, `weather`, and so on. | | `title` | text | A short name. | | `description` | text | What the dataset is. | | `licence` | text | Its licence class. See [licences](/docs/access#licence-classes). | | `attribution` | text or null | Text to show with the data, when the source asks for it. | | `access` | text | The lowest access level that reads it: `external` or `partner`. | | `updates` | text | When new values arrive. Missing when not recorded. | | `source` | object | Where the data comes from: `id`, `name` and `url`. | | `observations` | number | How many values it holds, counting every vintage. | | `events` | number | How many events it holds. | | `first_ts` | time or null | The earliest `ts` of any value. | | `last_ts` | time or null | The latest `ts` of any value. For forecasts this can be in the future. | | `last_run` | object or null | The last update: see below. | `last_run`: | Field | Type | What it is | | --- | --- | --- | | `status` | text | `written`: the update ran and was accepted. `refused`: the new batch failed a quality check and was not stored, so the last good data stands. `failed`: the update did not finish. | | `finished_at` | time | When it ended. | | `rows_written` | number | How many new values it stored. 0 is normal when nothing changed. | The counts and times are refreshed about twice a day, so they can trail the data by some hours. ## Errors `missing_api_key`, `invalid_api_key`, `unknown_parameter`, `rate_limited`. See [errors](/docs/errors). --- # GET /datasets/{id} Returns one dataset in full: what its times mean, what makes a new vintage, its entities and events, every metric with its meaning and unit, and its known limits. The same content is on each [dataset page](/docs/datasets). ```text GET /datasets/{id} ``` ## Parameters | Parameter | In | Required | What it is | | --- | --- | --- | --- | | `id` | path | yes | The dataset's id, such as `signals.sim.game`. | ## Example ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/datasets/signals.sim.game" ``` ```json { "data": { "id": "signals.sim.game", "domain": "sports", "title": "Sedona Sim: every NBA, NFL and college game's pregame forecast, point in time", "licence": "model", "attribution": null, "description": "Sedona's forecast of each NBA, NFL and college football game before it starts.", "access": "partner", "source": {"id": "sedona.models", "name": "Sedona models", "url": "https://sedonalabs.si"}, "updates": "...", "ts": "...", "known_at": "...", "vintages": "...", "entities": [{"kind": "team", "key": "...", "description": "..."}], "limitations": ["..."], "event_words": [{"kind": "game", "key": "...", "description": "...", "statuses": {"scheduled": "..."}, "roles": {"home": "..."}}], "metrics": [ { "metric": "p_home_win", "n": 566, "first_ts": "2026-10-15T23:30:00+00:00", "last_ts": "2026-11-21T05:00:00+00:00", "max_vintage": 5, "description": "The chance the home side wins, as the model estimates it.", "unit": "probability" } ], "entity_kinds": [{"kind": "team", "n": 200}], "observations": 8477, "events": 318, "first_ts": "2026-10-15T23:30:00+00:00", "last_ts": "2026-11-21T05:00:00+00:00", "last_run": {"status": "written", "finished_at": "2026-11-14T13:47:10.402000+00:00", "rows_written": 212} }, "has_more": false, "next": null, "licences": {"signals.sim.game": {"licence": "model", "attribution": null}} } ``` Text shown as `...` and the description wording are shortened here; the live answer carries the full text. ## The answer `data` holds the fields of [`GET /datasets`](/docs/api/datasets) and these: | Field | Type | What it is | | --- | --- | --- | | `ts` | text | What `ts` means in this dataset. | | `known_at` | text | What `known_at` means in this dataset, and whether `as_of` replays it faithfully. See [point in time](/docs/point-in-time). | | `vintages` | text | What makes a new vintage. | | `entities` | list | Each kind of entity: `kind`, the format of its `key`, and what it is. | | `event_words` | list | Each kind of event: `kind`, the format of its `key`, what it is, and the meaning of its `statuses`, `roles` and `attrs`. | | `limitations` | list of text | Known limits of the dataset. | | `metrics` | list | Every metric: see below. | | `entity_kinds` | list | How many entities of each kind the dataset's values are about. | Each item of `metrics`: | Field | Type | What it is | | --- | --- | --- | | `metric` | text | The metric's name. Pass it as `metric` to `/observations`. | | `description` | text | What the value means. | | `unit` | text | The unit of the value. | | `pattern` | text | For a name built from parts, the family it belongs to, such as `p_over_{stat}_{line}`. | | `n` | number | How many values of this metric the dataset holds, counting every vintage. | | `first_ts`, `last_ts` | time | The earliest and latest `ts` of this metric. | | `max_vintage` | number | The highest vintage any value of this metric has reached. 0 means it has never been revised. | ## Errors `unknown_dataset` when the id is wrong or outside the key's access level. Also `missing_api_key`, `invalid_api_key`, `unknown_parameter`, `rate_limited`. See [errors](/docs/errors). --- # GET /datasets/{id}/export Returns a whole dataset as one CSV file, in a single request. Use it for a first load, or to open a dataset in a spreadsheet or a data tool without writing a paging loop. ```text GET /datasets/{id}/export ``` ## Parameters | Parameter | In | Required | Default | What it is | | --- | --- | --- | --- | --- | | `id` | path | yes | | The dataset's id. | | `vintage` | query | no | `latest` | `latest`, `first` or `all`, as on [`/observations`](/docs/api/observations). Use `all` for a copy you will keep up to date. | | `metric` | query | no | every metric | Only this metric. | | `from` | query | no | | Only values whose `ts` is at or after this time. | | `to` | query | no | | Only values whose `ts` is before this time. | | `as_of` | query | no | now | The file as it would have been at this time. | ## Example ```bash curl -H "X-API-Key: $STRATA_API_KEY" -o sim_game.csv \ "https://sedonalabs.si/api/public/strata/v1/datasets/signals.sim.game/export?vintage=all" ``` ```text ts,entity_kind,entity_key,entity_name,metric,value,value_text,unit,vintage,known_at,loaded_at,event_domain,event_kind,event_key 2026-11-15T21:25:00+00:00,team,nfl:DAL,Dallas Cowboys,p_home_win,0.4128,,probability,0,2026-11-11T07:52:02.412000+00:00,2026-11-11T19:42:39.118204+00:00,nfl,game,2026_10_SF_DAL 2026-11-15T21:25:00+00:00,team,nfl:DAL,Dallas Cowboys,p_home_win,0.4125,,probability,1,2026-11-11T19:52:01.907000+00:00,,nfl,game,2026_10_SF_DAL ``` The answer is cut to its first two rows. ## The file A header row, then one row per value, in the order of `/observations`: by entity, then metric, then `ts`, then vintage. The columns are the fields of an observation, flattened: | Column | What it is | | --- | --- | | `ts` | The time the value is about. | | `entity_kind`, `entity_key`, `entity_name` | Who or what it is about. | | `metric` | What is measured. | | `value` | The number, or empty when the value is text. | | `value_text` | The value as text, or empty. | | `unit` | The unit of `value`. | | `vintage` | Which revision: 0 is the first. | | `known_at` | When the value became known. | | `loaded_at` | When the load that brought it in began, or empty. | | `event_domain`, `event_kind`, `event_key` | The event it belongs to, or empty. | Times are ISO 8601 in UTC. Text that holds a comma or a quote is quoted in the usual CSV way. ## Limits - **Size.** One file carries up to 2,000,000 values. A larger dataset answers 400 `dataset_too_large`; read it page by page with [`/observations`](/docs/api/observations). [Every dataset and metric](/docs/datasets) shows how many values each holds. - **One at a time.** A key downloads one file at a time. A second request while one is running answers 429 `export_in_progress`. - **Rate limit.** A download counts as one request, whatever its size. - **An interrupted download.** If the connection drops, the file is incomplete and your HTTP client reports an error. Start it again; nothing is kept on the server. ## Keeping the copy up to date Download with `vintage=all`, note the largest `loaded_at` in the file, and from then on ask [`/observations`](/docs/api/observations) with `loaded_after` set to it, or open a [stream](/docs/api/stream). See [following updates](/docs/updates). ## The licence The answer's `X-Strata-Licence` header carries the dataset's licence class. Its attribution text, when it has one, is in [`GET /datasets`](/docs/api/datasets). ## Errors `unknown_dataset`, `dataset_too_large`, `export_in_progress`, `invalid_parameter`, `unknown_parameter`. Also `missing_api_key`, `invalid_api_key`, `rate_limited`. See [errors](/docs/errors). --- # GET /entities Returns the entities of a dataset: the teams, players, stations, indicators and so on that its values and events are about. Use it to find the `key` to filter `/observations` and `/events` by. ```text GET /entities ``` ## Parameters | Parameter | Required | Default | What it is | | --- | --- | --- | --- | | `dataset` | yes | | The dataset's id. | | `kind` | no | every kind | Only entities of this kind, such as `team` or `player`. | | `q` | no | | Only entities whose name or key contains this text. Upper and lower case are treated the same. | | `limit` | no | 500 | Rows a page, 1 to 5,000. | | `cursor` | no | | The `next` value of the last page. See [pagination](/docs/pagination). | ## Example Find the Boston entity in the game forecasts: ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/entities?dataset=signals.sim.game&kind=team&q=bos" ``` ```json { "data": [ { "kind": "team", "key": "nba:BOS", "name": "Boston Celtics", "attrs": {"code": "BOS", "league": "nba"} } ], "has_more": false, "next": null, "licences": {"signals.sim.game": {"licence": "model", "attribution": null}} } ``` ## The answer Each item of `data`: | Field | Type | What it is | | --- | --- | --- | | `kind` | text | The type of entity. | | `key` | text | Its identifier, unique within its kind. Pass it as `entity`. | | `name` | text | A name to show. Where no name is on file it is a code or an id; the dataset's page says so. | | `attrs` | object | Extra facts, different for each kind. May be empty. | Rows are ordered by `kind`, then `key`. ## Notes - The same real team or player can have a different `key` in different datasets. Each dataset's page gives its key format; do not assume a key from one dataset works in another. - The first request for a large dataset's entities can take several seconds while the list is built. Later requests are fast. ## Errors `missing_parameter` without `dataset`, `unknown_dataset`, `invalid_parameter`, `invalid_cursor`, `unknown_parameter`. Also `missing_api_key`, `invalid_api_key`, `rate_limited`. See [errors](/docs/errors). --- # GET /events Returns the events of a dataset: the games, matches, releases, meetings, races or days it covers, each with the entities that take part. ```text GET /events ``` ## Parameters | Parameter | Required | Default | What it is | | --- | --- | --- | --- | | `dataset` | yes | | The dataset's id. | | `kind` | no | every kind | Only events of this kind, such as `game` or `release`. | | `key` | no | | Only the event with this key. | | `from` | no | | Only events that start at or after this time. | | `to` | no | | Only events that start before this time. | | `entity` | no | | Only events that involve the entity with this key. | | `entity_kind` | no | | With `entity`: the kind of that entity, when the key alone is not unique. | | `limit` | no | 500 | Rows a page, 1 to 5,000. | | `cursor` | no | | The `next` value of the last page. See [pagination](/docs/pagination). | ## Example The games Boston plays in the forecasts, from November 14, 2026: ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/events?dataset=signals.sim.game&entity=nba:BOS&from=2026-11-14" ``` ```json { "data": [ { "domain": "nba", "kind": "game", "key": "g-0d702a372ec59513", "title": "DAL @ BOS", "starts_at": "2026-11-15T23:00:00+00:00", "ends_at": null, "status": "scheduled", "attrs": {}, "entities": [ {"kind": "team", "key": "nba:BOS", "name": "Boston Celtics", "role": "home"}, {"kind": "team", "key": "nba:DAL", "name": "Dallas Mavericks", "role": "away"} ] } ], "has_more": false, "next": null, "licences": {"signals.sim.game": {"licence": "model", "attribution": null}} } ``` The answer is cut to its first event, as read on November 14, 2026. ## The answer Each item of `data`: | Field | Type | What it is | | --- | --- | --- | | `domain` | text | The area the event belongs to. | | `kind` | text | The type of event. | | `key` | text | Its identifier, unique within its domain and kind. An observation's `event.key` matches it. | | `title` | text | A readable name. | | `starts_at` | time or null | When it starts; for a release, when it was published. | | `ends_at` | time or null | When it ends. Empty for most datasets today. | | `status` | text | Where it stands. Each dataset's page lists the values it uses and their meaning. | | `attrs` | object | Extra facts, different for each kind. May be empty. | | `entities` | list | The entities that take part: `kind`, `key`, `name` and `role`. | Rows are ordered by when Strata first held the event, oldest first. That is usually close to time order but is not guaranteed to be; sort by `starts_at` if you need it. ## Joining events to values An observation's `event` object holds the `domain`, `kind` and `key` of its event. To get one game's values, read the game's `starts_at` and its home team here (the entity whose `role` is `home`), then call [`/observations`](/docs/api/observations) with that team's key as `entity` and a `from` and `to` around `starts_at`. Game forecasts are stored under the home team only: the away team's key returns nothing. Each dataset's page says which entity its values are stored under. ## Errors `missing_parameter` without `dataset`, `unknown_dataset`, `invalid_parameter` for a time that is not ISO 8601, `invalid_cursor`, `unknown_parameter`. Also `missing_api_key`, `invalid_api_key`, `rate_limited`, `query_timeout`. See [errors](/docs/errors). --- # GET /observations Returns the values of a dataset, point in time. Each row is one value of one metric, for one entity, at one time. ```text GET /observations ``` ## Parameters | Parameter | Required | Default | What it is | | --- | --- | --- | --- | | `dataset` | yes | | The dataset's id. | | `metric` | no | every metric | Only this metric, by its full name. | | `entity` | no | every entity | Only the entity with this key. | | `entity_kind` | no | | Only entities of this kind. With `entity`, it picks between entities that share a key. | | `from` | no | | Only values whose `ts` is at or after this time. | | `to` | no | | Only values whose `ts` is before this time. | | `as_of` | no | now | Answer as it stood at this time: only values whose `known_at` is at or before it. | | `loaded_after` | no | | Only values loaded into Strata after this time: what is new since you last read. See [following updates](/docs/updates). | | `vintage` | no | `latest` | `latest`: the newest value known at `as_of`. `first`: the first value held. `all`: every revision known at `as_of`. | | `limit` | no | 1,000 | Rows a page, 1 to 5,000. | | `cursor` | no | | The `next` value of the last page. See [pagination](/docs/pagination). | `from`, `to`, `as_of` and `loaded_after` take ISO 8601 times. A date alone, `2026-11-15`, means midnight UTC. [Point in time](/docs/point-in-time) explains `as_of` and `vintage`. ## Example Year-over-year US inflation for the first months of 2026, as first published: ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/observations?dataset=econ.cpi&metric=yoy_pct&entity=CUUR0000SA0&from=2026-01-01&to=2026-04-01&vintage=first" ``` ```json { "data": [ { "ts": "2026-01-01T00:00:00+00:00", "entity": { "kind": "indicator", "key": "CUUR0000SA0", "name": "CPI-U all items, U.S. city average, not seasonally adjusted" }, "metric": "yoy_pct", "value": 2.7, "value_text": null, "unit": "percent", "vintage": 0, "known_at": "2026-02-11T13:30:00+00:00", "loaded_at": "2026-02-11T13:45:00.412000+00:00", "event": {"domain": "econ", "kind": "release", "key": "cpi:2026-01"} } ], "has_more": false, "next": null, "as_of": null, "loaded_after": null, "vintage": "first", "licences": {"econ.cpi": {"licence": "public_domain", "attribution": null}} } ``` The value and release time in this example are illustrative, and the answer is cut to its first row. ## The answer | Field | Type | What it is | | --- | --- | --- | | `data` | list | The rows: see below. | | `has_more`, `next` | | See [pagination](/docs/pagination). | | `as_of` | time or null | The `as_of` you sent, or `null` for now. | | `loaded_after` | time or null | The `loaded_after` you sent, or `null`. | | `vintage` | text | The `vintage` mode used. | | `licences` | object | The dataset's licence and attribution. | Each item of `data`: | Field | Type | What it is | | --- | --- | --- | | `ts` | time | The time the value is about. | | `entity` | object | Who or what it is about: `kind`, `key`, `name`. | | `metric` | text | What is measured. | | `value` | number or null | The number. `null` when the value is text. | | `value_text` | text or null | The value as text, or a note beside the number. | | `unit` | text or null | The unit of `value`. | | `vintage` | number | Which revision: 0 is the first. | | `known_at` | time | When this value became known. | | `loaded_at` | time or null | When the load that brought this value into Strata began. Use it to follow updates. | | `event` | object or null | The event it belongs to: `domain`, `kind`, `key`. | ## Order Rows are ordered by entity, then metric, then `ts`, then vintage. They are not sorted by time across entities. To read one series in time order, pass `entity` and `metric`. ## Getting fast answers - **Name the metric.** A dataset can have hundreds of metrics; `metric` returns the one you need. - **Name the entity** when you want one team, player or station. - **Bound the time** with `from` and `to` when you want recent values or one day. - **Use big pages** when you read a whole dataset: `limit=5000`. A request with no filters is allowed: it reads the whole dataset, page by page. ## Common requests Every metric of one game's forecast (the home team is the entity): ```text /observations?dataset=signals.sim.game&entity=nfl:DAL&from=2026-11-15&to=2026-11-16 ``` How a forecast moved before the game: ```text /observations?dataset=signals.sim.game&entity=nfl:DAL&metric=p_home_win&from=2026-11-15&to=2026-11-16&vintage=all ``` What was loaded since your last read: ```text /observations?dataset=signals.sim.game&vintage=all&loaded_after=2026-11-15T17:00:00Z ``` A dataset as it stood at a past moment: ```text /observations?dataset=econ.jobs&as_of=2026-06-01T00:00:00Z ``` ## Errors `missing_parameter` without `dataset`, `unknown_dataset`, `invalid_parameter` (a bad time, `limit` or `vintage`), `invalid_cursor`, `unknown_parameter`, `query_timeout`. Also `missing_api_key`, `invalid_api_key`, `rate_limited`. See [errors](/docs/errors). An `entity` that does not exist is not an error: the answer is an empty list. --- # GET /stream Opens a connection that stays open and sends each new value of a dataset as soon as it is loaded. It uses server-sent events, a plain HTTP format that most languages can read line by line and that browsers read with `EventSource`. ```text GET /stream ``` Use it when you want values pushed to you. Asking [`/observations`](/docs/api/observations) with `loaded_after` once a minute gives the same values at nearly the same time; choose whichever fits your code. See [following updates](/docs/updates). ## Parameters | Parameter | Required | Default | What it is | | --- | --- | --- | --- | | `dataset` | yes | | The dataset's id. One stream follows one dataset. | | `metric` | no | every metric | Only this metric. | | `entity` | no | every entity | Only the entity with this key. | | `entity_kind` | no | | Only entities of this kind. | | `loaded_after` | no | now | Start after this load: the largest `loaded_at` you hold. Without it the stream sends only what is loaded from now on. | When an event-stream client reconnects, it sends a `Last-Event-ID` header by itself. The stream takes that as `loaded_after`, so a dropped connection resumes where it left off. ## Example ```bash curl -N -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/stream?dataset=signals.sim.game&metric=p_home_win" ``` ```text event: open data: {"dataset": "signals.sim.game", "loaded_after": "2026-11-15T17:02:11.508112+00:00", "poll_seconds": 10} : keep-alive event: observation data: {"ts": "2026-11-15T21:25:00+00:00", "entity": {"kind": "team", "key": "nfl:DAL", "name": "Dallas Cowboys"}, "metric": "p_home_win", "value": 0.4067, "value_text": null, "unit": "probability", "vintage": 5, "known_at": "2026-11-15T17:02:31.204000+00:00", "loaded_at": "2026-11-15T17:05:30.114873+00:00", "event": {"domain": "nfl", "kind": "game", "key": "2026_10_SF_DAL"}} event: loaded id: 2026-11-15T17:05:30.114873+00:00 data: {"loaded_at": "2026-11-15T17:05:30.114873+00:00", "values": 1} ``` The values in this example are illustrative. ## The events | Event | When | Its data | | --- | --- | --- | | `open` | Once, when the stream starts. | The dataset, the `loaded_after` it started from, and how often it looks for a new load (`poll_seconds`). | | `observation` | For each new value. | One value, in the shape of an item of [`/observations`](/docs/api/observations). Every revision is sent, as with `vintage=all`. | | `loaded` | After all the values of a load have been sent. | `loaded_at`, the mark to resume from, and `values`, how many were sent. Its `id` is the same mark. | | `end` | When the stream has been open for an hour. | `reason` and the `loaded_after` to connect again with. | A line that starts with a colon (`: keep-alive`) is a comment, sent about every 20 seconds when nothing is new so that the connection is not dropped as idle. Ignore it. ## Resuming without gaps Keep the `loaded_at` of the last `loaded` event you received. To resume, connect with `loaded_after` set to it. The mark moves only once a whole load has been sent. If a connection breaks in the middle of a load, resuming sends that load's values again from its start. So after a break you can receive a value twice, but you never miss one. Identify a value by its entity, metric, `ts` and vintage, and keep the first copy. ## Limits - **Two streams a key.** A third answers 429 `too_many_streams`. To follow more datasets than that, ask `/observations` with `loaded_after` for the others. - **One hour.** A stream ends with an `end` event after an hour. Connect again with the mark it gives; this is normal, not an error. - **How fast.** A value is sent within about 10 seconds of its load. Live Signals are loaded every five minutes, so a value arrives within about five minutes of being made. - **Rate limit.** Opening a stream counts as one request. The values it sends do not count. ## Reading it in Python ```python import json import os import requests BASE = "https://sedonalabs.si/api/public/strata/v1" mark = None # the last `loaded` mark; keep it somewhere durable while True: params = {"dataset": "signals.sim.game", "metric": "p_home_win"} if mark: params["loaded_after"] = mark with requests.get(BASE + "/stream", params=params, stream=True, timeout=(10, 90), headers={"X-API-Key": os.environ["STRATA_API_KEY"]}) as r: r.raise_for_status() event = None for line in r.iter_lines(decode_unicode=True): if line.startswith("event: "): event = line[7:] elif line.startswith("data: "): data = json.loads(line[6:]) if event == "observation": print(data["known_at"], data["entity"]["key"], data["value"], "vintage", data["vintage"]) elif event in ("loaded", "end"): mark = data.get("loaded_at") or data.get("loaded_after") or mark ``` The outer loop connects again after the hourly `end` and after a dropped connection, each time from the last mark. The read timeout of 90 seconds is longer than the gap between keep-alive lines, so a silent connection is noticed. ## Errors `missing_parameter` without `dataset`, `unknown_dataset`, `invalid_parameter`, `unknown_parameter`, `too_many_streams`. Also `missing_api_key`, `invalid_api_key`, `rate_limited`. These are ordinary JSON errors, returned before the stream starts. See [errors](/docs/errors). --- # GET /status Returns how fresh each dataset is: how much it holds, the latest time it covers, and how its last update went. Call it to check that data is arriving before you rely on it, or to see whether a dataset has new values to read. ```text GET /status ``` ## Parameters None. ## Example ```bash curl -H "X-API-Key: $STRATA_API_KEY" \ "https://sedonalabs.si/api/public/strata/v1/status" ``` ```json { "data": [ { "dataset": "weather.cli", "licence": "public_domain", "observations": 7597, "events": 1288, "first_ts": "2026-10-11T05:00:00+00:00", "last_ts": "2026-11-13T08:00:00+00:00", "last_run": { "status": "written", "finished_at": "2026-11-14T18:20:41.730000+00:00", "rows_written": 117 } } ], "generated_at": "2026-11-14T20:05:12.004000+00:00" } ``` The answer is cut to one dataset here. ## The answer | Field | Type | What it is | | --- | --- | --- | | `data` | list | One item per dataset the key can read. | | `generated_at` | time | When this answer was made. | Each item of `data`: | Field | Type | What it is | | --- | --- | --- | | `dataset` | text | The dataset's id. | | `licence` | text | Its licence class. | | `observations` | number | How many values it holds, counting every vintage. | | `events` | number | How many events it holds. | | `first_ts`, `last_ts` | time or null | The earliest and latest `ts` of any value. | | `last_run` | object or null | The last update: its `status`, when it `finished_at`, and `rows_written`. | `last_run.status` is one of: | Status | Meaning | | --- | --- | | `written` | The update ran and its data was accepted. `rows_written` can be 0 when nothing had changed. | | `refused` | The new batch failed a quality check and was not stored. The last good data stands. | | `failed` | The update did not finish. The last good data stands. | ## Notes - The counts and `last_ts` are refreshed about twice a day. `last_run` is at most ten minutes old. - A dataset whose `last_run` is `refused` or `failed` still answers with the data it held before. ## Errors `missing_api_key`, `invalid_api_key`, `unknown_parameter`, `rate_limited`. See [errors](/docs/errors). --- # Recipes Worked examples for common jobs. Each uses the `get` and `pages` helpers below, which are the ones from the [quickstart](/docs/quickstart) and [pagination](/docs/pagination) with retries added. ```python import os import time import requests BASE = "https://sedonalabs.si/api/public/strata/v1" http = requests.Session() http.headers["X-API-Key"] = os.environ["STRATA_API_KEY"] def get(path, **params): for attempt in range(5): r = http.get(BASE + path, params=params, timeout=60) if r.status_code == 200: return r.json() if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", "5"))) elif r.status_code in (500, 503): time.sleep(2 ** attempt) else: err = r.json()["error"] raise RuntimeError(f"{r.status_code} {err['code']}: {err['message']} ({r.headers['Request-Id']})") raise RuntimeError("gave up") def pages(path, **params): params.setdefault("limit", 5000) while True: page = get(path, **params) yield from page["data"] if not page["has_more"]: return params["cursor"] = page["next"] ``` ## Get the forecasts for a day's games Read every metric of every game that starts in a 24-hour window, and group the rows by game. ```python from collections import defaultdict games = defaultdict(dict) for row in pages("/observations", dataset="signals.sim.game", **{"from": "2026-11-15T08:00:00Z", "to": "2026-11-16T08:00:00Z"}): games[row["event"]["key"]][row["metric"]] = row["value"] games[row["event"]["key"]]["home"] = row["entity"]["key"] games[row["event"]["key"]]["starts_at"] = row["ts"] for key, g in sorted(games.items(), key=lambda kv: kv[1]["starts_at"]): print(g["starts_at"], g["home"], f"home win {g['p_home_win']:.0%}", f"margin {g['margin_median']:+.1f}") ``` The window here runs from 08:00 UTC, which is midnight Pacific in winter, so it holds one US game day. Each game's values hang on its home team. To name the away team, read the game from [`/events`](/docs/api/events): its `entities` hold both sides with their roles. ## Replay a forecast with no look-ahead To test a rule on the past, use each game's forecast as it stood at the moment you would have acted. Here the moment is two hours before the start. Read the dataset once with `vintage=all`, and for each game keep the newest value whose `known_at` is at or before your moment. A value's `ts` is the game's start, so the moment is computed from the row itself. ```python from datetime import datetime, timedelta best = {} for row in pages("/observations", dataset="signals.sim.game", metric="p_home_win", vintage="all"): start = datetime.fromisoformat(row["ts"]) if datetime.fromisoformat(row["known_at"]) <= start - timedelta(hours=2): best[(row["entity"]["key"], row["ts"])] = row # rows arrive in vintage order: the last one kept is the newest for (home, ts), row in sorted(best.items(), key=lambda kv: kv[0][1]): print(ts, home, row["value"], "made", row["known_at"]) ``` A game with no entry in `best` had no forecast two hours before its start. The `known_at` test is what keeps the result honest: a forecast made later cannot be used. For one moment in time across every game, `as_of` does the same work on the server: ```python rows = list(pages("/observations", dataset="signals.sim.game", metric="p_home_win", as_of="2026-11-13T12:00:00Z", **{"from": "2026-11-13", "to": "2026-11-17"})) ``` ## Check forecasts against results The history datasets store each forecast beside what happened. This measures how often the side the model favoured won, in `signals.sim.game_history`. ```python from collections import defaultdict games = defaultdict(dict) for row in pages("/observations", dataset="signals.sim.game_history"): if row["metric"] in ("p_home_win", "final_home_points", "final_away_points"): games[row["event"]["key"]][row["metric"]] = row["value"] done = [g for g in games.values() if len(g) == 3 and g["final_home_points"] != g["final_away_points"]] right = sum((g["p_home_win"] > 0.5) == (g["final_home_points"] > g["final_away_points"]) for g in done) print(f"{right} of {len(done)} favoured sides won ({right / len(done):.1%})") ``` A fairer test of a probability is calibration: of the games given about a 70% chance, about 70% should be won. Group the games by forecast chance and compare each group's average chance with its share of wins. ## Follow a number through its revisions Official figures are revised. `vintage=all` returns each published value of a period, in order. ```python for row in pages("/observations", dataset="econ.jobs", metric="change", vintage="all", **{"from": "2026-03-01", "to": "2026-04-01"}): print(row["entity"]["key"], row["vintage"], row["known_at"][:10], row["value"], row["unit"]) ``` ## Keep a local copy up to date Read a dataset once, store it, and afterwards ask only for what was loaded since. [Following updates](/docs/updates) explains the method; this is the whole of it in code. ```python def sync(dataset, store, mark=None): """Bring `store` (a dict keyed by row identity) up to date. Returns the new mark to pass next time.""" params = {"dataset": dataset, "vintage": "all"} if mark: params["loaded_after"] = mark for row in pages("/observations", **params): store[(row["entity"]["kind"], row["entity"]["key"], row["metric"], row["ts"], row["vintage"])] = row if row["loaded_at"] and (mark is None or row["loaded_at"] > mark): mark = row["loaded_at"] return mark store, mark = {}, None mark = sync("signals.sim.game", store, mark) # the first call reads everything mark = sync("signals.sim.game", store, mark) # later calls read only what is new ``` Never replace a row: a revision is a new row with a higher `vintage`, and the earlier row is what was known before. ## Find a team or player ```python for e in get("/entities", dataset="signals.nba.prop_chance", kind="player", q="cunningham")["data"]: print(e["key"], e["name"]) ``` Keys differ between datasets, so look an entity up in the dataset you are about to read. --- # A Python client A small client you can copy into your project. It needs Python 3.9 or later and the `requests` package. It handles the key, paging, rate limits, retries and errors, and nothing else. ```python """A minimal client for the Sedona Strata API.""" import os import time import requests BASE = "https://sedonalabs.si/api/public/strata/v1" class StrataError(Exception): def __init__(self, status, code, message, param, request_id): super().__init__(f"{status} {code}: {message} (request {request_id})") self.status, self.code, self.param, self.request_id = status, code, param, request_id class Strata: def __init__(self, key=None, base=BASE, timeout=60, tries=5): self.base, self.timeout, self.tries = base.rstrip("/"), timeout, tries self.http = requests.Session() self.http.headers["X-API-Key"] = key or os.environ["STRATA_API_KEY"] self.remaining = None # requests left this minute, from the last answer def get(self, path, **params): params = {k.rstrip("_"): v for k, v in params.items() if v is not None} # from_="..." -> from for attempt in range(self.tries): r = self.http.get(self.base + path, params=params, timeout=self.timeout) self.remaining = r.headers.get("RateLimit-Remaining") if r.status_code == 200: return r.json() if r.status_code == 429: time.sleep(int(r.headers.get("Retry-After", "5"))) continue if r.status_code in (500, 503) and attempt + 1 < self.tries: time.sleep(2 ** attempt) continue try: err = r.json()["error"] except (ValueError, KeyError): err = {"code": "unknown", "message": r.text[:200]} raise StrataError(r.status_code, err.get("code"), err.get("message"), err.get("param"), r.headers.get("Request-Id")) raise StrataError(429, "rate_limited", "still rate limited after retries", None, None) def pages(self, path, **params): params.setdefault("limit", 5000) while True: page = self.get(path, **params) yield from page["data"] if not page["has_more"]: return params["cursor"] = page["next"] def whoami(self): return self.get("/") def datasets(self): return self.get("/datasets")["data"] def dataset(self, dataset_id): return self.get(f"/datasets/{dataset_id}")["data"] def status(self): return self.get("/status")["data"] def entities(self, dataset, **filters): return self.pages("/entities", dataset=dataset, **filters) def events(self, dataset, **filters): return self.pages("/events", dataset=dataset, **filters) def observations(self, dataset, **filters): return self.pages("/observations", dataset=dataset, **filters) ``` ## Using it ```python s = Strata() # reads STRATA_API_KEY from the environment print(s.whoami()["scope"]) for d in s.datasets(): print(d["id"], d["observations"]) for row in s.observations("signals.sim.game", metric="p_home_win", from_="2026-11-15", to="2026-11-16", as_of="2026-11-13T12:00:00Z"): print(row["ts"], row["entity"]["key"], row["value"], row["known_at"]) ``` `from` is a reserved word in Python, so the client accepts `from_` and sends `from`. ## Handling an error ```python try: list(s.observations("signals.sim.game", metrics="p_home_win")) # a misspelt parameter except StrataError as e: print(e.code, e.param, e.request_id) # unknown_parameter metrics req_... ``` ## Into a table With pandas, a dataset becomes a table in two lines. Each row of the API is one value, so pivot to get one row per entity and time with a column per metric. ```python import pandas as pd df = pd.json_normalize(list(s.observations("signals.sim.game", from_="2026-11-15", to="2026-11-16"))) wide = df.pivot_table(index=["ts", "entity.key"], columns="metric", values="value") ``` ## Other languages Any HTTP client works: the API is plain `GET` requests that answer JSON. To generate a client, use the OpenAPI description at `/openapi.json` under the base address. --- # Status and support ## Is the data arriving? [`GET /status`](/docs/api/status) is the first thing to check. For each dataset your key reads, it gives the latest time the data covers and how the last update went. | What you see | What it means | | --- | --- | | `last_run.status` is `written` and `finished_at` is recent | The dataset is being updated normally. | | `last_run.status` is `refused` | The newest batch failed a quality check and was not stored. The data from before still answers. This usually clears at the next update. | | `last_run.status` is `failed` | The update did not finish. The data from before still answers. | | Status 503 on every request | Strata's database cannot be reached. Retry with a pause. | Each dataset's page says when its new values normally arrive, so you can tell late from normal. ## When something looks wrong Before you write to us, three checks settle most questions: 1. **Read the dataset's page.** Its "Reading it" and "Limits" sections explain what `ts` and `known_at` mean there and what the dataset does not cover. Start from [every dataset and metric](/docs/datasets). 2. **Check `as_of` and `vintage`.** An answer that seems to be missing rows is often an honest point-in-time answer: the rows were not known at your `as_of`. See [point in time](/docs/point-in-time). 3. **Read the error's `code`.** [Errors](/docs/errors) says what each one means and what to do. ## Reporting a problem Send your Sedona contact: - the `request_id` of the request (it is in the `Request-Id` header of every answer, and in the body of every error); - the address you called, without your key; - what you expected, and what you got. Never send your API key in a message. We can find your requests from the `request_id`. ## Asking for something Your Sedona contact can also help with a higher rate limit, a new key, a dataset or metric you need that is not here, and questions about what your agreement allows.