Querying impact data
Lab has two ways to read, matching the two questions a portfolio holder asks: “show me the evidence across everyone” (table reads) and “show me this organization’s model” (tree reads). All the graph joins happen server-side — you receive ready-to-render JSON.
Table reads
Section titled “Table reads”GET /rows/indicator-reports ?theme=<IRI> repeatable — OR within the parameter &fund=<funder name> repeatable — OR within the parameter &org=<org id> &period[from]=2025-01-01 &period[to]=2025-12-31 &limit=50&cursor=<opaque>Different parameters AND together; repeating one parameter ORs its values. Each row is fully denormalized:
{ "data": [{ "object": "indicator_report", "id": "…", "value": 94, "value_text": "94", "unit": "%", "period_start": "2025-01-01", "period_end": "2025-12-31", "themes": ["https://metadata.un.org/sdg/1"], "indicator": {"id": "…", "name": "Tenant retention rate (%)"}, "outcome": {"id": "…", "name": "Housing stability"}, "org": {"id": "…", "name": "Greenfield Housing Co-op"} }], "meta": {"row_count": 1, "cursor": null, "cids_version": "cids-3.0", "generated_at": "…"}}Pagination: pass meta.cursor back as ?cursor=; null means done.
Loop on the cursor, never on row_count. For a scheduled full pull use
the snapshot export instead of paging.
Every row also carries the caller’s own uri for itself and for the
indicator, outcome, and org it references, plus capsule_id and
ingested_at naming the capsule it came from (null for entities created
through CRUD calls). meta.version is the namespace’s data version — it
changes on every write — and meta.schema_version changes only when a
field is removed or renamed.
The other tables
Section titled “The other tables”The same envelope, paging, and uri/capsule_id columns are available
for every core type, so a warehouse can load dimension tables without
walking organizations one at a time:
| Path | Row | Filters |
|---|---|---|
/rows/organizations |
one organization | — |
/rows/outcomes |
one outcome with its themes and org | org |
/rows/indicators |
one indicator with unit, outcome, org | org |
/rows/funding-statuses |
one sff:FundingStatus: funder, state, org |
org |
Snapshot export
Section titled “Snapshot export”GET /rows/{table}/export If-None-Match: "<meta.version from the last pull>" # optionalThe whole table as one file, built once per data version and served from storage, so a scheduled pull never queries the graph:
- 200
{"url": "…", "version": "…", "row_count": N, "generated_at": "…", "schema_version": "1", "expires_in": 900}— downloadurl(valid 15 minutes). The file is the paged envelope with every row indata. - 304 — nothing changed since the version you sent.
- 202
{"status": "building", "retry_after": 15}— something changed and the snapshot is being rebuilt; retry afterRetry-Afterseconds.
Snapshots refresh as soon as a capsule is accepted or deleted; after CRUD edits the first pull triggers the rebuild.
Period semantics — overlap. period[from]/period[to] match any
report whose period intersects the range, so an annual report spanning a
year-end appears in both years’ queries. That matches the “what happened
during FY2025” intuition.
Theme semantics. A row’s themes collects themes attached to the
indicator’s outcome and directly to the indicator (both are legal CIDS).
Three consequences worth knowing before building a dashboard:
- An outcome with two themes puts its reports under both — theme-group totals can legitimately exceed the report total.
- Reports whose indicator links to no outcome (and carries no theme of its
own) have
themes: []and are unreachable by anytheme=filter. The themes facet tells you how many exist. - Theme filter values are IRIs, URL-encoded. Nobody types them — copy them from the facet endpoint.
Fund semantics. fund= matches reports from organizations that have an
sff:FundingStatus naming this funder. See
modelling funds.
Facets
Section titled “Facets”Small endpoints that answer “what can I filter by?” — the feed for any dashboard’s filter pickers, with values copy-paste ready for the corresponding filter:
GET /facets/themes# → {"data": [{"theme": "https://metadata.un.org/sdg/11", "report_count": 39}, …],# "meta": {"total_reports": 2030, "unthemed_reports": 12}}
GET /facets/funds# → {"data": [{"fund": "Horizon Impact Fund", "org_count": 11, "report_count": 143}, …]}meta.unthemed_reports counts the reports no theme filter can reach. The
older paths /rows/themes and /rows/funds still answer the same way.
Tree reads
Section titled “Tree reads”One organization’s full theory of change as a nested document:
GET /orgs/{id}?expand=outcomes.indicators.reportsGET /orgs/{id}/everything # shorthand for the full expansionexpand takes comma-separated dotted paths (a path implies its prefixes):
outcomes, outcomes.indicators, outcomes.indicators.reports,
indicators, indicators.reports. Without expand, GET /orgs/{id}
returns the plain organization.
{ "object": "organization", "id": "…", "uri": "…", "name": "Greenfield Housing Co-op", "outcomes": [{ "object": "outcome", "name": "Housing stability", "themes": ["https://metadata.un.org/sdg/1"], "indicators": [{ "object": "indicator", "name": "Tenant retention rate (%)", "unit": "%", "reports": [{"value": 94, "value_text": "94", "period_start": "2025-01-01", "period_end": "2025-12-31"}] }] }], "indicators": []}An indicator appears exactly once per tree. Indicators linked to an
outcome nest under it; indicators with no outcome link (optional in CIDS)
surface in the organization-level indicators array rather than being
dropped. Every object self-identifies via "object".
Aggregation
Section titled “Aggregation”Rows are small and fully denormalized on purpose: group-by is a few lines
client-side, and qualitative reports (value: null) stay visible instead
of skewing sums. For example, numeric totals per theme:
import collections, requests
rows = requests.get(f"{BASE}/rows/indicator-reports?limit=500", headers=headers).json()["data"]totals = collections.defaultdict(float)for r in rows: if r["value"] is not None: for theme in r["themes"]: totals[theme] += r["value"]Server-side aggregation is planned.
Export
Section titled “Export”GET /export?format=jsonld|nquads # everything in the namespaceGET /capsules/{id}/export?format=… # one capsule's triples, as ingestedGET /orgs/{id}/export?format=… # one organization's subgraphThe organization-scoped export selects the org, its outcomes and
indicators, their reports and measure values, plus anything pointing at the
org via forOrganization (profiles, funding statuses). Exports return the
exact identifiers that were submitted.
Export endpoints do not return the serialization inline. They answer with a short-lived download link:
{"url": "https://…/exports/…", "expires_in": 900, "content_type": "application/ld+json", "size_bytes": 123456}Fetch the payload from url within 15 minutes — no credentials needed:
curl -s -H "X-API-Key: $KEY" -H "X-Namespace: $NS" \ "$BASE/export?format=jsonld" | jq -r .url | xargs curl -s > export.jsonldJSON-LD exports use the CIDS interchange shape — one
{"@context": […], "@graph": […]} document compacted against the published
contexts — so an export can be re-posted as a capsule.
For a filtered export (chosen organizations, a reporting period) use a background job: the result is one CIDS document, kept under the job for re-download.
