See OpenRiC live — 2D/3D graph against a real archival dataset. See it in action ↗

OpenRiC Viewing API

Version: 0.37.0 Status: Active — RiC-O 1.1 namespace remediation complete Last updated: 2026-04-25


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.

A reference implementation exists in the Heratio ahg-ric package’s LinkedDataApiController. Where this spec and the reference implementation diverge, this spec is authoritative.

2. Conformance levels

Note (v0.37): The L2-core / L2-graph / L2-query / L2-full table below is legacy terminology, retained for historical context. Current conformance claims SHOULD be profile-basedcore-discovery, authority-context, graph-traversal, digital-object-linkage, round-trip-editing, provenance-event, export-only, and sparql-access (draft). Mapping: L2-corecore-discovery; L2-graphgraph-traversal + export-only; L2-querysparql-access. The openric_conformance.profiles declaration in GET / 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.

{
  "@context": "https://openric.org/ns/v1/context.jsonld",
  "@type": "openric:Service",
  "openric:version": "0.1.0",
  "openric:conformance": ["L2-core", "L2-graph"],
  "openric:endpoints": {
    "records": "/api/ric/v1/records",
    "agents":  "/api/ric/v1/agents",
    "graph":   "/api/ric/v1/graph",
    "sparql":  "/api/ric/v1/sparql"
  },
  "openric:implementation": {
    "name": "Heratio",
    "version": "0.93.119",
    "url": "https://github.com/ArchiveHeritageGroup/heratio"
  }
}

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 (non-normative — outside the OpenRiC contract)

Status as of v0.36.0: SPARQL is not part of the OpenRiC conformance contract, by design. OpenRiC is a purpose-built API for working archives (like IIIF for images); exposing SPARQL directly duplicates /graph?uri=…&depth=N for the common case, opens a DoS surface that’s expensive to harden, and competes with the backing triplestore’s own SPARQL endpoint for no net gain to an OpenRiC client. Clients that need SPARQL have better tools: run queries against the Turtle dump from /export?format=ttl in an offline processor, or talk directly to the implementation’s triplestore if it exposes one.

Implementations MAY expose a SPARQL endpoint at an implementation-specific path (for example /sparql-direct, /admin/sparql, or directly at their Fuseki / GraphDB / Oxigraph host). This is entirely out of scope for OpenRiC:

The reference implementation historically carried /api/ric/v1/sparql as a stub; that route is retained for backwards compatibility with any consumer that wired to it, but it is not and will not become part of the normative surface unless a future sparql-query profile is defined in response to concrete implementer demand. See guides/triplestore-choice.md for a comparison of backing stores implementers may choose.

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}

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}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 a JSON error body:

{
  "@type": "openric:Error",
  "openric:status": 404,
  "openric:code": "not-found",
  "openric:message": "No record with identifier 'AHG-XYZ' exists.",
  "openric:detail": ".../records/AHG-XYZ"
}

Codes: not-found, forbidden, bad-request, rate-limited, unavailable, internal.

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.0 description at:

GET /api/ric/v1/openapi.json

This enables auto-generated clients, Postman collections, and CI validation.

11. Change log

Version Date Notes
0.1.0-draft 2026-04-17 Initial draft extracted from Heratio LinkedDataApiController.

Back to OpenRiC