---
title: Multi-Format Document API with curl
nextjs:
metadata:
title: Multi-Format Document API with curl
description: A hands-on tutorial for using the TerminusDB Document API with curl to work with JSON, JSON-LD, RDF/XML, and Turtle formats. Covers a full round-trip cycle for enterprise reference data management.
keywords: document api, curl, json-ld, rdf xml, turtle, ttl, enterprise, reference data, multi-format
alternates:
canonical: https://terminusdb.org/docs/document-format-api-curl-tutorial/
openGraph:
images: https://assets.terminusdb.com/docs/technical-documentation-terminuscms-og.png
---
Enterprise reference data systems rarely live in isolation. Master data feeds into downstream analytics platforms, regulatory reporting pipelines, and partner integrations — each expecting a different serialization. A product taxonomy might need to be JSON for the internal API, JSON-LD for a linked-data catalog, RDF/XML for a standards body submission, and Turtle for human review or SPARQL tooling — all from the same source of truth.
The TerminusDB Enterprise multi-format Document API lets you store reference data once and retrieve or ingest it in any of these formats without writing conversion code. This tutorial walks through a complete round-trip using only `curl`, from database creation through schema design, data entry, multi-format export, and re-import.
## What you will learn
By the end of this tutorial you will be able to:
- Create a database and schema for operational reference data
- Insert documents using the standard JSON API
- Retrieve those documents as **JSON**, **JSON-LD**, **RDF/XML**, and **Turtle**
- Understand when each format is the right choice
- Re-import data from JSON-LD, RDF/XML, and Turtle back into TerminusDB
- Verify round-trip integrity across all four formats
## Prerequisites
- A running TwinfoxDB Enterprise instance (Docker or local) on `localhost:6363`
- `curl` installed (any recent version)
- Basic familiarity with JSON and HTTP
All commands in this tutorial use basic authentication with the default `admin:root` credentials. Adjust the password if you have changed it.
## Setup: encode your credentials
Every request needs an `Authorization` header. Set it once in your shell session so the remaining commands stay readable:
```bash
export BASE64=$(echo -n 'admin:root' | base64)
export SERVER="http://localhost:6363"
export DB="admin/ProductCatalog"
export AUTH="Authorization: Basic $BASE64"
```
The database path `admin/ProductCatalog` represents the `admin` organization and a data product called `ProductCatalog` — a realistic name for a reference data system that governs product master data across an enterprise.
---
## Step 1 — Create the database
A reference data system starts with a dedicated data product. Create it with a single POST:
```bash
curl -s -X POST \
-H "Content-Type: application/json" \
-H "$AUTH" \
-d '{
"label": "Product Catalog",
"comment": "Enterprise product reference data",
"schema": true
}' \
"$SERVER/api/db/$DB"
```
**Expected response:**
```json
{"@type":"api:DbCreateResponse","api:status":"api:success"}
```
**Why this matters:** In an enterprise setting, each reference data domain — products, customers, locations — lives in its own data product. Version control, branching, and access control all operate at this level, giving data stewards an isolated workspace for each domain.
---
## Step 2 — Define the schema
A good reference data schema captures both the structure and the business rules. The schema below models a product catalog with categories and products, using subdocuments for nested structures and lexical keys for human-readable identifiers.
```bash
curl -s -X POST \
-H "Content-Type: application/json" \
-H "$AUTH" \
-d '[
{
"@type": "Class",
"@id": "Category",
"@key": { "@type": "Lexical", "@fields": ["code"] },
"@documentation": {
"@comment": "A product category in the reference taxonomy"
},
"code": "xsd:string",
"label": "xsd:string",
"description": "xsd:string"
},
{
"@type": "Class",
"@id": "UnitOfMeasure",
"@key": { "@type": "Lexical", "@fields": ["symbol"] },
"@subdocument": [],
"symbol": "xsd:string",
"name": "xsd:string"
},
{
"@type": "Class",
"@id": "Product",
"@key": { "@type": "Lexical", "@fields": ["sku"] },
"@documentation": {
"@comment": "A product in the enterprise catalog"
},
"sku": "xsd:string",
"name": "xsd:string",
"category": "Category",
"price": "xsd:decimal",
"weight_kg": "xsd:decimal",
"unit": "UnitOfMeasure",
"active": "xsd:boolean"
}
]' \
"$SERVER/api/document/$DB?author=admin&message=Initial+schema&graph_type=schema"
```
**Expected response:** A list of the created schema document IDs.
**Design notes:**
- **Lexical keys** (`@key`) produce deterministic, human-readable IDs like `Product/SKU-1001` — essential when external systems need stable references (they are full IRIs, with a common base prefix for the data product).
- **Subdocuments** (`@subdocument`) like `UnitOfMeasure` are embedded inside their parent and do not have independent lifecycle — perfect for value objects that only make sense in context
- **Typed fields** (`xsd:decimal`, `xsd:boolean`) enforce data quality at write time, preventing the "garbage in" problem that plagues loosely-typed reference stores.
---
## Step 3 — Insert reference data
Now populate the catalog with a category and two products. In a real system, this is where an ETL pipeline or data steward would load master data:
```bash
curl -s -X POST \
-H "Content-Type: application/json" \
-H "$AUTH" \
-d '[
{
"@type": "Category",
"code": "ELEC",
"label": "Electronics",
"description": "Consumer and industrial electronic products"
},
{
"@type": "Product",
"sku": "SKU-1001",
"name": "Industrial Sensor Module",
"category": "Category/ELEC",
"price": 249.99,
"weight_kg": 0.35,
"unit": {
"@type": "UnitOfMeasure",
"symbol": "pcs",
"name": "pieces"
},
"active": true
},
{
"@type": "Product",
"sku": "SKU-1002",
"name": "Precision Thermocouple",
"category": "Category/ELEC",
"price": 87.50,
"weight_kg": 0.12,
"unit": {
"@type": "UnitOfMeasure",
"symbol": "pcs",
"name": "pieces"
},
"active": true
}
]' \
"$SERVER/api/document/$DB?author=admin&compress_id=true&message=Initial+product+data"
```
**Expected response:** The full IRIs of the three inserted documents:
```json
[
"terminusdb:///data/Category/ELEC",
"terminusdb:///data/Product/SKU-1001",
"terminusdb:///data/Product/SKU-1002"
]
```
The insert endpoint returns full IRIs rather than compressed IDs. The IDs themselves are deterministic and readable because of the lexical key strategy — `Product/SKU-1001` directly encodes the business key. When you retrieve documents with GET, the response uses compressed IDs by default (controlled by the `prefixed` parameter).
---
## Step 4 — Retrieve as JSON (default)
The default format is the standard TerminusDB JSON representation — compact, with compressed IRIs relative to the database context. This is the format your internal APIs and applications will typically use.
### Retrieve all products
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?type=Product&as_list=true" | jq
```
**Expected response:**
```json
[
{
"@id": "Product/SKU-1001",
"@type": "Product",
"active": true,
"category": "Category/ELEC",
"name": "Industrial Sensor Module",
"price": 249.99,
"sku": "SKU-1001",
"unit": {
"@id": "Product/SKU-1001/unit/UnitOfMeasure/pcs",
"@type": "UnitOfMeasure",
"name": "pieces",
"symbol": "pcs"
},
"weight_kg": 0.35
},
{
"@id": "Product/SKU-1002",
"@type": "Product",
"active": true,
"category": "Category/ELEC",
"name": "Precision Thermocouple",
"price": 87.50,
"sku": "SKU-1002",
"unit": {
"@id": "Product/SKU-1002/unit/UnitOfMeasure/pcs",
"@type": "UnitOfMeasure",
"name": "pieces",
"symbol": "pcs"
},
"weight_kg": 0.12
}
]
```
### Retrieve a single document by ID
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?id=Product/SKU-1001" | jq
```
**When to use JSON:** Internal microservices, REST API consumers, front-end applications, and any system that speaks idiomatic JSON. This is the leanest format with the smallest payload size.
---
## Step 5 — Retrieve as JSON-LD
JSON-LD adds semantic context to the same data, making it self-describing. Each document carries an `@context` that maps short property names to full IRIs — turning your product data into linked data without changing its shape.
### Stream mode (one context per document)
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?type=Product&format=jsonld" | jq
```
Each document in the response is a self-contained JSON-LD object:
```json
{
"@context": {
"@base": "terminusdb:///data/",
"@vocab": "terminusdb:///schema#"
},
"@id": "Product/SKU-1001",
"@type": "Product",
"active": true,
"category": "Category/ELEC",
"name": "Industrial Sensor Module",
"price": 249.99,
"sku": "SKU-1001",
"unit": {
"@id": "Product/SKU-1001/unit/UnitOfMeasure/pcs",
"@type": "UnitOfMeasure",
"name": "pieces",
"symbol": "pcs"
},
"weight_kg": 0.35
}
```
### List mode (shared context with @graph)
For batch processing, the list mode wraps all documents in a single JSON-LD envelope with a shared `@context`:
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?type=Product&format=jsonld&as_list=true" | jq
```
**Expected response:**
```json
{
"@context": {
"@base": "terminusdb:///data/",
"@vocab": "terminusdb:///schema#"
},
"@graph": [
{
"@id": "Product/SKU-1001",
"@type": "Product",
"active": true,
"category": "Category/ELEC",
"name": "Industrial Sensor Module",
"price": 249.99,
"sku": "SKU-1001",
"unit": {
"@id": "Product/SKU-1001/unit/UnitOfMeasure/pcs",
"@type": "UnitOfMeasure",
"name": "pieces",
"symbol": "pcs"
},
"weight_kg": 0.35
},
{
"@id": "Product/SKU-1002",
"@type": "Product",
"active": true,
"category": "Category/ELEC",
"name": "Precision Thermocouple",
"price": 87.50,
"sku": "SKU-1002",
"unit": {
"@id": "Product/SKU-1002/unit/UnitOfMeasure/pcs",
"@type": "UnitOfMeasure",
"name": "pieces",
"symbol": "pcs"
},
"weight_kg": 0.12
}
]
}
```
### Using the Accept header instead
You can also request JSON-LD through content negotiation rather than the query parameter:
```bash
curl -s -X GET \
-H "$AUTH" \
-H "Accept: application/ld+json" \
"$SERVER/api/document/$DB?type=Product&as_list=true" | jq
```
The result is identical. The `format=` query parameter takes precedence if both are present.
**When to use JSON-LD:** Data catalog registration, linked-data publishing, semantic web integrations, and any scenario where the consumer needs to resolve property names to globally unique IRIs. JSON-LD is also the format of choice when submitting reference data to industry consortia or standards bodies that require RDF-compatible metadata.
---
## Step 6 — Retrieve as RDF/XML
RDF/XML is the classic serialization for the Resource Description Framework, still required by many regulatory and standards-based systems. TerminusDB Enterprise serializes your documents directly into well-formed RDF/XML with proper namespace declarations.
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?type=Product&format=rdfxml"
```
**Expected response:**
```xml
true
Industrial Sensor Module
249.99
SKU-1001
pieces
pcs
0.35
true
Precision Thermocouple
87.50
SKU-1002
pieces
pcs
0.12
```
You can also use the Accept header:
```bash
curl -s -X GET \
-H "$AUTH" \
-H "Accept: application/rdf+xml" \
"$SERVER/api/document/$DB?type=Product"
```
**Key observations about the RDF/XML output:**
- **Namespace declarations** (`xmlns:schema`, `xmlns:rdf`, `xmlns:xsd`) are generated from your database context
- **Typed literals** carry `rdf:datatype` attributes — `xsd:decimal` for prices, `xsd:boolean` for flags
- **String values** omit the datatype (the RDF default)
- **References** to other documents use `rdf:resource` (e.g., the `category` link)
- **Subdocuments** are nested inline as child elements
**When to use RDF/XML:** Regulatory submissions (pharmaceutical, financial, government data standards), SKOS taxonomy exchange, OWL ontology alignment, and integration with legacy triple stores or SPARQL endpoints that require RDF/XML as their ingest format.
---
## Step 7 — Retrieve as Turtle
Turtle (Terse RDF Triple Language) is a compact, human-readable RDF serialization. Where RDF/XML is verbose and XML-centric, Turtle uses prefixed names, semicolons for predicate grouping, and square brackets for blank nodes — making it easy to read, diff, and hand-edit.
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?type=Product&format=turtle"
```
**Expected response:**
```turtle
@base .
@prefix schema: .
@prefix xsd: .
@prefix rdf: .
a schema:Product ;
schema:active "true"^^xsd:boolean ;
schema:category ;
schema:name "Industrial Sensor Module" ;
schema:price "249.99"^^xsd:decimal ;
schema:sku "SKU-1001" ;
schema:unit [
a schema:UnitOfMeasure ;
schema:name "pieces" ;
schema:symbol "pcs"
] ;
schema:weight_kg "0.35"^^xsd:decimal .
a schema:Product ;
schema:active "true"^^xsd:boolean ;
schema:category ;
schema:name "Precision Thermocouple" ;
schema:price "87.50"^^xsd:decimal ;
schema:sku "SKU-1002" ;
schema:unit [
a schema:UnitOfMeasure ;
schema:name "pieces" ;
schema:symbol "pcs"
] ;
schema:weight_kg "0.12"^^xsd:decimal .
```
You can also use the Accept header:
```bash
curl -s -X GET \
-H "$AUTH" \
-H "Accept: text/turtle" \
"$SERVER/api/document/$DB?type=Product"
```
**Key observations about the Turtle output:**
- **`@prefix` declarations** map short prefixes to full IRIs — the same role as JSON-LD `@context` and RDF/XML `xmlns:`
- **`@base`** sets the base IRI for resolving relative references
- **`a`** is Turtle shorthand for `rdf:type`
- **Typed literals** use `^^` notation — `"249.99"^^xsd:decimal`, `"true"^^xsd:boolean`
- **String values** are plain quoted literals without a datatype
- **References** to other documents use full IRIs in angle brackets
- **Subdocuments** are nested as blank node blocks using `[ ... ]`
**When to use Turtle:** Human review of RDF data, version control diffs where line-oriented output makes changes easy to spot, SPARQL tooling, academic data exchange, and debugging scenarios where compact readable output helps trace data issues.
---
## Step 8 — Import from JSON-LD
The round-trip works in both directions. You can POST documents as JSON-LD and TerminusDB will parse the `@context`, resolve IRIs, and insert them into the instance graph. Here we are also remapping `@id` and `@type` into `id` and `type` using the context, to enable easier document exchange with other systems.
First, let's create a new product by submitting JSON-LD:
```bash
curl -s -X POST \
-H "Content-Type: application/ld+json" \
-H "$AUTH" \
-d '{
"@context": {
"@base": "terminusdb:///data/",
"@vocab": "terminusdb:///schema#",
"id": "@id",
"type": "@type"
},
"type": "Product",
"id": "Product/SKU-2001",
"sku": "SKU-2001",
"name": "Fiber Optic Transceiver",
"category": "Category/ELEC",
"price": 189.00,
"weight_kg": 0.08,
"unit": {
"type": "UnitOfMeasure",
"symbol": "pcs",
"name": "pieces"
},
"active": true
}' \
"$SERVER/api/document/$DB?author=admin&message=Import+from+JSON-LD"
```
**Expected response:**
```json
["terminusdb:///data/Product/SKU-2001"]
```
The `Content-Type: application/ld+json` header tells TerminusDB to parse the body as JSON-LD. The `@context` in the payload is used to resolve any prefixed or relative IRIs back to full identifiers.
**Why this matters:** When a partner sends you their product master data as JSON-LD (perhaps exported from their own linked-data platform), you can ingest it directly without pre-processing. The semantic context travels with the data.
---
## Step 9 — Import from RDF/XML
You can also submit documents as RDF/XML. This is particularly valuable when receiving data from systems that only export in RDF formats — common in government, healthcare, and standards bodies. TwinfoxDB automatically reflows the document to match the internal structure of the document and its subdocuments.
```bash
curl -s -X POST \
-H "Content-Type: application/rdf+xml" \
-H "$AUTH" \
-d '
SKU-3001
Calibration Reference Standard
425.00
1.20
pcs
pieces
true
' \
"$SERVER/api/document/$DB?author=admin&message=Import+from+RDF/XML"
```
**Expected response:**
```json
["terminusdb:///data/Product/SKU-3001"]
```
The `Content-Type: application/rdf+xml` header triggers the RDF/XML parser. TerminusDB reads the namespace declarations, resolves `rdf:about` URIs to document IDs, maps `rdf:datatype` to schema types, and inserts the documents into the instance graph — all validated against your schema.
**Why this matters:** Consider a regulatory agency that publishes approved product classifications as RDF/XML. Instead of writing a custom parser, you POST the file directly into your reference data system. Schema validation catches any structural issues immediately, and the data is instantly queryable alongside your existing catalog.
---
## Step 10 — Import from Turtle
Turtle input works the same way. Set `Content-Type: text/turtle` and submit a Turtle document with `@prefix` declarations and typed literals:
```bash
echo '@prefix schema: .
@prefix xsd: .
a schema:Product ;
schema:sku "SKU-4001" ;
schema:name "RF Signal Analyzer" ;
schema:category ;
schema:price "1850.00"^^xsd:decimal ;
schema:weight_kg "3.40"^^xsd:decimal ;
schema:unit [
a schema:UnitOfMeasure ;
schema:symbol "pcs" ;
schema:name "pieces"
] ;
schema:active "true"^^xsd:boolean .' | \
curl -s -X POST \
-H "Content-Type: text/turtle" \
-H "$AUTH" \
--data-binary @- \
"$SERVER/api/document/$DB?author=admin&message=Import+from+Turtle"
```
The reason for the odd syntax is that the document starts with `@` which is treated as a file reference by `curl`, so we use stdin here instead.
**Expected response:**
```json
["terminusdb:///data/Product/SKU-4001"]
```
The `Content-Type: text/turtle` header triggers the Turtle parser. TerminusDB reads the `@prefix` declarations, resolves prefixed names to full IRIs, maps `^^xsd:decimal` to schema types, and inserts the documents into the instance graph — all validated against your schema.
**Why this matters:** Turtle is the format most commonly produced by SPARQL `CONSTRUCT` queries, academic data repositories, and linked-data tooling. Being able to ingest Turtle directly means you can integrate data from these sources without an intermediate conversion step.
---
## Step 11 — Verify the round-trip
Now confirm that all six products — the original two, the JSON-LD import, the RDF/XML import, and the Turtle import — are in the database and structurally consistent:
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?type=Product&as_list=true" | jq
```
You should see six Product documents. Let's verify the imported ones individually:
### Check the JSON-LD import
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?id=Product/SKU-2001" | jq
```
### Check the RDF/XML import
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?id=Product/SKU-3001" | jq
```
### Check the Turtle import
```bash
curl -s -X GET \
-H "$AUTH" \
"$SERVER/api/document/$DB?id=Product/SKU-4001" | jq
```
All three imports should have the same structure as the originals — the same fields, the same types, the same subdocument nesting. The format used for insertion is transparent; once stored, every document is a first-class citizen regardless of how it arrived.
### Full-format cross-check
For a thorough verification, retrieve the same document in all four formats and confirm the data is identical:
```bash
echo "=== JSON ==="
curl -s -X GET -H "$AUTH" "$SERVER/api/document/$DB?id=Product/SKU-4001"
echo ""
echo "=== JSON-LD ==="
curl -s -X GET -H "$AUTH" "$SERVER/api/document/$DB?id=Product/SKU-4001&format=jsonld"
echo ""
echo "=== RDF/XML ==="
curl -s -X GET -H "$AUTH" "$SERVER/api/document/$DB?id=Product/SKU-4001&format=rdfxml"
echo ""
echo "=== Turtle ==="
curl -s -X GET -H "$AUTH" "$SERVER/api/document/$DB?id=Product/SKU-4001&format=turtle"
```
All four representations describe the same underlying graph — a single product with its properties, types, and relationships. The format is just a view over the immutable, versioned data.
---
## Step 12 — Replace a document using JSON-LD
Multi-format support extends to updates as well. Use PUT with `Content-Type: application/ld+json` to replace a document:
```bash
curl -s -X PUT \
-H "Content-Type: application/ld+json" \
-H "$AUTH" \
-d '{
"@context": {
"@base": "terminusdb:///data/",
"@vocab": "terminusdb:///schema#"
},
"@id": "Product/SKU-2001",
"@type": "Product",
"sku": "SKU-2001",
"name": "Fiber Optic Transceiver (Gen 2)",
"category": "Category/ELEC",
"price": 199.00,
"weight_kg": 0.07,
"unit": {
"@type": "UnitOfMeasure",
"symbol": "pcs",
"name": "pieces"
},
"active": true
}' \
"$SERVER/api/document/$DB?author=admin&message=Update+via+JSON-LD"
```
The document is replaced in place. Because TerminusDB is immutable under the hood, the previous version remains in the commit history — retrievable at any time through the version control endpoints.
---
## Step 13 — Clean up
When you are done experimenting, remove the test database:
```bash
curl -s -X DELETE \
-H "$AUTH" \
"$SERVER/api/db/$DB"
```
---
## Format selection guide
Choosing the right format depends on who is consuming the data and what they need:
{% table %}
- Scenario
- Format
- Reason
---
- Internal REST API
- JSON
- Smallest payload, fastest parsing, native to most frameworks
---
- Data catalog or metadata registry
- JSON-LD
- Self-describing with semantic context, compatible with linked-data tooling
---
- Regulatory or standards submission
- RDF/XML
- Required by many government and industry bodies, compatible with SPARQL endpoints
---
- Partner data exchange (inbound)
- JSON-LD, RDF/XML, or Turtle
- Accept whatever your partner produces, no custom parser needed
---
- Backup and migration
- JSON
- Compact, easy to diff, works with TerminusDB version control
---
- Knowledge graph federation
- JSON-LD
- Context enables merging data from multiple sources without IRI conflicts
---
- Human review and debugging
- Turtle
- Compact, line-oriented, easy to read and hand-edit
---
- Version control diffs
- Turtle
- Line-oriented format produces clean, readable diffs in pull requests
---
- SPARQL tooling
- Turtle
- Native format for SPARQL CONSTRUCT output and academic data exchange
{% /table %}
## Key takeaways
Multi-format support in TerminusDB Enterprise means your reference data system speaks the language of every downstream consumer. A few points worth remembering:
- **The data is format-independent.** Documents are stored as typed, schema-validated graphs. JSON, JSON-LD, RDF/XML, and Turtle are just views over that graph.
- **Round-trips are lossless.** Export as Turtle, send it to a partner, receive their edits back as JSON-LD, and import — nothing is lost or distorted.
- **Schema validation applies to every format.** Whether you POST JSON, JSON-LD, RDF/XML, or Turtle, the data is checked against the schema before it is committed. Bad data is rejected with a clear error regardless of the input format.
- **Content negotiation is standard HTTP.** Use the `Accept` header or the `format` query parameter — both work, and the query parameter takes precedence when both are present.
- **Version control covers everything.** Every insert, update, and replace — in any format — creates an immutable commit in the history. You can always go back to see exactly what was submitted and when.
## Further reading
- [Document API Reference Guide](/docs/document-insertion/) — full parameter reference for the document endpoint
- [How to use the HTTP Documents API](/docs/http-documents-api/) — authentication and connection examples
- [Document Graph API Howto](/docs/document-graph-api/) — overview of the document and schema endpoints
- [Documents in a knowledge graph](/docs/documents-explanation/) — how documents and graphs work together in TerminusDB
- [Immutability and version control](/docs/immutability-explanation/) — how commits and branches provide an audit trail