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. Lab 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
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.

Terminal window
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.jsonld

Size. 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.

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: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:

  1. Every core entity must carry its own @id (organization, outcome, indicator, indicator report). Blank nodes are rejected — Lab 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. 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.

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 present

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

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.

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 /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.