Working with capsules
An impact data capsule is a complete CIDS/SFF JSON-LD document — the interchange unit of the Common Approach ecosystem. Lab ingests capsules whole: validated, stored, and removed as one unit.
Upload and poll
Section titled “Upload and poll”Processing is asynchronous — POST returns immediately with 202 and a
capsule id; poll until the status is terminal:
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 |
failed |
could not be processed after retries; nothing was stored, error says why — resubmit |
deleted |
rolled back via DELETE /capsules/{id}, or superseded by a replacement |
Safe retries. Send an Idempotency-Key header (any string, unique per
submission within the namespace). Repeating the POST with the same key
returns the original capsule_id and its status with "replayed": true
instead of ingesting the file twice.
Corrections. Every capsule keeps its own triples — a new capsule never
overwrites an earlier one, even for the same IRIs. To resubmit a corrected
capsule, POST it with ?replaces={old capsule id}. The old capsule is
deleted only once the new one is accepted, so a rejected correction
changes nothing, and there is never a moment with neither.
curl -X POST "$BASE/capsules?replaces=$OLD_ID" \ -H "X-API-Key: $KEY" -H "X-Namespace: $NS" \ -H "Idempotency-Key: spo-42-fy2026-v2" \ --data-binary @capsule-corrected.jsonldSize. Bodies posted to /capsules travel through the API gateway and
are capped near 6 MB. Larger capsules go through
POST /capsules/uploads ({"filename": "…"} → a presigned upload; POST
the file to upload.url as a multipart form with upload.fields first
and the file last). The upload lands in storage directly and triggers the
same ingest, with the same capsule id to poll. 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.
Validation
Section titled “Validation”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:forThemevalues 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:value→i72:Measure→i72: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:
- Every core entity must carry its own
@id(organization, outcome, indicator, indicator report). Blank nodes are rejected — Lab never assigns identifiers. - Every outcome and indicator must be linked to an organization within
the capsule — via
hasOutcome/hasIndicatoron the organization, orforOrganizationon the entity. Unlinked entities could never be attributed to an organization in portfolio reads. - Every identifier must be a valid absolute IRI. Spaces, backticks,
quotes, angle brackets, or stray newlines inside an
@idor reference — common artifacts of spreadsheet exports — reject the capsule with each offending identifier named verbatim, so the producer knows exactly what to fix. Lab never repairs identifiers on your behalf.
Lab 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 Lab
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.
Report values: numeric and qualitative
Section titled “Report values: numeric and qualitative”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. Lab 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 presentQualitative reports are never dropped from table reads; they surface
with value: null. When aggregating, sum value and skip nulls.
Getting a capsule back
Section titled “Getting a capsule back”GET /capsules/{id}/file returns a short-lived download URL for the
original bytes exactly as submitted; GET /capsules/{id}/export?format=…
serialises only that capsule’s triples from the graph (see
Export). The file download works in any
namespace, including the console’s ~explorer file space; the export is a
graph read and works in graph namespaces only.
Rollback
Section titled “Rollback”curl -X DELETE $BASE/capsules/{capsule_id} \ -H "X-API-Key: $KEY" -H "X-Namespace: $NS" # → 204Everything 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).
Modelling funds
Section titled “Modelling funds”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 /facets/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.
