OSCAL Content Registry — API Reference
The OSCAL Content Registry exposes a RESTful API for listing, retrieving,
creating, updating, and deleting OSCAL documents. All request and response
bodies use JSON (application/json).
Base URL
The API is served at https://api.oscal.io. All paths in this document
are relative to that base URL.
The web-based registry UI is available at https://registry.oscal.io.
Authentication
Public endpoints (listing and retrieving documents) require no authentication.
Write operations (PUT, DELETE, and POST /api/upload) require
an Auth0 session cookie. The cookie is set automatically when a user
signs in through the web UI at /auth/login.
Obtaining a Token for Programmatic Access
For server-to-server or CLI usage you can obtain an access token from your
Auth0 tenant and pass it as a bearer token, or authenticate through a
browser flow and reuse the appSession cookie.
Browser cookie approach (quick testing):
- Sign in at
{BASE_URL}/auth/loginin a browser. - Copy the
appSessioncookie value from your browser's dev tools. - Pass it with your requests:
curl -H "Cookie: appSession=<value>" \
https://api.oscal.io/api/v1/catalogs
Note: For production integrations, configure a machine-to-machine application in Auth0 and use the Client Credentials flow to obtain an access token.
Error Responses for Unauthenticated Requests
{
"status-code": 401,
"message": "Authentication required"
}
Model Types
The registry supports all seven OSCAL model types. Use the URL segment exactly as shown:
| Model Type | URL Segment |
|---|---|
| Catalog | catalogs |
| Profile | profiles |
| Component Definition | component-definitions |
| System Security Plan | system-security-plans |
| Assessment Plan | assessment-plans |
| Assessment Results | assessment-results |
| Plan of Action and Milestones | plans-of-action-and-milestones |
Error Format
All errors follow a consistent structure:
{
"status-code": 422,
"message": "Human-readable description of the error"
}
Common status codes:
| Code | Meaning |
|---|---|
400 | Bad request / invalid JSON |
401 | Authentication required |
403 | Forbidden — not the document owner or admin |
404 | Document not found |
409 | Conflict — content-uuid mismatch |
415 | Unsupported media type |
422 | Unprocessable — invalid model type or OSCAL structure |
Endpoints
List Documents by Model Type
GET /api/v1/{model}
Returns all documents of the specified model type. Public — no authentication required.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
model | string | One of the model URL segments listed above |
Response 200 OK
[
{
"content-uuid": "4429306e-0300-4900-9414-c340fe372f76",
"title": "NIST SP 800-53 Rev 5",
"oscal-version": "1.1.2",
"document-version": "5.0.0",
"last-modified": "2024-02-06T18:30:00.000Z",
"self": "/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76"
}
]
Example
# List all catalogs
curl https://api.oscal.io/api/v1/catalogs
# List all system security plans
curl https://api.oscal.io/api/v1/system-security-plan
Get a Document
GET /api/v1/{model}/{contentUuid}
Returns the full OSCAL JSON document. Public — no authentication required.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
model | string | Model type URL segment |
contentUuid | UUID | The content-uuid of the document |
Response 200 OK
The complete OSCAL JSON document as stored.
{
"catalog": {
"uuid": "...",
"metadata": {
"title": "NIST SP 800-53 Rev 5",
"version": "5.0.0",
"oscal-version": "1.1.2",
"last-modified": "2024-02-06T18:30:00.000Z"
},
"groups": [ "..." ]
}
}
Error Responses
| Status | Condition |
|---|---|
404 | Document not found |
422 | Unknown model type |
Example
# Retrieve a specific catalog document
curl https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76
# Save to a file
curl -o catalog.json \
https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76
Create or Update a Document (PUT)
PUT /api/v1/{model}/{contentUuid}
Creates a new document or replaces an existing one (upsert). Requires authentication. Only the document owner can update an existing document.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
model | string | Model type URL segment |
contentUuid | UUID | The content-uuid of the document |
Headers
| Header | Value |
|---|---|
Content-Type | application/json |
Cookie | appSession=<session-cookie> |
Request Body
A complete, valid OSCAL JSON document. The document must:
- Contain a recognized OSCAL root key (e.g.,
"catalog","profile") - Match the model type in the URL
- Contain a
content-uuid(insidemetadata) that matches the URL parameter, or auuidat the model root from which the content-uuid can be derived
Response
| Status | Meaning |
|---|---|
201 Created | New document was created |
204 No Content | Existing document was updated |
Error Responses
| Status | Condition |
|---|---|
400 | Invalid JSON body or invalid metadata |
401 | Not authenticated |
403 | You are not the owner of this document |
409 | content-uuid in the document doesn't match the URL |
415 | Content-Type is not application/json |
422 | Unknown model type or document structure invalid |
Example — Create a New Catalog
curl -X PUT \
-H "Content-Type: application/json" \
-H "Cookie: appSession=<session-cookie>" \
-d @my-catalog.json \
https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76
Example — Update an Existing Document
# Modify the local file, then PUT it back
curl -X PUT \
-H "Content-Type: application/json" \
-H "Cookie: appSession=<session-cookie>" \
-d @my-catalog-updated.json \
https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76
Delete a Document
DELETE /api/v1/{model}/{contentUuid}
Deletes a document. Requires authentication. Only the document owner or an admin can delete a document.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
model | string | Model type URL segment |
contentUuid | UUID | The content-uuid of the document |
Headers
| Header | Value |
|---|---|
Cookie | appSession=<session-cookie> |
Response 204 No Content
The document was successfully deleted.
Error Responses
| Status | Condition |
|---|---|
401 | Not authenticated |
403 | Not the document owner or admin |
404 | Document not found |
422 | Unknown model type |
Example
curl -X DELETE \
-H "Cookie: appSession=<session-cookie>" \
https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76
Version History
The registry tracks every update to a document as a numbered version.
When you PUT a document, a new version is created automatically —
the previous content is preserved and remains accessible through the
version endpoints below.
How Versioning Works
- Each document is identified by its
content-uuid. - Every
PUTto the samecontent-uuidincrements the version number (1, 2, 3, …). GET /api/v1/{model}/{contentUuid}always returns the latest version.- Older versions can be retrieved by their version number.
List Versions
GET /api/v1/{model}/{contentUuid}/versions
Returns metadata for all versions of a document, ordered from newest to oldest. Requires authentication — only the document owner or an admin can list versions.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
model | string | Model type URL segment |
contentUuid | UUID | The content-uuid of the document |
Headers
| Header | Value |
|---|---|
Cookie | appSession=<session-cookie> |
Response 200 OK
[
{
"id": 42,
"version": 3,
"title": "NIST SP 800-53 Rev 5 (updated)",
"documentVersion": "5.0.1",
"oscalVersion": "1.1.2",
"lastModified": "2025-06-15T10:00:00.000Z",
"fileSize": 2048000,
"createdAt": "2025-06-15T10:00:00.000Z"
},
{
"id": 41,
"version": 2,
"title": "NIST SP 800-53 Rev 5",
"documentVersion": "5.0.0",
"oscalVersion": "1.1.2",
"lastModified": "2025-03-08T14:30:00.000Z",
"fileSize": 2040000,
"createdAt": "2025-03-08T14:30:00.000Z"
},
{
"id": 40,
"version": 1,
"title": "NIST SP 800-53 Rev 5",
"documentVersion": "5.0.0",
"oscalVersion": "1.1.2",
"lastModified": "2025-03-07T09:00:00.000Z",
"fileSize": 2035000,
"createdAt": "2025-03-07T09:00:00.000Z"
}
]
Error Responses
| Status | Condition |
|---|---|
401 | Not authenticated |
403 | Not the document owner or admin |
404 | Document not found |
422 | Unknown model type |
Example
curl -H "Cookie: appSession=<session-cookie>" \
https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76/versions
Get a Specific Version
GET /api/v1/{model}/{contentUuid}/versions/{version}
Returns the full OSCAL JSON for a specific version. Public — no authentication required.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
model | string | Model type URL segment |
contentUuid | UUID | The content-uuid of the document |
version | integer | The version number (1-based) |
Response 200 OK
The complete OSCAL JSON document as stored for that version.
Error Responses
| Status | Condition |
|---|---|
400 | Invalid version number |
404 | Version not found |
422 | Unknown model type |
Example
# Retrieve version 2 of a catalog
curl https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76/versions/2
# Compare two versions using diff
diff \
<(curl -s https://api.oscal.io/api/v1/catalogs/$UUID/versions/1 | jq .) \
<(curl -s https://api.oscal.io/api/v1/catalogs/$UUID/versions/2 | jq .)
Delete a Specific Version
DELETE /api/v1/{model}/{contentUuid}/versions/{version}
Deletes a specific version of a document. Requires authentication. Only the document owner or an admin can delete versions.
Note: You cannot delete the only remaining version. To remove the document entirely, use
DELETE /api/v1/{model}/{contentUuid}instead.
Path Parameters
| Parameter | Type | Description |
|---|---|---|
model | string | Model type URL segment |
contentUuid | UUID | The content-uuid of the document |
version | integer | The version number to delete |
Headers
| Header | Value |
|---|---|
Cookie | appSession=<session-cookie> |
Response 204 No Content
The version was successfully deleted.
Error Responses
| Status | Condition |
|---|---|
400 | Invalid version number |
401 | Not authenticated |
403 | Not the document owner or admin |
404 | Document or version not found |
409 | Cannot delete the only remaining version |
422 | Unknown model type |
Example
# Delete version 1 of a catalog
curl -X DELETE \
-H "Cookie: appSession=<session-cookie>" \
https://api.oscal.io/api/v1/catalogs/4429306e-0300-4900-9414-c340fe372f76/versions/1
Upload a Document (Auto-detect)
POST /api/upload
Upload an OSCAL document with automatic model-type and content-uuid detection. Accepts either a JSON body or a multipart file upload. Requires authentication.
If a document with the same content-uuid already exists and belongs to
the authenticated user, it will be updated. If it belongs to another user,
the request will be rejected with 403.
Headers
| Header | Value |
|---|---|
Content-Type | application/json or multipart/form-data |
Cookie | appSession=<session-cookie> |
Request Body
Option A — JSON body:
{
"catalog": {
"uuid": "...",
"metadata": { "..." }
}
}
Option B — Multipart form:
| Field | Type | Description |
|---|---|---|
file | File | An OSCAL JSON file |
Response 201 Created (new) or 200 OK (updated)
{
"content-uuid": "4429306e-0300-4900-9414-c340fe372f76",
"model-type": "catalog",
"title": "My Catalog",
"action": "created"
}
Error Responses
| Status | Condition |
|---|---|
400 | Invalid JSON or missing file |
401 | Not authenticated |
403 | Document exists and belongs to another user |
415 | Unsupported content type |
422 | Not a valid OSCAL document |
Example — Upload JSON Directly
curl -X POST \
-H "Content-Type: application/json" \
-H "Cookie: appSession=<session-cookie>" \
-d @my-ssp.json \
https://api.oscal.io/api/upload
Example — Upload a File
curl -X POST \
-H "Cookie: appSession=<session-cookie>" \
-F "file=@my-catalog.json" \
https://api.oscal.io/api/upload
Quick-Start Examples
Browse the Registry
# List all model types and their documents
for model in catalogs profiles component-definitions system-security-plans \
assessment-plans assessment-results plans-of-action-and-milestones; do
echo "=== $model ==="
curl -s https://api.oscal.io/api/v1/$model | python3 -m json.tool
done
Download, Edit, and Re-upload a Document
BASE=https://api.oscal.io
DOC_UUID=4429306e-0300-4900-9414-c340fe372f76
# 1. Download
curl -s "$BASE/api/v1/catalogs/$DOC_UUID" -o catalog.json
# 2. Edit the file with your preferred editor
# (update title, add controls, etc.)
# 3. Re-upload via PUT
curl -X PUT \
-H "Content-Type: application/json" \
-H "Cookie: appSession=<session-cookie>" \
-d @catalog.json \
"$BASE/api/v1/catalogs/$DOC_UUID"
Upload a New Document (Let the Server Detect Everything)
curl -X POST \
-H "Content-Type: application/json" \
-H "Cookie: appSession=<session-cookie>" \
-d @my-new-document.json \
https://api.oscal.io/api/upload
Use with jq for Scripting
# Get all catalog titles
curl -s https://api.oscal.io/api/v1/catalogs \
| jq -r '.[].title'
# Get the content-uuid of the first catalog
curl -s https://api.oscal.io/api/v1/catalogs \
| jq -r '.[0]."content-uuid"'
# Count documents by model type
for model in catalogs profiles component-definitions; do
count=$(curl -s https://api.oscal.io/api/v1/$model | jq length)
echo "$model: $count"
done
Fetch and Open in OSCAL Viewer
The registry integrates with viewer.oscal.io. You can construct viewer links programmatically:
https://viewer.oscal.io/{model}/?url={encoded-api-url}
Example:
BASE=https://api.oscal.io
MODEL=catalog
UUID=4429306e-0300-4900-9414-c340fe372f76
# URL-encode the API endpoint and open in viewer
API_URL="$BASE/api/v1/$MODEL/$UUID"
echo "https://viewer.oscal.io/$MODEL/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$API_URL', safe=''))")"
Rate Limits & Constraints
| Constraint | Value |
|---|---|
| Maximum document size | Limited by server memory (~50 MB) |
| Accepted formats | OSCAL JSON only (XML and YAML are not supported) |
| Authentication method | Auth0 session cookie |
| Content-Type for writes | application/json or multipart/form-data |
Sample Data & Testing
The registry ships with seven sample OSCAL documents — one for every model type — so you can exercise the API immediately after seeding.
Seeding the Database
# Load env vars and run the seed script
npm run db:seed
The seed script is idempotent: running it again skips documents that already exist.
Sample Documents
| Model Type | Title | Content UUID | Version |
|---|---|---|---|
catalog | NIST SP 800-53 Rev 5.1.1 | a1b2c3d4-0001-4000-8000-000000000001 | 5.1.1+u4 |
profile | NIST 800-53 Rev 5 Moderate Baseline Sample Tailoring | a1b2c3d4-0002-4000-8000-000000000002 | 1.0.0 |
component-definition | Azure Blob Storage Component Definition | a1b2c3d4-0003-4000-8000-000000000003 | 0.3.2 |
system-security-plan | FedRAMP System Security Plan (SSP) | a1b2c3d4-0004-4000-8000-000000000004 | fedramp-3.0.0rc1-oscal-1.1.2 |
assessment-plan | CISA SCuBA M365 Assessment Plan | a1b2c3d4-0005-4000-8000-000000000005 | EXPERIMENTAL |
assessment-results | CISA SCuBA M365 Assessment Results | a1b2c3d4-0006-4000-8000-000000000006 | 1.0 |
plan-of-action-and-milestones | IFA GoodRead Plan of Action and Milestones | a1b2c3d4-0007-4000-8000-000000000007 | 1.0 |
Quick Test: List All Documents
curl -s https://api.oscal.io/api/v1/catalogs | jq '.documents[].title'
# → "NIST SP 800-53 Rev 5.1.1"
Quick Test: Retrieve a Specific Document
# Fetch the sample SSP by its content UUID
curl -s https://api.oscal.io/api/v1/system-security-plans/a1b2c3d4-0004-4000-8000-000000000004 \
| jq '.["system-security-plan"].metadata.title'
# → "FedRAMP System Security Plan (SSP)"
Quick Test: Retrieve Every Model Type
BASE=https://api.oscal.io
for model in catalogs profiles component-definitions system-security-plans \
assessment-plans assessment-results plans-of-action-and-milestones; do
echo "=== $model ==="
curl -s "$BASE/api/v1/$model" | jq '.documents | length'
done
Quick Test: Open in OSCAL Viewer
# Open the sample catalog in the OSCAL Viewer
BASE=https://api.oscal.io
UUID=a1b2c3d4-0001-4000-8000-000000000001
API_URL="$BASE/api/v1/catalogs/$UUID"
echo "https://viewer.oscal.io/catalog/?url=$(python3 -c "import urllib.parse; print(urllib.parse.quote('$API_URL', safe=''))")"
Quick Test: Upload a New Document (authenticated)
# Upload the sample catalog via the multipart endpoint
curl -X POST https://api.oscal.io/api/upload \
-F "file=@seed-data/catalog-800-53r5.json" \
--cookie "your-session-cookie"
Tip: For authenticated endpoints, sign in through the browser first and copy the
appSessioncookie from DevTools → Application → Cookies.