---
title: "Compliance Publishing"
description: "Policies API v1 endpoints for publishing compliance snapshots and evidence packages."
diataxis: reference
applies_to:
  product: "nirmata-control-hub"
audience: ["platform-engineer","developer"]
last_updated: 2026-10-06
url: https://docs.nirmata.io/docs/reference/rest-api/policies_api_v1/compliance_publishing/
---


<!-- Generated by scripts/gen-policies-api-v1/gen.py. Edit inventory.json, not this file. -->

Publish compliance snapshots and evidence packages to Nirmata Control Hub. `nctl` uses these endpoints when publishing compliance results. You can also call them directly from your own tooling.

All paths are relative to `/policies/api/v1`. See [Policies API v1](../) for authentication and conventions.

## Endpoints

| Operation | Method | Path |
|---|---|---|
| [List evidence packages](#list-evidence-packages) | `GET` | `/compliance/evidence-packages` |
| [Declare an evidence package](#declare-an-evidence-package) | `POST` | `/compliance/evidence-packages` |
| [Publish a compliance snapshot](#publish-a-compliance-snapshot) | `POST` | `/compliance/snapshots` |
| [Get an evidence package](#get-an-evidence-package) | `GET` | `/compliance/evidence-packages/{id}` |
| [Download an evidence package](#download-an-evidence-package) | `GET` | `/compliance/evidence-packages/{id}/download` |
| [Upload an evidence package part](#upload-an-evidence-package-part) | `PUT` | `/compliance/evidence-packages/{id}/parts/{index}` |

## Reference

### List evidence packages

```http
GET /policies/api/v1/compliance/evidence-packages
```

Lists the tenant's evidence packages, newest collection first, optionally filtered by target and standard.

**Roles:** `admin`, `platform`, `security`, `devops`

**Query parameters**

| Name | Type | Required | Default | Description |
|---|---|---|---|---|
| `targetId` | `string` | No | — | Filter by cluster ID (UUID) |
| `standard` | `string` | No | — | Filter by standard key |
| `limit` | `integer` | No | `50` | Page size (1-500) |
| `offset` | `integer` | No | `0` | Items to skip (&gt;=0) |

**Response** `200` (`application/json`)

Array of EvidencePackageDto (no total count)

| Field | Type | Description |
|---|---|---|
| `id` | `string (uuid)` | Package ID |
| `publishId` | `string (uuid)` | Idempotency key |
| `targetId` | `string (uuid)` | Cluster ID |
| `targetName` | `string` | Cluster name |
| `standard` | `string` | — |
| `reportId` | `string (uuid)` | Linked snapshot report, if any |
| `publisher` | `string` | — |
| `publisherVersion` | `string` | — |
| `collectedAt` | `string (date-time)` | — |
| `contentType` | `string` | Archive media type |
| `sha256` | `string` | Hex SHA-256 of the archive |
| `sizeBytes` | `integer` | — |
| `partSizeBytes` | `integer` | Required size of every part except the last |
| `partCount` | `integer` | — |
| `receivedParts` | `integer[]` | Part indexes already stored (for resuming) |
| `status` | `string` | uploading \| complete \| failed \| deleting |
| `errorMessage` | `string` | Set when failed |
| `metadata` | `object` | Producer metadata |
| `createdBy` | `string` | — |
| `createdAt` | `string (date-time)` | — |
| `completedAt` | `string (date-time)` | — |
| `duplicate` | `boolean` | Only on the declare response |

**Errors**

- `400` — INVALID_PAGINATION or INVALID_ID (bad targetId)
- `401` — Not authenticated
- `403` — Caller's role is not in the allowed roles

### Declare an evidence package

```http
POST /policies/api/v1/compliance/evidence-packages
```

Declares an evidence archive (size, SHA-256, metadata) before uploading its bytes in parts. The response gives partSizeBytes/partCount. Idempotent on publishId: re-declaring returns the existing package with its receivedParts (200, duplicate=true) so an interrupted upload can resume.

**Roles:** `admin`, `security`

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `publishId` | `string (uuid)` | Yes | Idempotency key |
| `schemaVersion` | `string` | Yes | Envelope version ("v1") |
| `publisher` | `object` | Yes | {name, version} |
| `cluster` | `object` | Yes | {id, kubeSystemUid, name}; at least one of id/kubeSystemUid |
| `standard` | `string` | Yes | Standard key |
| `collectedAt` | `string (date-time)` | Yes | When evidence was collected |
| `snapshotPublishId` | `string (uuid)` | No | publishId of a snapshot already published for the same cluster/standard |
| `contentType` | `string` | Yes | application/gzip \| application/x-gzip \| application/x-tar \| application/zip |
| `sizeBytes` | `integer` | Yes | Archive size (&gt;0, subject to a server max) |
| `sha256` | `string` | Yes | 64 hex chars |
| `metadata` | `object` | No | JSON object of producer metadata (size-capped) |

**Response** `201` (`application/json`)

EvidencePackageDto (200 when duplicate)

| Field | Type | Description |
|---|---|---|
| `id` | `string (uuid)` | Package ID |
| `publishId` | `string (uuid)` | Idempotency key |
| `targetId` | `string (uuid)` | Cluster ID |
| `targetName` | `string` | Cluster name |
| `standard` | `string` | — |
| `reportId` | `string (uuid)` | Linked snapshot report, if any |
| `publisher` | `string` | — |
| `publisherVersion` | `string` | — |
| `collectedAt` | `string (date-time)` | — |
| `contentType` | `string` | Archive media type |
| `sha256` | `string` | Hex SHA-256 of the archive |
| `sizeBytes` | `integer` | — |
| `partSizeBytes` | `integer` | Required size of every part except the last |
| `partCount` | `integer` | — |
| `receivedParts` | `integer[]` | Part indexes already stored (for resuming) |
| `status` | `string` | uploading \| complete \| failed \| deleting |
| `errorMessage` | `string` | Set when failed |
| `metadata` | `object` | Producer metadata |
| `createdBy` | `string` | — |
| `createdAt` | `string (date-time)` | — |
| `completedAt` | `string (date-time)` | — |
| `duplicate` | `boolean` | Only on the declare response |

**Errors**

- `400` — Validation failure (e.g. INVALID_PUBLISH_ID, UNSUPPORTED_CONTENT_TYPE, INVALID_SHA256, INVALID_SIZE, INVALID_METADATA, INVALID_SNAPSHOT_PUBLISH_ID, cluster errors)
- `409` — PUBLISH_ID_CONFLICT, PACKAGE_DELETED (expired), or AMBIGUOUS_CLUSTER
- `413` — EVIDENCE_PACKAGE_TOO_LARGE
- `422` — CLUSTER_NOT_FOUND / CLUSTER_MISMATCH, SNAPSHOT_NOT_FOUND, or SNAPSHOT_MISMATCH
- `401` — Not authenticated
- `403` — Caller's role is not in the allowed roles

### Publish a compliance snapshot

```http
POST /policies/api/v1/compliance/snapshots
```

Stores a compliance snapshot computed outside Nirmata (for example by `nctl scan compliance --publish`) as a completed compliance report for the identified cluster. Idempotent on publishId: returns 201 when created and 200 with duplicate=true (describing the original report) when that publishId was already recorded. Controls unknown to the tenant's catalog are dropped and listed in ignoredControls.

**Roles:** `admin`, `security`

**Request body** (`application/json`)

| Field | Type | Required | Description |
|---|---|---|---|
| `publishId` | `string (uuid)` | Yes | Producer-generated idempotency key |
| `schemaVersion` | `string` | Yes | Envelope version; only "v1" accepted |
| `publisher` | `object` | Yes | {name: lowercase tool id e.g. "nctl" (required), version (&lt;=128 chars)} |
| `cluster` | `object` | Yes | {id: Nirmata cluster UUID, kubeSystemUid: kube-system namespace UID, name (informational)}; at least one of id/kubeSystemUid required, must agree if both given |
| `snapshot` | `object` | Yes | nctl state.Snapshot: {id, standard (required, must be a known standard), cluster, context, timestamp (required, not in the future), controls: [{id, policy, policy_count, status: pass\|fail\|not-evaluated, pass_count, fail_count}], exceptions: ["ns/name"]} |

**Response** `201` (`application/json`)

SnapshotPublishResponseDto (200 when duplicate)

| Field | Type | Description |
|---|---|---|
| `reportId` | `string (uuid)` | Created (or original) report ID |
| `publishId` | `string` | — |
| `targetId` | `string (uuid)` | Resolved cluster ID |
| `targetName` | `string` | Nirmata cluster name |
| `standard` | `string` | — |
| `scanTimestamp` | `string (date-time)` | — |
| `score` | `number` | 0-100 |
| `level` | `string` | green \| yellow \| red \| unknown |
| `duplicate` | `boolean` | True if publishId was already recorded |
| `ignoredControls` | `string[]` | Control IDs dropped as unknown (empty on duplicate) |

**Errors**

- `400` — Validation failure, e.g. BODY_REQUIRED, PUBLISH_ID_REQUIRED, INVALID_PUBLISH_ID, UNSUPPORTED_SCHEMA_VERSION, INVALID_PUBLISHER, CLUSTER_REQUIRED, INVALID_CLUSTER_ID, SNAPSHOT_REQUIRED, STANDARD_REQUIRED, UNKNOWN_STANDARD, TIMESTAMP_REQUIRED, FUTURE_TIMESTAMP, unsupported control status
- `409` — PUBLISH_ID_CONFLICT (publishId reused for different content) or AMBIGUOUS_CLUSTER (several clusters share the kube-system UID)
- `422` — CLUSTER_NOT_FOUND or CLUSTER_MISMATCH (id and kubeSystemUid refer to different clusters)
- `401` — Not authenticated
- `403` — Caller's role is not in the allowed roles

### Get an evidence package

```http
GET /policies/api/v1/compliance/evidence-packages/{id}
```

Returns one evidence package's metadata and upload status.

**Roles:** `admin`, `platform`, `security`, `devops`

**Path parameters**

| Name | Type | Description |
|---|---|---|
| `id` | `string` | Package ID (UUID) |

**Response** `200` (`application/json`)

EvidencePackageDto

| Field | Type | Description |
|---|---|---|
| `id` | `string (uuid)` | Package ID |
| `publishId` | `string (uuid)` | Idempotency key |
| `targetId` | `string (uuid)` | Cluster ID |
| `targetName` | `string` | Cluster name |
| `standard` | `string` | — |
| `reportId` | `string (uuid)` | Linked snapshot report, if any |
| `publisher` | `string` | — |
| `publisherVersion` | `string` | — |
| `collectedAt` | `string (date-time)` | — |
| `contentType` | `string` | Archive media type |
| `sha256` | `string` | Hex SHA-256 of the archive |
| `sizeBytes` | `integer` | — |
| `partSizeBytes` | `integer` | Required size of every part except the last |
| `partCount` | `integer` | — |
| `receivedParts` | `integer[]` | Part indexes already stored (for resuming) |
| `status` | `string` | uploading \| complete \| failed \| deleting |
| `errorMessage` | `string` | Set when failed |
| `metadata` | `object` | Producer metadata |
| `createdBy` | `string` | — |
| `createdAt` | `string (date-time)` | — |
| `completedAt` | `string (date-time)` | — |
| `duplicate` | `boolean` | Only on the declare response |

**Errors**

- `400` — INVALID_ID
- `404` — PACKAGE_NOT_FOUND
- `401` — Not authenticated
- `403` — Caller's role is not in the allowed roles

### Download an evidence package

```http
GET /policies/api/v1/compliance/evidence-packages/{id}/download
```

Streams the complete evidence archive as an attachment (evidence-<standard>-<id>.tar.gz|tar|zip), with Content-Length and an X-Content-SHA256 header for integrity checking. Only complete packages can be downloaded.

**Roles:** `admin`, `platform`, `security`, `devops`

**Path parameters**

| Name | Type | Description |
|---|---|---|
| `id` | `string` | Package ID (UUID) |

**Response** `200` (`archive media type of the package (e.g. application/gzip)`)

Binary archive; headers Content-Disposition, Content-Length, X-Content-SHA256

**Errors**

- `400` — INVALID_ID
- `404` — PACKAGE_NOT_FOUND
- `409` — PACKAGE_NOT_COMPLETE, PACKAGE_FAILED or PACKAGE_DELETED
- `401` — Not authenticated
- `403` — Caller's role is not in the allowed roles

### Upload an evidence package part

```http
PUT /policies/api/v1/compliance/evidence-packages/{id}/parts/{index}
```

Uploads one part of a declared package as raw bytes. Every part except the last must be exactly partSizeBytes. When the last missing part arrives the archive is verified against its SHA-256 and the package becomes complete (or failed). Re-uploading a part is safe.

**Roles:** `admin`, `security`

**Path parameters**

| Name | Type | Description |
|---|---|---|
| `id` | `string` | Package ID (UUID) |
| `index` | `integer` | Zero-based part index (0 to partCount-1) |

**Request body** (`application/octet-stream`)

**Response** `200` (`application/json`)

EvidencePackageDto

| Field | Type | Description |
|---|---|---|
| `id` | `string (uuid)` | Package ID |
| `publishId` | `string (uuid)` | Idempotency key |
| `targetId` | `string (uuid)` | Cluster ID |
| `targetName` | `string` | Cluster name |
| `standard` | `string` | — |
| `reportId` | `string (uuid)` | Linked snapshot report, if any |
| `publisher` | `string` | — |
| `publisherVersion` | `string` | — |
| `collectedAt` | `string (date-time)` | — |
| `contentType` | `string` | Archive media type |
| `sha256` | `string` | Hex SHA-256 of the archive |
| `sizeBytes` | `integer` | — |
| `partSizeBytes` | `integer` | Required size of every part except the last |
| `partCount` | `integer` | — |
| `receivedParts` | `integer[]` | Part indexes already stored (for resuming) |
| `status` | `string` | uploading \| complete \| failed \| deleting |
| `errorMessage` | `string` | Set when failed |
| `metadata` | `object` | Producer metadata |
| `createdBy` | `string` | — |
| `createdAt` | `string (date-time)` | — |
| `completedAt` | `string (date-time)` | — |
| `duplicate` | `boolean` | Only on the declare response |

**Errors**

- `400` — INVALID_ID, INVALID_PART_INDEX, or INVALID_PART_SIZE
- `404` — PACKAGE_NOT_FOUND
- `409` — PACKAGE_FAILED or PACKAGE_DELETED
- `401` — Not authenticated
- `403` — Caller's role is not in the allowed roles

**Example**

```bash
curl -X PUT -H 'Content-Type: application/octet-stream' --data-binary @part0.bin .../compliance/evidence-packages/{id}/parts/0
```


