API Keys & Bulk Export
An API key lets a script or another tool talk to Clariti without a person signing in. The most common use is bulk export: pulling every asset and finding into your SIEM, a BI dashboard, a spreadsheet, or a nightly backup.
This page is the reference for the public API. The production API does not serve an interactive explorer (/docs, /redoc and /openapi.json are turned off), so everything you need is here.
Create an API Key
Only workspace admins can create or revoke keys.
- In Clariti, open Settings → API keys (under the Workspace group).
- Click Create API key.
- Give it a Name that says what uses it, such as
SIEM exportorNightly backup. - Choose an Access level (see below). Pick the narrowest one that does the job.
- Click Create key, then Copy the key and store it in your password manager or secrets vault.
The key is shown once. Clariti keeps only a hash of it, so nobody (including Clariti support) can show it to you again. If you lose it, revoke it and create a new one.
Keys look like clk_1a2b3c4d5e6f_.... The part after clk_ up to the next underscore is a public prefix: it is what you see in the key list, so you can tell keys apart without exposing the secret. The key list also shows when each key was created and last used.
A workspace can have up to 25 active keys. Revoke keys you no longer use.
Access Levels
| Access | Can do | Cannot do |
|---|---|---|
| Export only | GET /api/v1/export/*: download assets and findings | Anything else, including other read endpoints |
| Read only | GET requests on your workspace data | Any change (POST, PATCH, PUT, DELETE) |
| Read & write | Read and change workspace data, e.g. triage findings | Manage the workspace |
No key, whatever its access level, can call /api/v1/admin/*, /api/v1/billing/* or /api/v1/oauth/*. A key can never add members, create or revoke other keys, change billing, or connect integrations. Those need a signed-in admin.
For a SIEM or reporting feed, use Export only.
When a Key Stops Working
A key acts on behalf of the admin who created it, and every action it takes is attributed to that admin. It stops working when:
- You revoke it in Settings → API keys. The next request fails with
401. - Its creator is disabled. Requests fail with
403until the admin is re-enabled. - Its creator is removed from the workspace. Their keys are deleted with them, and requests fail with
401.
Before you remove an admin, check the key list for keys they created and replace any that are still in use.
Authentication
Send the key as a bearer token (Authorization: Bearer clk_...) on every request to https://api.clariti-ai.xyz. With your key in the CLARITI_API_KEY environment variable:
curl -sS "https://api.clariti-ai.xyz/api/v1/export/findings?limit=10" \
-H "Authorization: Bearer $CLARITI_API_KEY"
Keep keys out of scripts and source control. Load them from an environment variable or a secrets manager instead, as this example and the ones below do.
Export Endpoints
| Endpoint | Returns |
|---|---|
GET /api/v1/export/assets | Every asset record (devices, users, software, cloud and network assets), one per source: the same laptop seen by two tools appears twice |
GET /api/v1/export/findings | Every finding |
Query Parameters
| Parameter | Values | Default | Notes |
|---|---|---|---|
format | jsonl or csv | jsonl | JSONL is one JSON object per line |
limit | 1 to 5000 | 1000 | Rows per page |
cursor | Value of the previous page's X-Next-Cursor | none | Leave it out for the first page |
Each request returns one page, oldest record first. The response body is the page itself:
format=jsonlreturnsapplication/x-ndjson. List fields are JSON arrays and missing values arenull.format=csvreturnstext/csvwith a header row on every page. List fields are joined with;, object fields are compact JSON, booleans aretrueorfalse, and any cell starting with=,+,-,@, a tab or a carriage return is prefixed with'so spreadsheets don't run it as a formula.
An empty workspace returns 200 with an empty body.
Pagination
Paging information comes back in response headers, not in the body:
| Header | Meaning |
|---|---|
X-Has-More | true if there is another page, false on the last page |
X-Next-Cursor | Pass this as cursor to get the next page. Empty on the last page |
Keep requesting with the new cursor until X-Has-More is false. Records created while you are paging show up on a later page rather than shifting earlier ones, so you won't get duplicates or gaps.
Pages are ordered by when a record was created, so a cursor only moves forward. A finding that changes status after you exported it won't reappear on a later page. To capture changes, run a fresh export from the first page (for example, nightly) rather than resuming from an old cursor.
Example: curl and bash
Exports every finding to findings.jsonl:
#!/usr/bin/env bash
set -euo pipefail
API="https://api.clariti-ai.xyz/api/v1/export/findings"
KEY="${CLARITI_API_KEY:?set CLARITI_API_KEY first}"
OUT="findings.jsonl"
cursor=""
: > "$OUT"
while true; do
curl -sS --fail-with-body -G "$API" \
-H "Authorization: Bearer $KEY" \
--data-urlencode "format=jsonl" \
--data-urlencode "limit=5000" \
${cursor:+--data-urlencode "cursor=$cursor"} \
-D headers.txt -o page.jsonl
cat page.jsonl >> "$OUT"
has_more=$(grep -i '^x-has-more:' headers.txt | tr -d '\r' | awk '{print $2}')
cursor=$(grep -i '^x-next-cursor:' headers.txt | tr -d '\r' | awk '{print $2}')
[ "$has_more" = "true" ] || break
done
rm -f headers.txt page.jsonl
echo "Wrote $(wc -l < "$OUT") findings to $OUT"
Change findings to assets in API (and the file name) to export assets. --fail-with-body needs curl 7.76 or later; it makes the script stop on an error instead of saving the error as data.
Example: Python
Uses the requests library (pip install requests). Handles both formats, waits and retries when rate-limited, and keeps only the first CSV header row:
import os
import time
import requests
API = "https://api.clariti-ai.xyz/api/v1/export"
KEY = os.environ["CLARITI_API_KEY"]
def export(resource: str, path: str, fmt: str = "jsonl", limit: int = 5000) -> int:
"""Write every page of /export/<resource> to one file. Returns the page count."""
session = requests.Session()
session.headers["Authorization"] = f"Bearer {KEY}"
cursor = None
pages = 0
with open(path, "w", encoding="utf-8", newline="") as out:
while True:
params = {"format": fmt, "limit": limit}
if cursor:
params["cursor"] = cursor
resp = session.get(f"{API}/{resource}", params=params, timeout=60)
if resp.status_code == 429:
time.sleep(int(resp.headers.get("Retry-After", "60")))
continue
if not resp.ok:
try:
err = resp.json()["error"]
detail = f"{err['code']}: {err['message']}"
except (ValueError, KeyError):
detail = resp.text[:200]
raise RuntimeError(f"HTTP {resp.status_code} {detail}")
resp.encoding = "utf-8"
body = resp.text
if fmt == "csv" and pages > 0:
body = body.split("\n", 1)[1] if "\n" in body else "" # drop repeated header
out.write(body)
pages += 1
if resp.headers.get("X-Has-More") != "true":
return pages
cursor = resp.headers["X-Next-Cursor"]
if __name__ == "__main__":
export("assets", "assets.csv", fmt="csv")
export("findings", "findings.jsonl")
Columns
The export columns are a fixed allow-list, in this order. CSV header rows use exactly these names, and JSONL objects use the same keys. Raw vendor payloads are never included.
Assets
| Column | Description |
|---|---|
id | Clariti asset ID |
source | Integration the record came from |
source_id | The asset's ID in that integration |
asset_type | DEVICE, USER, SOFTWARE, CLOUD, NETWORK or OTHER |
hostname | Hostname |
os_name | Operating system |
os_version | Operating system version |
serial_number | Hardware serial number |
ip_addresses | IP addresses (list) |
mac_addresses | MAC addresses (list) |
status | ACTIVE, STALE or DECOMMISSIONED |
owner | Assigned owner |
department | Department |
business_criticality | CRITICAL, HIGH, MEDIUM, LOW or UNKNOWN |
tags | Tags (list) |
managed_by | Tools managing the asset (list) |
software_count | Number of installed software packages |
quality_score | How complete the record is, from 0 to 1 |
site_id | Site the asset belongs to |
first_seen | First seen (ISO 8601 timestamp) |
last_seen | Last seen (ISO 8601 timestamp) |
created_at | When Clariti first recorded it (ISO 8601 timestamp) |
Findings
| Column | Description |
|---|---|
id | Clariti finding ID |
finding_type | coverage_gap, drift, anomaly, stale_asset, config_risk or vulnerability |
severity | critical, high, medium, low or info |
status | open, in_progress, resolved, accepted_risk or false_positive |
title | Short title |
description | Full description |
source | What raised it: correlation, configcheck, hygiene, manual, integration or intel |
source_ref | Details that tie the finding back to its source (an object; see below) |
affected_asset_ids | IDs of affected assets (list; matches the assets export id) |
cvss_score | CVSS score |
epss_score | EPSS exploit probability |
epss_percentile | EPSS percentile |
kev_listed | true if on CISA's Known Exploited Vulnerabilities list |
due_date | Remediation due date |
assigned_at | When it was assigned |
resolved_at | When it was resolved |
created_at | When it was created |
updated_at | When it last changed |
last_activity_at | Last activity on it |
exposure_category | The exposure score category it counts in, for example exploited_vulns or identity_access |
exposure_points | How much exposure it adds to that category |
projected_lift | Roughly how many score points (0-100) fixing it recovers, once the fix is verified |
All timestamps are ISO 8601. Empty values are blank in CSV and null in JSONL. kev_listed is true or false in both formats.
The three exposure columns are empty for a finding that does not count toward the exposure score (for example a stale-asset or alert finding, or one marked false positive). projected_lift is an estimate: after a fix is verified, the recalculated score is the real number. New columns are always added at the end, so existing column positions do not change.
source_ref is a JSON object in JSONL. In CSV the cell holds the same object as compact JSON with its keys sorted (for example {"check_id":"mfa_disabled","run_id":"..."}), so any JSON parser can read it.
Errors
Every error uses the same JSON envelope:
{
"error": {
"code": "FORBIDDEN",
"message": "This API key can only call the export endpoints",
"details": {}
}
}
Branch on code, not on message: messages are written for people and may be reworded. details holds extra context when there is any, such as the list of invalid fields on a 422.
| Status | Code | Common causes |
|---|---|---|
401 | UNAUTHORIZED | Missing Authorization header, a mistyped key, a revoked key, or the key's creator was removed |
403 | FORBIDDEN | The key's access level doesn't allow this request, the route is off-limits to all keys (/admin, /billing, /oauth), or the key's creator is disabled |
422 | VALIDATION_ERROR | A bad query parameter, e.g. format=xml or limit=10000 |
429 | RATE_LIMITED | Too many requests. Wait for the number of seconds in Retry-After, then retry |
A cursor that has been edited or truncated returns 400 with code VALIDATION_ERROR. Start again from the first page.
Rate Limits
Each workspace can make 100 requests per minute to each endpoint. The limit counts the endpoint and HTTP method together, so GET /api/v1/export/assets and GET /api/v1/export/findings each have their own 100.
The limit is per workspace, not per key. All of your keys, and everyone using the Clariti app in your workspace, share it. Responses include X-RateLimit-Limit and X-RateLimit-Remaining so you can see how close you are.
When you go over, you get 429 RATE_LIMITED with a Retry-After: 60 header. Wait that long and retry. With limit=5000, exporting 50,000 findings takes 10 requests, so a typical SMB export never gets near the limit.
Security Tips
- Give each tool its own key, so revoking one doesn't break the others.
- Use Export only unless the tool really needs more.
- Store keys in a secrets manager or environment variable, never in a script or a shared document.
- Check Last used in the key list. A key nobody has used in months is a key to revoke.
- If a key may have leaked, revoke it right away and create a new one. Revoking takes effect on the next request.