This document describes how an autonomous agent (or any HTTP/LLM client) drives a running LinkedDataHub (LDH) instance's HTTP API. It is the API-usage counterpart to CLAUDE.md (which is for contributing to the codebase).
LinkedDataHub is a data-driven Knowledge Graph platform. Everything — documents, applications, access control, the UI — is RDF, managed over a small, uniform HTTP API and standard protocols. There is no bespoke REST surface to learn: you work with RDF documents and SPARQL.
- The content is a hierarchy of documents (containers and items). A container holds child documents; items are leaves.
- Every document URL is a named graph. Reading a document returns the RDF in that graph; writing changes it. This is the SPARQL 1.1 Graph Store Protocol.
- Identifiers are opaque URLs. Do not parse structure out of them; follow links (hypermedia) instead.
- The vocabularies LDH defines live under
https://w3id.org/atomgraph/linkeddatahub:
| Prefix | Namespace | Holds |
|---|---|---|
ldh: |
https://w3id.org/atomgraph/linkeddatahub# |
content blocks, views, charts, imports, packages |
dh: |
https://w3id.org/atomgraph/linkeddatahub/document-hierarchy# |
dh:Container, dh:Item |
lds: |
https://w3id.org/atomgraph/linkeddatahub/dataspaces# |
dataspaces, services, ontologies |
lacl: |
https://w3id.org/atomgraph/linkeddatahub/admin/acl# |
the admin ACL extensions to acl: |
ac: is https://w3id.org/atomgraph/client# (Web-Client's presentation vocabulary) and acl: is http://www.w3.org/ns/auth/acl#.
- WebID-TLS (client certificate) is the primary mechanism for programmatic agents. Every request carries the cert; the certificate's WebID is the agent identity. With
curl:-E cert.pem:password(-kin dev with self-signed certs). - OAuth2 (Google) and OpenID Connect (ORCID) are available for human logins.
- Delegation: an authorized secretary agent can act for a principal via the
On-Behalf-Of: <principal-WebID>request header. - Authorization is WebID-based ACLs (
acl:Read/Append/Write/Control), enforced per document. - Every response advertises its context in
Linkheaders, whose relation types are the property URIs:acl:agent(the authenticated agent's WebID), oneacl:modeper access mode the agent holds on this resource,sd:endpoint(the SPARQL endpoint),lds:dataspace,lds:ontologyandac:stylesheet. Read the wiring off these rather than assembling URLs by convention.
GET a document URL with content negotiation:
Accept: text/turtle·application/rdf+xml·application/ld+json·application/n-triples(any RDF serialization Jena supports) → the document's RDF.Accept: text/html→ the application shell (Saxon-JS then renders client-side). Request RDF, not HTML, when you want data.Accept-Languageis honoured and answered withContent-LanguageandVary: Accept-Language. Property values come ordered by the reader's language preference.
HEAD is answered for any access mode the agent holds, so an agent with acl:Write or acl:Append alone can read the validator its writes have to quote. GET needs acl:Read.
Writes go through the document URLs, never through the SPARQL endpoint (which is read-only):
| Intent | Method | Body | Answer |
|---|---|---|---|
| Create a document at a URL you choose | PUT document URL |
RDF (e.g. Content-Type: text/turtle) |
201 with Location = that URL. The parent container must already exist |
| Replace an existing document | PUT document URL |
RDF | 200; replaces the whole named graph |
| Append to a document | POST document URL |
RDF | 204 with the updated ETag; merges into the named graph |
| Update a document in place | PATCH document URL |
Content-Type: application/sparql-update |
204 with the updated ETag; a SPARQL Update (INSERT/DELETE) applied to that named graph |
| Delete a document | DELETE document URL |
— | Removes the named graph |
| Upload a file | POST/PUT document URL |
multipart/form-data |
The file is content-addressed at {base}uploads/{sha1} |
There is no LDP-style POST to a container that mints a child URL: the client picks the URL (a slug, or a UUID), and PUT creates it. Relative URIs in a request body resolve against the target URL. Blank nodes are skolemized against it.
The PATCH update is executed in the context of the one named graph, so the GRAPH keyword is not allowed in it. An update that empties the graph is treated as a DELETE. An update that strips rdf:type off a resource that keeps other properties answers 422 — SPIN constraints are class-scoped, and removing the type would bypass them silently. So does a result that violates the ontology's SHACL shapes, carrying bounded descriptions of the violating resources.
Since 6.0.0 every write is conditional:
- A write to a document that already exists must carry
If-Match: <ETag>. Without it the server answers428 Precondition Required. An emptyIf-Matchcounts as absent. - A write meant to create carries
If-None-Match: *instead, and cannot also carryIf-Match. - A failed precondition answers
412with the document's current entity tag, so the retry needs no second read. - An entity tag identifies a negotiated variant (a SHA-256 digest of the graph URI and its statements, folded with the variant, languages included). The read that supplies the validator must therefore send the same
AcceptandAccept-Languageas the write that quotes it. A tag read asapplication/rdf+xmland quoted by a write that negotiates to HTML can never agree —412. - A write invalidates the tag it quoted, so the next one cannot reuse it.
POSTandPATCHanswer204with the updated tag — record it. After aPUT, re-read the validator withHEADunder the sameAcceptandAccept-Language.
ldh does all of the above for you.
The dataspace exposes a read-only SPARQL 1.1 Query endpoint (advertised via the Service Description sd:endpoint; conventionally /sparql). GET/POST a SELECT/CONSTRUCT/DESCRIBE/ASK; results are content-negotiated. The endpoint does not accept SPARQL Update — mutate via PATCH on document URLs (above).
Write portable, standard SPARQL: use explicit GRAPH patterns, no engine-specific extensions.
Outbound SERVICE and LOAD — from the triplestore and from the platform alike — are routed through the egress forward proxy, which refuses loopback, private and link-local destinations. A federated query reaches public endpoints and cannot reach the deployment's own services. With no proxy configured and ALLOW_INTERNAL_URLS unset, in-JVM SERVICE (in a PATCH update or an import mapping) is disabled outright.
- Documents carry ordered content blocks, attached by RDF container membership properties —
rdf:_1,rdf:_2, … on the document resource. Reordering a block rewrites those predicates. - Only
ldh:Object(an embedded RDF resource view,rdf:valuenaming the resource) andldh:XHTML(rich text,rdf:valueanrdf:XMLLiteral) are permitted as block values; anything else must be wrapped in anldh:Object. - Views (
ldh:View) are SPARQL-driven blocks (SELECT/CONSTRUCT/DESCRIBE) rendered as lists, tables, grids, charts, maps, or a graph. - Forms and validation are ontology-driven (SPIN constructors + SHACL shapes), so instance data is shaped by the app's ontology rather than hardcoded schemas.
- Imports are documents too: an
ldh:CSVImportorldh:RDFImportresource withldh:file(the source),spin:query(the transformation) and, for CSV,ldh:delimiter. Creating one submits the ingest; the source URIs are validated before being fetched. - Packages bundle an ontology with an optional stylesheet. An application imports one by declaring a single
<app> ldh:import <package>triple in its settings — the declaration is the installation. It takes effect on the next request, with no restart.
A document in a versioned application serves the three RFC 7089 roles as query parameters on its own URL, advertised in Link headers with type="application/link-format":
| Role | Request | Answer |
|---|---|---|
| Version history (TimeMap) | GET <doc>?timemap |
A prov:Collection of mementos, or application/link-format. 404 when the document has no versions |
| A historical version (Memento) | GET <doc>?version=<sha> |
That version, with Memento-Datetime and a rel=original link back |
| TimeGate | GET <doc>?timegate with Accept-Datetime |
302 to the closest version, Vary: accept-datetime, no-store |
The TimeMap is described with PROV-O (prov:Entity mementos, prov:specializationOf the original, prov:generatedAtTime, prov:wasRevisionOf). application/link-format is served only for ?timemap; asking for it anywhere else answers 406.
| Path | Method | Access | Purpose |
|---|---|---|---|
/sparql |
GET/POST |
acl:AuthenticatedAgent |
Read-only SPARQL 1.1 Query over the dataspace |
/ns |
GET/POST |
public | A SPARQL endpoint over the in-memory application ontology. A plain GET returns the standalone namespace graph (not the full owl:imports closure) |
/access?this=<doc URI> |
GET |
public | The current agent's acl:Authorizations for that document |
/access/request |
POST |
public | Request access to a document |
/settings |
GET, PATCH |
owner | The running dataspace's settings; PATCH (application/sparql-update) is how an ldh:import is declared live |
/clear |
POST |
owner | Empties the ontology cache; the optional uri form parameter also purges that URI's proxy caches and reassembles its closure |
/uploads/{sha1} |
GET |
per ACL | A content-addressed upload |
/sign up |
GET, POST |
public | WebID sign-up, which hands back a PKCS12 keystore |
/oauth2/login/{google,orcid}, /oauth2/authorize/{google,orcid} |
GET |
public | Human login flows |
/sitemap.xml, /robots.txt |
GET |
public | Per-dataspace, generated at startup from that dataspace's public read rules and served at servlet level |
/ns and /sign up are public in the ACL query itself. The rest of the access column is the authorizations a dataspace is seeded with, which a deployment is free to change — read the acl:mode Link headers of a response to learn what the agent actually holds.
An external URI is read through the dataspace, never fetched cross-origin: GET <base>?uri=<external URI>.
- With an RDF
Accept, the server dereferences the URI, parses the RDF and returns it, forwarding the external response'sLinkheaders. - With
Accept: text/htmlthe proxy is bypassed and the local application shell is returned; the browser then makes the RDF-typed request itself. MAX_CONTENT_LENGTHbounds what the proxy fetches. An oversize upstream response answers502.
Federation rides on the forwarded headers: the remote dataspace's sd:endpoint arrives as a Link, so a client can query the remote app's endpoint and write to its documents under its own preconditions.
A single instance hosts multiple dataspaces, each a subdomain (origin). Each dataspace pairs an end-user app (<subdomain>) with an admin app at the admin. prefix (admin.<subdomain>) — never an /admin path. Admin apps manage ontologies, ACLs, and app settings.
403where you expect404. The ACL query fails closed on a typeless resource, so a non-owner request for a URL that does not exist is answered403.428on any write to an existing document that carries noIf-Match;412when the tag quoted was read under a different negotiated variant. See Conditional writes.502(rather than413) when an upstream response exceedsMAX_CONTENT_LENGTH: what was too large is the upstream's response. Requests to the deployment's own SPARQL, Graph Store and Quad Store services are exempt from the bound.406forapplication/link-formaton anything but?timemap.
-
CLI:
ldh(built fromcli/) wraps every operation above and is the authoritative reference for request shapes. Commands group by verb:get,post,put,patch,delete— the bare HTTP methods.post/puttake an optionalFILEargument and read the RDF syntax from its extension;-t/--content-typeis required on stdin.getalso addresses the Memento roles with--timemap,--version <sha>and--timegate [--datetime].create item|container—PUTs a new document (--parent/--container,--title,--slug,--primary-topic).add view|construct|select|result-set-chart|file|generic-service|rdf-import|csv-import|object-block|xhtml-block—POSTs an append onto an existing document.remove block— names the single request it makes.import rdf|csv— the workflows composingadd construct,add fileandadd {rdf,csv}-import.packages list|add|remove— the application's package imports, written throughPATCH /settings.listreads the registry catalog through the Linked Data proxy and marks the imported ones.push— replays a directory into the document tree it maps to: RDF files arePUTto the document their path spells,root.ttlis the target document, other files are uploaded, subdirectories recurse. Honours a gitignore-style.ldhignore, prints its plan with--dry-run, writes one URL per line and stops at the first failed request.admin {create,add,clear,import,make-public}— the same verbs scoped to the admin application (ontologies, classes, constructors, constraints, restrictions, ACL groups and authorizations).
Authentication is the agent's WebID credential given to
-c/--cert, as either a PKCS12 keystore or a PEM file holding the certificate and its PKCS#8 private key, told apart by content (-p/--cert-password, orLDH_CERT_FILE/LDH_CERT_PASSWORD;LDH_BASEandLDH_PROXYfor-band--proxy). Commands that create or append print the document's URL as the only line on stdout, so they compose in pipelines; exit codes are0success,1HTTP or runtime failure,2usage error. Shell completion:source <(ldh generate-completion).Certificate and WebID tooling (
webid-keygen.sh,server-cert-gen.sh) remains inbin/; thebin/HTTP API scriptsldhreplaces are deprecated. -
Programmatic / MCP: Web-Algebra is the recommended path for agent-composed workflows — a JSON DSL and MCP server whose operations (create container/item, add view/chart, generate portal, …) compose multi-step LDH writes atomically under WebID auth.
WebID-TLS · SPARQL 1.1 Query · SPARQL 1.1 Update (over PATCH) · Graph Store Protocol · HTTP conditional requests (RFC 9110) · Web Linking (RFC 8288) · Memento (RFC 7089) · PROV-O · SHACL · SPIN · OWL · RDF (Turtle/RDF-XML/JSON-LD/N-Triples). LDH composes existing W3C/IETF standards; it does not define new wire protocols.