Skip to content

Working with capsules

An impact data capsule is a complete CIDS/SFF JSON-LD document — the interchange unit of the Common Approach ecosystem. Graph Cast ingests capsules whole: validated, stored, and removed as one unit.

Processing is asynchronous — POST returns immediately with 202 and a capsule id; poll until the status is terminal:

Terminal window
curl -X POST $BASE/capsules \
-H "X-API-Key: $KEY" -H "X-Namespace: $NS" \
-H "Content-Type: application/json" \
--data-binary @capsule.jsonld
# → {"capsule_id": "…", "status": "pending"}
curl -H "X-API-Key: $KEY" -H "X-Namespace: $NS" \
$BASE/capsules/{capsule_id}
# → {"status": "accepted", "triple_count": 315, "validation": {…}}
Status Meaning
pending queued or processing
accepted validated and stored; entities are queryable
rejected validation failed; nothing was stored
deleted rolled back via DELETE /capsules/{id}

Capsules up to 10 MiB are accepted. Both common shapes parse: a top-level {"@context": …, "@graph": […]} document, or a bare JSON array of node objects each carrying its own @context. The official Common Approach context URLs are resolved from bundled copies — ingest never fetches over the network.

Capsules are validated against a focused subset of the official CIDS Basic Tier SHACL shapes. Violations reject the whole capsule and store nothing; warnings are accepted and reported. The validation report is written in plain language, intentionally suitable for forwarding verbatim to the organization that produced the file:

{"severity": "violation",
"message": "An organization in the capsule has no legal name…",
"focus": "https://vendor.example/Organization/acme"}

What rejects a capsule (severity: violation):

  • An organization without exactly one non-empty legal name (org:hasLegalName).
  • An outcome or indicator without exactly one non-empty name (org:hasName).
  • An outcome whose cids:forTheme values are not IRIs — themes must be codelist IRIs (e.g. an SDG URL), not text values or blank nodes.
  • An indicator report missing cids:forIndicator, missing a value (i72:valuei72:Measurei72:hasNumericalValue), or missing its reporting period (prov:startedAtTime / prov:endedAtTime).

What’s reported as a warning (capsule still accepted):

  • An organization without a description.
  • An indicator that doesn’t reference the outcome it measures (cids:forOutcome).

Three structural rules beyond the shapes:

  1. Every core entity must carry its own @id (organization, outcome, indicator, indicator report). Blank nodes are rejected — Graph Cast never assigns identifiers.
  2. Every outcome and indicator must be linked to an organization within the capsule — via hasOutcome/hasIndicator on the organization, or forOrganization on the entity. Unlinked entities could never be attributed to an organization in portfolio reads.
  3. Every identifier must be a valid absolute IRI. Spaces, backticks, quotes, angle brackets, or stray newlines inside an @id or reference — common artifacts of spreadsheet exports — reject the capsule with each offending identifier named verbatim, so the producer knows exactly what to fix. Graph Cast never repairs identifiers on your behalf.

Graph Cast does not enforce every constraint in the Basic Tier file: cids:unitDescription on indicators, org:hasName and cids:forOrganization on indicator reports, the required Address fields, and the xsd:dateTime typing of report periods are accepted as-is today. A capsule that conforms to the official Basic Tier shapes passes Graph Cast validation, with one deliberate exception: entities and themes carried as blank nodes are rejected, because every entity must have its own IRI.

Everything else the capsule carries — organization profiles, addresses, people, team and EDG profiles, funding statuses, characteristics — is stored verbatim, included in exports, and preserved even though it has no dedicated CRUD endpoints yet.

CIDS carries report values as strings (i72:hasNumericalValue is xsd:string in the official shapes), and real-world capsules routinely contain qualitative values — “Yes”, narrative text. Graph Cast keeps both readings, everywhere a report appears:

"value": 94, // parsed decimal — null when the string isn't numeric
"value_text": "94" // the verbatim CIDS string — always present

Qualitative reports are never dropped from table reads; they surface with value: null. When aggregating, sum value and skip nulls.

Terminal window
curl -X DELETE $BASE/capsules/{capsule_id} \
-H "X-API-Key: $KEY" -H "X-Namespace: $NS" # → 204

Everything that capsule asserted is removed in one atomic operation. Entities asserted by multiple capsules survive until the last asserting capsule is deleted. Entities created via CRUD are untouched by capsule rollback (and vice versa: CRUD deletes don’t remove capsule-sourced data — capsules are removed only by rollback).

To make an organization filterable by fund, submit a small capsule of sff:FundingStatus nodes whose forOrganization points at the organization’s IRI:

[{"@context": ["https://ontology.commonapproach.org/contexts/cidsContext.jsonld",
"https://ontology.commonapproach.org/contexts/sffContext.jsonld"],
"@type": "sff:FundingStatus",
"@id": "https://yourfund.example/funding-status/ghc-2026",
"forOrganization": "https://greenfield.example/Organization/ghc",
"forFunder": "Horizon Impact Fund",
"hasFundingState": "https://codelist.commonapproach.org/FundingState#Invested"}]

The fund= filter and the /rows/funds facet read these directly. Note forFunder is a string in the official SFF context, so fund identity is exact-string matching — spell funder names consistently.