SecaniDocumentation
OSCAL

Validation API

Validate OSCAL 1.2.2 JSON documents over HTTP – stateless, anonymous or with an API key, powered by the same engine as the browser validator.

The Secani OSCAL Validation API validates OSCAL 1.2.2 JSON documents over HTTP. POST the document itself as the request body – no envelope, no account – and receive a machine-readable report from the same engine that powers the browser validator. It is built for machines in flight: CI jobs, integration scripts, and other tools that produce or transform OSCAL documents.

Privacy contract. Your document is processed transiently in memory on EU servers (Frankfurt) and discarded when the response is sent. Nothing is persisted, logged, or used for anything besides producing the response; telemetry is aggregate only (model type, size bucket, outcome, duration). The browser validator remains fully browser-local – your document never leaves the browser there.

Quickstart

curl -sS -X POST https://secani.com/api/oscal/v1/validate \
  -H "content-type: application/json" \
  --data-binary @system-security-plan.json

Large artifacts (full catalogs can exceed the 4 MB raw body limit) compress extremely well – send them gzipped:

gzip -c catalog.json | curl -sS -X POST https://secani.com/api/oscal/v1/validate \
  -H "content-type: application/json" \
  -H "content-encoding: gzip" \
  --data-binary @-

Response

A completed validation always returns HTTP 200 – "valid": false is a successful validation of an invalid document. Branch on the valid field, not the status code.

{
  "valid": false,
  "model": "system-security-plan",
  "oscalVersion": "1.2.2",
  "level": "schema",
  "complete": true,
  "issues": [
    {
      "ruleId": "nist-schema:1.2.2:system-security-plan:required:system-security-plan/metadata/version:required",
      "path": "/system-security-plan/metadata/version",
      "message": "must have required property 'version'",
      "keyword": "required",
      "severity": "error"
    }
  ],
  "issueCount": 37,
  "truncated": false,
  "meta": {
    "engine": "@secani/oscal",
    "validator": "1.2.2",
    "schemaSource": "NIST OSCAL v1.2.2 release JSON schemas",
    "patches": ["profile-combine-method"],
    "durationMs": 84
  }
}
FieldMeaning
validWhether the document validated without error findings. warning findings do not make it false.
modelDetected OSCAL model (one of the eight OSCAL 1.2.2 models).
oscalVersionThe oscal-version declared in the document's metadata; unknown if absent.
levelValidation level that ran: schema (default) or full (see Validation levels).
completetrue when every requested level ran to completion for a recognized model.
issues[].ruleIdStable id of the rule that produced the finding, schema-derived or semantic.
issues[].pathRFC 6901 JSON Pointer into the submitted document.
issues[].severityerror or warning; warning is advisory and does not make valid false.
issueCountTrue total number of issues, even when issues is truncated.
truncatedtrue when more than 200 issues were found and the list was capped.
meta.validatorOSCAL release the precompiled validators target (currently 1.2.2).
meta.patchesDocumented deviations from the raw NIST release schemas.

Errors

Errors use RFC 9457 problem details (application/problem+json) with a stable machine-readable code:

StatusCodeMeaning
400ERR_INVALID_JSONThe body is not well-formed JSON.
400ERR_INVALID_UTF8The request body is not valid UTF-8.
400ERR_UNKNOWN_MODELJSON, but no recognizable OSCAL root model.
400ERR_INVALID_CONTENT_ENCODINGContent-Encoding: gzip declared but the body is not valid gzip.
400ERR_UNSUPPORTED_PARAMETERA query parameter carries an invalid or unsupported value (e.g. an unknown rules id).
401ERR_INVALID_API_KEYThe presented API key is unknown or revoked.
405ERR_METHOD_NOT_ALLOWEDOnly POST (and OPTIONS) are accepted.
413ERR_PAYLOAD_TOO_LARGERaw body over 4 MB or decompressed body over 24 MB.
415ERR_UNSUPPORTED_MEDIA_TYPEContent type other than application/json, or an unsupported encoding.
429ERR_RATE_LIMITEDRate limit exceeded; see Retry-After.
500ERR_INTERNALUnexpected failure; no internals are disclosed.
503ERR_KEY_SERVICE_UNAVAILABLEKey verification is temporarily unavailable; see Retry-After.

Validation levels

The level query parameter controls validation depth. With none given, level=schema runs – plain JSON Schema validation, unchanged. level=full additionally engages the semantic layer: constraint and reference checks that go beyond what JSON Schema alone can express.

curl -sS -X POST "https://secani.com/api/oscal/v1/validate?level=full" \
  -H "content-type: application/json" \
  --data-binary @catalog.json

A full run that actually reaches the semantic layer can return findings with severity: "warning". warning findings are advisory and do not make a document invalid: they flag questionable but non-schema-violating patterns (such as an internal link that points at nothing), and valid stays true. Keep branching on valid; severity then separates errors from hints.

{
  "valid": true,
  "model": "catalog",
  "oscalVersion": "1.2.2",
  "level": "full",
  "complete": true,
  "issues": [
    {
      "ruleId": "CAT-002.a",
      "path": "/catalog/controls/0/links/0/href",
      "message": "local catalog link '#missing-part' does not resolve to a control, part, or resource",
      "keyword": "resolved-catalog-link",
      "severity": "warning"
    }
  ],
  "issueCount": 1,
  "truncated": false,
  "meta": {
    "engine": "@secani/oscal",
    "validator": "1.2.2",
    "schemaSource": "NIST OSCAL v1.2.2 release JSON schemas",
    "patches": ["profile-combine-method"],
    "durationMs": 112,
    "semanticRules": {
      "count": 30,
      "sha256": "d5e9ef745394fed06db58bcd5999c505cbccc83e460db024b6f3061cb710d213"
    }
  }
}

Default-off rules opt in individually through rules= – a comma-separated list of rule ids, valid only alongside level=full. An unknown id returns 400 ERR_UNSUPPORTED_PARAMETER naming the offending id, without dumping the rule catalog:

curl -sS -X POST "https://secani.com/api/oscal/v1/validate?level=full&rules=PROF-002.a" \
  -H "content-type: application/json" \
  --data-binary @profile.json

Every full run that reaches the semantic layer reports meta.semanticRules: the number of active semantic rules (count) and a stable SHA-256 over the sorted set of their ids (sha256). A full run rejected at the schema layer never reaches semantics, so it carries no meta.semanticRules. The hash identifies exactly which rule set was applied, so opting a rule in through rules= changes it. A stored response thus becomes a reproducible record of what was actually checked, even as the rule catalog grows across future releases.

Limits

  • Request body: 4 MB raw, 24 MB decompressed (Content-Encoding: gzip is supported and recommended for large documents).
  • One document per request.
  • Anonymous rate limits: 10 requests per minute and 300 per day per client, surfaced through RateLimit-Limit, RateLimit-Remaining, and RateLimit-Reset response headers, with Retry-After on 429 responses. Need more? Create an API key (see below) or contact hello@secani.com.

API keys

Higher, durable limits come with an API key. You create keys in the organization settings; each key is shown exactly once – at creation. Send it as Authorization: Bearer sk_oscal_…:

curl -sS -X POST https://secani.com/api/oscal/v1/validate \
  -H "authorization: Bearer sk_oscal_..." \
  -H "content-type: application/json" \
  --data-binary @system-security-plan.json

With a key, limits rise to 120 requests per minute and 10,000 per day per key – enforced centrally and independently of any single server instance, while the anonymous limits are best-effort per instance. The anonymous tier stays: requests without an Authorization header behave unchanged. An unknown or revoked key returns 401 ERR_INVALID_API_KEY and never falls back to the anonymous tier.

Reserved parameters

level and rules are no longer reserved – they are live; see Validation levels above. One parameter keeps a reserved value range:

  • oscal-version – pins the specification version and accepts only 1.2.2 today. Any other value is reserved for future OSCAL releases and returns ERR_UNSUPPORTED_PARAMETER with an explanation.

OpenAPI

The machine-readable contract is served at /api/oscal/v1/openapi.json. CORS is open (*), so the API can be called directly from browser-based tools.

See also: the browser validator for interactive use and the TypeScript toolkit behind both.