Part of specification v0.55.1. Earlier versions are preserved as tagged releases.
OpenRiC Viewing API
Last revised in: v0.54.0
1. Purpose
The Viewing API defines a REST + JSON-LD contract that any OpenRiC-conformant server exposes so that viewers, aggregators, and downstream consumers can retrieve RiC data consistently regardless of the server’s internal storage.
Design inspiration: the IIIF Presentation API. The server decides what to surface; the viewer decides how to render it. The contract between them is narrow and stable.
The reference implementation is openric/service, at ric.theahg.co.za. Where this spec and the reference implementation diverge, this spec is authoritative, and the divergence is listed in the drift log.
2. Conformance levels
Note (v0.37): The
L2-core/L2-graph/L2-query/L2-fulltable below is legacy terminology, retained for historical context. Current conformance claims SHOULD be profile-based -core-discovery,authority-context,graph-traversal,digital-object-linkage,round-trip-editing,provenance-event,export-only, andsparql-access(draft). Mapping:L2-core≈core-discovery;L2-graph≈graph-traversal+export-only;L2-query≈sparql-access. Theopenric_conformance.profilesdeclaration inGET /is the authoritative claim.
| Level | Requirement | Current profile equivalent |
|---|---|---|
| L2-core | /vocabulary, /records, /records/{id}, /agents, /agents/{id}, /repositories, /repositories/{id} |
core-discovery |
| L2-graph | L2-core + /graph, /records/{id}/export |
core-discovery + graph-traversal + export-only |
| L2-query | L2-core + /sparql, /validate |
core-discovery + sparql-access (draft) |
| L2-full | All of the above | All seven normative profiles + sparql-access |
Level is advertised in the service description (§6) - but new implementations SHOULD declare profiles in openric_conformance.profiles instead.
3. Base URL
All endpoints live under:
{scheme}://{host}/api/ric/v1/
HTTPS is REQUIRED in production. Implementations MAY serve HTTP for local development.
3.1 Semantic URIs vs API endpoints
OpenRiC distinguishes between stable semantic entity URIs (the linked-data identity of an archival entity) and API endpoints (one access mechanism for retrieving, editing, validating, or exporting that entity).
| Layer | Pattern | Purpose |
|---|---|---|
| Internal database ID | 12345 |
Local persistence only. |
| API endpoint | https://archives.example.org/api/ric/v1/agents/12345 |
Application access - defined by this spec. |
| Semantic URI (recommended) | https://archives.example.org/id/agent/12345 |
Linked-data identity - dereferences via content negotiation (§7). |
| External authority URI | https://rdf.archives-nationales.culture.gouv.fr/agent/009941 |
Identity in an external authority dataset (e.g. Garance, Wikidata, GeoNames). |
| Reconciliation link | skos:exactMatch / skos:closeMatch / owl:sameAs |
Identity / alignment relationship between local and external URIs. |
Recommended URI pattern for the semantic identity layer:
{scheme}://{host}/id/{kind}/{id}
Where {kind} is one of record, record-set, record-part, agent, person, corporate-body, family, mechanism, place, rule, activity, instantiation, function. Implementations MAY serve the semantic URI as a 303-See-Other redirect to the content-negotiated representation (HTML, JSON-LD, Turtle, RDF/XML).
A server MAY expose only the API layer (/api/ric/v1/...) and treat that as both identity and access - but doing so couples the linked-data identity to the implementation API version. The two-layer pattern is RECOMMENDED for institutions that want their entity URIs to outlive any specific OpenRiC version.
External authority URIs MUST be preserved as external identifiers - implementations MUST NOT silently replace them with locally-minted URIs without retaining the original via skos:exactMatch / skos:closeMatch / owl:sameAs. See §7.3 below for the reconciliation-relation guidance.
3.2 Content negotiation
Every entity URI (semantic or API) SHOULD support content negotiation:
Accept header |
Response |
|---|---|
text/html (or browser default) |
Human-readable entity page (recommended) |
application/ld+json |
JSON-LD (REQUIRED in core profiles) |
text/turtle |
Turtle |
application/rdf+xml |
RDF/XML (RECOMMENDED for archival interoperability) |
application/n-triples |
N-Triples (OPTIONAL) |
application/json |
API JSON representation (when distinct from JSON-LD) |
Servers MUST set Vary: Accept on every response that content-negotiates and SHOULD support 303-See-Other redirects from the semantic URI to the content-specific representation (e.g. /id/agent/123 → /api/ric/v1/agents/123.jsonld).
Implementations MAY initially support only application/ld+json and application/json; the broader set is a strong RECOMMENDATION at the Linked-Data Publication tier and is required in the (forthcoming) Linked-Data Publication Profile.
4. Endpoints
4.1 Service description
GET /api/ric/v1/
Returns a JSON description of the server’s capabilities, conformance level, and endpoint catalogue. MUST be returned without authentication.
{
"name": "Example Archive Catalogue",
"version": "2.4.1",
"openapi": "https://archives.example.org/api/ric/v1/openapi.json",
"openric_conformance": {
"spec_version": "0.55.1",
"profiles": [
{ "id": "core-discovery", "version": "0.7.0", "conformance": "full" },
{ "id": "graph-traversal", "version": "0.6.0", "conformance": "full" }
]
}
}
version is the implementation’s own release. openric_conformance is the conformance claim; Conformance §2 describes it.
4.2 Vocabulary
GET /api/ric/v1/vocabulary
Returns the subset of RiC-O the server actually emits, plus any OpenRiC extension terms it supports. Allows clients to discover server capabilities before constructing queries.
4.3 Records (information objects)
| Method & path | Purpose |
|---|---|
GET /records |
Paginated list of records |
GET /records/{id} |
Single record as RiC-O JSON-LD |
GET /records/{id}/export |
Full RecordSet export (record + all descendants + related agents) |
List parameters:
| Param | Meaning | Default | Max |
|---|---|---|---|
page |
Page number (1-based) | 1 |
- |
limit |
Items per page | 50 |
200 |
level |
Filter by level-of-description (fonds, series, file, item) |
- | - |
q |
Free-text search on title + identifier | - | - |
List response:
{
"@context": "https://openric.org/ns/v1/context.jsonld",
"@type": "openricx:RecordSetList",
"openric:total": 1423,
"openric:page": 1,
"openric:limit": 50,
"openric:items": [
{ "@id": ".../AHG-A001", "@type": "rico:RecordSet",
"rico:identifier": "AHG-A001", "rico:title": "Papers of JC Smuts" }
],
"openric:next": ".../records?page=2&limit=50"
}
4.4 Agents (actors)
| Method & path | Purpose |
|---|---|
GET /agents |
Paginated list of agents |
GET /agents/{id} |
Single agent as RiC-O JSON-LD |
List parameters:
| Param | Meaning |
|---|---|
type |
person, corporate body, family |
q |
Free-text search on name |
page, limit |
Pagination |
4.5 Repositories
| Method & path | Purpose |
|---|---|
GET /repositories |
Paginated list of repositories |
GET /repositories/{id} |
Single repository (rico:CorporateBody per ISDIAH) |
4.6 Functions (optional - L2-full)
| Method & path | Purpose |
|---|---|
GET /functions |
Paginated list of ISDF functions |
GET /functions/{id} |
Single function as openricx:Function |
4.7 Graph
GET /api/ric/v1/graph?uri={entity-uri}&depth={N}
Returns a subgraph rooted at uri, suitable for visualisation clients. Response shape defined in Graph Primitives:
{
"@context": "https://openric.org/ns/v1/context.jsonld",
"@type": "openric:Subgraph",
"openric:root": "https://archives.example.org/actor/smuts-jc",
"openric:depth": 2,
"openric:nodes": [
{ "id": ".../actor/smuts-jc", "type": "rico:Person",
"label": "Smuts, Jan Christian" },
{ "id": ".../informationobject/AHG-A001", "type": "rico:RecordSet",
"label": "Papers of JC Smuts" }
],
"openric:edges": [
{ "source": ".../informationobject/AHG-A001",
"target": ".../actor/smuts-jc",
"predicate": "rico:hasCreator",
"label": "created by" }
]
}
Parameters:
| Param | Meaning | Default | Max |
|---|---|---|---|
uri |
Root entity URI (REQUIRED) | - | - |
depth |
BFS depth from root | 1 |
3 |
direction |
in, out, both |
both |
- |
types |
Comma-separated filter of node RiC types | - | - |
4.8 SPARQL
SPARQL is not required by any normative profile. A server that offers it does so under the draft SPARQL Access profile, which defines /sparql, /sparql/info, the access posture, rate limits and read-only behaviour, and which the conformance probe tests when a server declares it. A server that does not declare the profile need not expose /sparql, and no client should assume it. See guides/triplestore-choice.md for backing stores.
For the common graph-walk case, use /graph?uri=…&depth=N - it’s stable across every conformant server, SHACL-validated under Graph Traversal, and never degrades into an unbounded query.
4.9 Validate (optional - L2-query)
POST /api/ric/v1/validate
Content-Type: application/ld+json
Body: <candidate RiC-O JSON-LD>
Validates the candidate graph against the server’s SHACL shapes. Returns a ValidationReport:
{
"@type": "sh:ValidationReport",
"sh:conforms": false,
"sh:result": [
{
"sh:focusNode": ".../records/xyz",
"sh:resultPath": "rico:title",
"sh:resultMessage": "RecordSet must have exactly one non-empty title",
"sh:resultSeverity": "sh:Violation"
}
]
}
4.10 Health
GET /api/ric/v1/health
Returns { "status": "ok" } with HTTP 200 when the server is reachable and its backing store is healthy. Non-authenticated. Intended for monitoring.
4.11 RiC-native entities - Places, Rules, Activities, Instantiations
These four entity types are first-class RiC-O resources that exist independently of records. Each gets a list, a show, and (for Places) a flat-name-and-id endpoint suitable for parent pickers.
GET /api/ric/v1/places
GET /api/ric/v1/places/{id}
GET /api/ric/v1/places/flat?exclude_id={id}
GET /api/ric/v1/rules
GET /api/ric/v1/rules/{id}
GET /api/ric/v1/activities
GET /api/ric/v1/activities/{id}
GET /api/ric/v1/instantiations
GET /api/ric/v1/instantiations/{id}
List endpoints accept page, per_page, and OPTIONAL type filters matching the taxonomy (e.g. ?type_id=country). Show endpoints return full rico:Place / rico:Rule / rico:Activity / rico:Instantiation serialisations with any owl:sameAs authority links.
/places/flat returns {items: [{id, name}], count} - small-payload parent-picker companion. Unpaginated; bounded by server-side policy.
4.12 Relations
GET /api/ric/v1/relations?q={query}&page={p}&per_page={n}
GET /api/ric/v1/relations-for/{entity-id}
GET /api/ric/v1/relation-types?domain={class}&range={class}
/relationsis the paginated global list.qmatches againstrdf:predicate,dropdown_code, and evidence text./relations-for/{id}returns{outgoing: [...], incoming: [...], total, entity_id}grouped by direction. Use this on entity show-pages./relation-typesreturns the catalog of allowed relation predicates, optionally filtered by domain/range class.
4.13 Hierarchy walk
GET /api/ric/v1/hierarchy/{entity-id}?include=parent,children,siblings
For Places, returns parent + children + siblings from ric_place.parent_id. For other entity types, walks ric_relation_meta.dropdown_code IN (has_part, includes, is_superior_of, is_part_of, is_included_in). Use include to restrict the walk.
4.14 Autocomplete
GET /api/ric/v1/autocomplete?q={query}&types={types}&limit={n}
Cross-entity name/title search. types is a comma-delimited subset of place,rule,activity,instantiation,actor,io,repository,digital_object - omit for all. Returns [{id, label, type}], capped at limit (default 20, max 200).
4.15 Vocabulary (single-taxonomy)
GET /api/ric/v1/vocabulary/{taxonomy}
Returns {taxonomy, items: [{code, label, color, icon, is_default, metadata}], count}. {taxonomy} is a dropdown name like ric_place_type, ric_rule_type, ric_activity_type, ric_carrier_type, ric_relation_type, certainty_level. 404 when the taxonomy isn’t defined.
4.16 Records - linked RiC entities
GET /api/ric/v1/records/{id}/entities?types={types}
Aggregates every RiC-native entity linked to a given record (via the relation table) into {places: [...], rules: [...], activities: [...], instantiations: [...]}. types is the same comma-delimited filter as /autocomplete.
4.17 Entity info card
GET /api/ric/v1/entities/{id}/info
Minimal JSON-LD entity card: {id, class, slug, name, type, description}. For popovers / hover tooltips / autocomplete result expansion. Cheap to fetch.
4.18 Write operations (added in v0.2.0; L3 conformance)
All write operations require X-API-Key with a scope matching the operation. See §6 (Authentication) for details.
4.18.1 Create / update / delete RiC-native entities
POST /api/ric/v1/{type} scope: write
PATCH /api/ric/v1/{type}/{id} scope: write
PUT /api/ric/v1/{type}/{id} scope: write (alias of PATCH)
DELETE /api/ric/v1/{type}/{id} scope: delete
{type} ∈ agents | records | repositories | functions | places | rules | activities | instantiations. Body keys match the DB schema for the entity (e.g. for Place: name, type_id, latitude, longitude, authority_uri, parent_id, address, description). Type picker values come from /vocabulary/{taxonomy}.
Success responses:
| Operation | Status | Body |
|---|---|---|
| POST | 201 Created | {id, slug, type, href} |
| PATCH/PUT | 200 OK | {success: true, id} |
| DELETE | 200 OK | {success: true, id} |
4.18.2 Create / update / delete relations
POST /api/ric/v1/relations scope: write
PATCH /api/ric/v1/relations/{id} scope: write
DELETE /api/ric/v1/relations/{id} scope: delete
POST body: {subject_id, object_id, relation_type} plus optional {start_date, end_date, certainty, evidence}. relation_type is a code from ric_relation_type vocabulary.
If the underlying ric_relation_meta marks the relation as symmetric, the server MUST create a mirror inverse-direction relation at the same time (same id space, linked via ric_relation_meta.inverse_predicate). Both sides are visible in /relations-for/{id} output.
4.18.3 Delete by id (type-agnostic)
DELETE /api/ric/v1/entities/{id} scope: delete
Convenience endpoint for UIs that hold a numeric entity id but not its type. Server looks up object.class_name and dispatches to the appropriate delete handler. Returns 422 for entities that aren’t one of the four RiC-native types.
5. Request and response formats
5.1 Content negotiation
Accept header |
Response |
|---|---|
application/ld+json (default) |
RiC-O JSON-LD |
application/json |
Same as above - for clients that can’t set ld+json |
text/turtle |
RiC-O Turtle |
application/rdf+xml |
RDF/XML (optional) |
Servers MUST support application/ld+json. Other formats are OPTIONAL.
5.2 Language negotiation
Accept-Language selects the culture for rico:title, openricx:description, etc. Servers SHOULD honour the header. When a requested language is unavailable, the server MUST fall back to the entity’s sourceCulture.
5.3 CORS
All GET endpoints MUST send:
Access-Control-Allow-Origin: *
This enables browser-based viewers hosted on other domains.
Write endpoints (POST, PATCH, PUT, DELETE) SHOULD also return Access-Control-Allow-Origin: * and handle the CORS preflight (OPTIONS) with:
Access-Control-Allow-Methods: GET, POST, PUT, PATCH, DELETE, OPTIONS
Access-Control-Allow-Headers: Content-Type, X-API-Key, X-REST-API-Key, Authorization, Accept
Access-Control-Max-Age: 86400
Without this, browser-based capture clients (like capture.openric.org) can’t write to the server.
6. Authentication
Reads - public by default. /health, /, /vocabulary*, and the read-side entity + relation + graph endpoints SHOULD be reachable without credentials. An ODRL-based rights-enforcement layer (OpenRiC-Rights, forthcoming) will define how to expose per-record access controls for per-record private reads.
Writes - authenticated via X-API-Key header. Alternative header names that SHOULD be accepted: X-REST-API-Key, Authorization: Bearer <key>.
API keys carry a list of scopes. The required scope per operation:
| Operation | Required scope |
|---|---|
POST /{type}, POST /relations |
write |
PATCH /{type}/{id}, PATCH /relations/{id} |
write |
DELETE /{type}/{id}, DELETE /relations/{id}, DELETE /entities/{id} |
delete |
Missing key → 401 Unauthorized. Key present but missing scope → 403 Forbidden. Both responses MUST be JSON: {error: "unauthorized", message: "…"}.
Key issuance is operator-defined - the spec doesn’t mandate a particular UI or workflow. The reference implementation issues SHA-256-hashed keys with per-key scope lists and optional expiry. See the reference ahg-api package for a model.
7. Pagination
All list endpoints use the same pattern:
{
"openric:total": 1423,
"openric:page": 1,
"openric:limit": 50,
"openric:next": ".../records?page=2&limit=50",
"openric:prev": null,
"openric:items": [ … ]
}
next and prev are absolute URLs or null. Clients SHOULD follow next rather than construct their own URLs.
8. Error responses
Errors MUST use HTTP status codes correctly and return an RFC 7807 problem document with Content-Type: application/problem+json, as Core Discovery §4 requires:
{
"type": "https://openric.org/errors/not-found",
"title": "Not Found",
"status": 404,
"detail": "No record with identifier 'AHG-XYZ' exists.",
"instance": "/api/ric/v1/records/AHG-XYZ"
}
8.5 Pagination envelope policy
OpenRiC defines two list-envelope shapes - implementations MUST use the right shape for the right endpoint class:
| Endpoint class | Envelope | Required keys |
|---|---|---|
| JSON-LD list endpoints (Records, Agents, Places, Rules, Activities, Instantiations, Functions, Repositories) | JSON-LD with @context and openric: prefix |
@context, @type (one of openricx:RecordList, openricx:AgentList, …), openric:total, openric:page, openric:limit, openric:items, optionally openric:next / openric:prev |
| Plain JSON convenience endpoints (revision lists, relation lists, autocomplete, vocabulary lookups) | Plain JSON, no @context |
total, limit, offset OR page/per_page, items. Servers MAY use either offset- or page-style pagination but MUST be internally consistent within a single endpoint. |
| OAI-PMH | OAI-PMH XML protocol envelopes only | per OAI-PMH 2.0 spec |
| Autocomplete | Plain JSON | query, items (array of {id, label, type} triples), limit |
A server MUST NOT mix the two shapes within a single endpoint (e.g., emit openric:items alongside plain total). When in doubt: if the endpoint returns RDF entities, use the JSON-LD envelope; if it returns operational metadata (revisions, audit, autocomplete), use the plain envelope.
8.6 Node type CURIE policy
Graph and list responses use CURIEs (prefixed names like rico:Person, rico:RecordSet, openricx:Function) for @type and Node.type values, not bare local names. Bare local names ("Person") MUST NOT be emitted - clients cannot disambiguate rico:Person from foaf:Person.
Where a server wants to expose its implementation-specific subtype (e.g. "box" for a rico:Thing carrier kind), use the dedicated openric:localType slot, not the canonical type field.
9. Rate limiting
Servers SHOULD rate-limit. The reference implementation uses 60 requests / minute / IP. When limiting, servers MUST return HTTP 429 with Retry-After header.
10. OpenAPI description
Every conformant server MUST expose a valid OpenAPI 3.x description of its own surface at:
GET /api/ric/v1/openapi.json
This enables auto-generated clients, Postman collections, and CI validation. The specification’s own API contract is OpenAPI 3.1.
11. Change log
| Version | Date | Notes |
|---|---|---|
| 0.1.0-draft | 2026-04-17 | Initial draft extracted from Heratio LinkedDataApiController. |
| v0.46.1 | 2026-10-15 | Service description example in the current openric_conformance shape; errors are RFC 7807, as Core Discovery has required since v0.30.0; SPARQL points to the SPARQL Access draft profile; writes listed for every entity type. |