Skip to content

Identifiers — bring your own URIs

Graph Cast is URI-agnostic: it never assigns, rewrites, or normalizes entity identifiers. Every entity — organization, outcome, indicator, indicator report, and anything else a capsule carries — is identified by an IRI you supply, stored verbatim, and returned verbatim in every read and export.

This will likely surprise you if you’re used to server-assigned IDs. The rules:

  1. Every entity carries its own IRI. In a capsule that’s the @id on each node; in the CRUD lane it’s the required uri field on every create. Creating without one is an error (422), and a capsule containing a blank-node core entity (no @id) is rejected with a message saying so.
  2. Identity is the IRI, exactly. Same IRI, either lane → one entity; re-submitting it replaces its properties (an upsert). A different IRI — even with an identical legal name — is a different entity. IRIs are compared byte-for-byte: case, trailing slashes, and whitespace all matter.
  3. Graph Cast will not guess that two IRIs refer to the same thing. Reconciliation is the data owner’s call, never the platform’s.
  4. IRIs never change. Updates (PUT) modify properties — a rename is a label change. There is no operation that moves an entity to a new IRI.

Use the organization’s own IRI everywhere. If a social purpose organization publishes capsules, its capsule @id (e.g. https://greenfield.example/Organization/ghc) is its identity. When a funder pre-creates that organization via CRUD — for example to attach form-based data before the first capsule arrives — it should create it under that same IRI, so everything converges on one entity:

Terminal window
curl -X POST $BASE/orgs \
-H "X-API-Key: $KEY" -H "X-Namespace: $NS" \
-H "Content-Type: application/json" \
-d '{"uri": "https://greenfield.example/Organization/ghc",
"name": "Greenfield Housing Co-op"}'

Pick stable, boring URIs for data you originate. For form or spreadsheet intake where no upstream IRI exists, mint your own under a domain you control and never change them — https://yourplatform.example/org/{slug}, …/report/{indicator}-{year}. Stable report URIs make re-imports and corrections free: same URI, new value, old value replaced.

The failure mode to design against: the same real-world organization loaded under two IRIs becomes two organizations, silently splitting its data across portfolio rollups. Agree on one IRI per entity across your intake paths before loading from multiple sources.

URLs like /orgs/{id} use an opaque id rather than the full IRI. The id is derived from the IRI — the first 128 bits of SHA-256 over the exact IRI string, base32-encoded (26 characters) — so it is stable and computable client-side without asking the server:

import base64, hashlib
def graph_cast_id(iri: str) -> str:
digest = hashlib.sha256(iri.encode()).digest()[:16]
return base64.b32encode(digest).decode().lower().rstrip("=")

Every API response includes both id and uri, so in practice you read ids from responses; the formula matters only when you want to address an entity knowing nothing but its IRI.

An entity URI must be an absolute http(s) IRI without characters that are illegal in IRIs (spaces, quotes, angle brackets). Anything else is rejected with a 400 explaining the problem. External codelist references — SDG IRIs, population-served codes, units — are stored verbatim as opaque identifiers, exactly as CIDS intends.