> ## Documentation Index
> Fetch the complete documentation index at: https://docs.enkryptai.com/llms.txt
> Use this file to discover all available pages before exploring further.

# Compliance

# Compliance API Documentation

Welcome to the Compliance API documentation. These endpoints connect a **ChatGPT Enterprise workspace** to Enkrypt, then keep reading its compliance logs and scanning every message through one of your saved guardrails.

## Purpose

Your workspace already records what people send to and receive from the assistant. A compliance scan turns that record into something you can audit: it polls the provider's compliance export on a fixed interval, runs every message through the guardrail you name, and files the result — so you can answer "was anything sensitive pasted into ChatGPT last month" without asking anyone to forward a screenshot.

A scan is a long-lived object, not a one-off job. You create it once, point it at a workspace and a start time, and it keeps up from there. `cursor_end_time` is where the provider's own clock got to, and it moves forward on its own as the scan reads.

**Message text is never stored.** The findings, the counts and the coverage record are; the text of a message is fetched live from the provider when you deliberately ask for it with Get Compliance Message, and only while the provider still holds it.

## Who can call these APIs

Every endpoint on this page — reads as well as writes — requires the **governance officer** role.

* The role is granted **per project**, inside an organization, by an organization administrator.
* **The organization owner is not exempt.** An owner who has not been granted the role gets `403` from every endpoint here, exactly like any other member. Compliance is the one part of the platform where owning the account does not by itself let you read it.
* **Individual accounts cannot hold the role**, because there is no organization to grant it in. An individual account gets `403` with a message saying so. Compliance is therefore an organization-only feature today.

If you get a `403`, ask an administrator of your organization to grant you the governance officer role on the project your API key belongs to — the role is the only thing that changes the answer.

## Projects

A scan lives in a project, and the project comes from your **API key**, not from the request: a project key files its scans in that project, any other key uses `default`. Scan names are unique per project, so the same name may be used once in each.

## Offered APIs

* **test-connection**: Probe a compliance key and a workspace before any scan exists. Says whether the key works, whether the workspace was found, which event types that workspace can actually serve, and — if you propose a start time — how much history a backfill from there would pull. Stores nothing.

* **add-scan**: Create a scan. Starts polling immediately unless you create it with `status: "paused"`.

* **get-scan**: One scan's record, plus `lag_s` — how far behind the provider's clock the scan currently is.

* **modify-scan**: Change a scan's name, guardrail, event types, cursor, cadence, status or stored key.

* **delete-scan**: Stop the scan and delete its record, along with its log-file history.

* **list-scans**: Every scan in your project, paginated, optionally filtered by status.

* **start-scan** / **pause-scan**: Resume or suspend polling without editing anything else.

* **list-log-files**: The coverage record — which provider export files this scan opened and closed, how many messages each held and how many were flagged.

* **get-message**: One message's text, fetched live from the provider.

## Connecting a workspace

Two values identify the source, and one of them is easy to get wrong.

* `compliance_api_key` is the compliance API key issued by the provider for that workspace. Enkrypt stores it encrypted, and **no endpoint ever returns it** — not in plaintext and not masked. Replace it by sending a new one to Modify Compliance Scan.
* `workspace_id` is the **ChatGPT workspace UUID**. It is not an OpenAI organization id. An organization id has the right shape, so it is accepted, and then every poll comes back empty forever with nothing to explain why. If a scan is finding nothing at all, check this value first.

Run Test Compliance Connection before creating anything:

```bash theme={"system"}
curl -X POST https://api.enkryptai.com/compliance/test-connection \
  -H "apikey: YOUR_ENKRYPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "compliance_api_key": "YOUR_PROVIDER_COMPLIANCE_KEY",
    "workspace_id": "00000000-0000-0000-0000-000000000000",
    "event_types": ["CONVERSATION_MESSAGE", "CODEX_LOG"],
    "proposed_cursor": "2026-09-01T00:00:00Z"
  }'
```

A rejected key is **not** an HTTP error: the call returns `200` with `key_valid: false`. Read the fields, not the status code.

It also answers the two questions that are expensive to get wrong later:

* **Which event types this workspace serves.** Each entry in `event_types` comes back with `supported: true` or `false`. Selecting a service the workspace will never serve produces a scan that stays empty with no explanation.
* **How big the backfill is.** `proposed_cursor` makes the response carry a `backfill` estimate — files and bytes since that time. Volume varies enormously between workspaces, so the same 30-day start that seeds one workspace in minutes pulls millions of messages in another. Quote the estimate before committing to a start time.

The probe **stores nothing**, so Add Compliance Scan has to carry the key again.

## Creating a scan

```bash theme={"system"}
curl -X POST https://api.enkryptai.com/compliance/add-scan \
  -H "apikey: YOUR_ENKRYPT_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "scan_name": "ChatGPT Enterprise",
    "guardrails_name": "My Guardrail",
    "provider": "openai",
    "workspace_id": "00000000-0000-0000-0000-000000000000",
    "event_types": ["CONVERSATION_MESSAGE"],
    "cursor_end_time": "2026-09-01T00:00:00Z",
    "compliance_api_key": "YOUR_PROVIDER_COMPLIANCE_KEY"
  }'
```

* `guardrails_name` is one of your **saved guardrails** — the ones [Add Guardrail](/api-reference/guardrails-api-reference/endpoint/add-guardrail) creates. Every message the scan reads is evaluated against it, so it decides what "flagged" means for this scan. The name is not checked against your saved guardrails when the scan is created, so a typo shows up later as a scan that runs and flags nothing.
* `cursor_end_time` is where reading starts. It is the most consequential field on the request: it sets how much history gets pulled.
* `event_types` must be non-empty, and every value must be one the workspace serves.
* `provider` and `workspace_id` are **immutable**. Sending either to Modify Compliance Scan is a `400` — a scan whose workspace changed would carry a cursor and a history belonging to something else. Delete it and create a new one instead.

Everything else has a default: `status` is `active`, `list_interval_s` is 60 seconds and `pending_cap` is 5000 queued events before the poller backs off.

## Addressing a scan

Every endpoint except Add Compliance Scan, List Compliance Scans and Test Compliance Connection addresses one scan by **name**, in the `X-Enkrypt-Scan` header:

```bash theme={"system"}
curl https://api.enkryptai.com/compliance/get-scan \
  -H "apikey: YOUR_ENKRYPT_API_KEY" \
  -H "X-Enkrypt-Scan: ChatGPT Enterprise"
```

Omitting the header is a `400`; a name that is not yours is a `404`. The internal `scan_id` is returned on the record, but it is never something you have to send.

Names may contain letters, numbers, spaces and `_ & - . /`, must start and end with a letter or a number, and are at most 64 characters. Renaming is allowed — send `scan_name` to Modify Compliance Scan — and the new name is what `X-Enkrypt-Scan` must carry afterwards.

## Status, and the one value you cannot set

| Status            | Meaning                                                                         |
| ----------------- | ------------------------------------------------------------------------------- |
| `active`          | Polling. The only state that reads anything.                                    |
| `paused`          | Not polling. Nothing already queued is lost; Start Compliance Scan resumes it.  |
| `disabled`        | Not polling. Same effect as paused; use it to mean "switched off deliberately". |
| `needs_reconnect` | **Written by the scanner, never by you.** The provider rejected the stored key. |

`needs_reconnect` is not a value you may send — Add and Modify accept only the first three. It is how a scan tells you its credential stopped working. Recover by sending a fresh `compliance_api_key` to Modify Compliance Scan, or by calling Start Compliance Scan once the key is replaced; either one re-resolves the key.

You can list exactly the scans in that state:

```bash theme={"system"}
curl "https://api.enkryptai.com/compliance/list-scans?status=needs_reconnect" \
  -H "apikey: YOUR_ENKRYPT_API_KEY"
```

## Is it keeping up?

Get Compliance Scan answers this with three fields:

* `cursor_end_time` — how far through the provider's timeline the scan has read.
* `lag_s` — seconds between that cursor and now. A backfill starts with a very large value and works it down; a caught-up scan sits near its polling interval.
* `last_polled_at` — set on every successful poll, **whether or not anything new came back**. This is what separates a quiet workspace (polling, nothing to read) from a stopped scan (not polling at all). A `lag_s` that keeps growing while `last_polled_at` keeps moving means the workspace is busier than the scan can drain; a `last_polled_at` that has stopped moving means something else.

List Compliance Log Files is the coverage record behind those numbers: one row per provider export file the scan opened, with `messages_count` and `flagged_count` once the file is closed. A file marked `skipped` aged out of the provider's 30-day retention window before it could be downloaded.

## Reading a message

```bash theme={"system"}
curl "https://api.enkryptai.com/compliance/get-message?event_id=EVENT_ID" \
  -H "apikey: YOUR_ENKRYPT_API_KEY" \
  -H "X-Enkrypt-Scan: ChatGPT Enterprise"
```

This is the one endpoint that goes back to the provider for content, because the text was never stored. Serving it means re-downloading the entire export file the message came from — up to 15 MB for one message — out of the same rate-limited budget the scan itself uses.

**Put it behind a deliberate click.** Never call it from a list view, a hover card or a prefetch: a burst of calls starves the scan that is reading new messages.

`410` is not a failure. It means the file has aged out of the provider's 30-day retention window, which is the normal steady state for anything older than a month. The record's `content_expires_at` says when that will happen.

## Deleting a scan

Delete Compliance Scan stops the scan first and deletes the record only if the stop succeeded — if the scanner cannot be reached, the call fails and the scan is left intact rather than being orphaned into a workspace it keeps polling. The scan's log-file history is deleted with it, so export the coverage record first if you need to keep it.

## Errors you will actually see

| Status | What it means                                                                                                                                 |
| ------ | --------------------------------------------------------------------------------------------------------------------------------------------- |
| `400`  | A malformed body, an unknown field, a value outside an enum, a missing `X-Enkrypt-Scan`, or an attempt to change `provider` / `workspace_id`. |
| `401`  | Missing or invalid `apikey`.                                                                                                                  |
| `403`  | You do not hold the governance officer role — or the account is an individual account, which cannot hold it.                                  |
| `404`  | No scan of that name in this project.                                                                                                         |
| `409`  | A scan with that name already exists in this project.                                                                                         |
| `410`  | Get Compliance Message only: the provider no longer holds that content.                                                                       |
| `500`  | Something failed on our side.                                                                                                                 |
| `503`  | The scanning service is unreachable. Only the calls that need it live fail this way.                                                          |

A write that succeeded in the database but could not reach the scanning service comes back as a normal success carrying a `warning` field — the record is saved and the scanner picks it up on its next reconcile. Check for `warning` rather than assuming a `200` means the timer reloaded.

## Python SDK

The [Python SDK](/sdk-reference/python/introduction#compliance) ships a `ComplianceClient` from version `1.0.42`, with one method per endpoint on this page: `test_connection`, `add_scan`, `get_scan`, `modify_scan`, `delete_scan`, `list_scans`, `start_scan`, `pause_scan`, `list_log_files` and `get_message`.
