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

# REST API

> Run your saved Configview queries from your own applications over HTTP, using scoped machine-to-machine API keys.

<Note>
  **This is the programmatic path for *applications*.** If you want a person to ask questions in chat, connect an [AI assistant](/llm) instead. If you want to author the queries this API runs, see [Saved queries](/saved-queries).
</Note>

## What it is

The Configview **REST API** lets your own applications — scripts, CI jobs, internal dashboards, scheduled audits — run your [saved queries](/saved-queries) over HTTP and get JSON back. It reuses the exact same query engine that powers the dashboard, the Slack bot, and the AI connector, so a query behaves identically no matter which surface runs it.

Calls are authenticated with a **machine-to-machine API key**, not a user login. Each key is minted by an admin and **scoped to an explicit allowlist of queries** — a key can only run the queries you granted it, and nothing else.

|          | REST API (this page)                           | [AI connector (MCP)](/llm)                  |
| -------- | ---------------------------------------------- | ------------------------------------------- |
| Caller   | Your applications / scripts                    | A person, through Claude / ChatGPT / Gemini |
| Auth     | Long-lived API key (`Authorization: Bearer …`) | OAuth 2.1, per-user                         |
| Scope    | Explicit per-key query allowlist               | The curated catalog                         |
| Best for | Automation, CI, scheduled jobs, embedding      | Ad-hoc questions in natural language        |

Both are **read-only** — they run saved queries, they don't write to your systems and can't run arbitrary SQL.

***

## Base URL

Every Configview customer has a dedicated API at:

```
https://<your-subdomain>.configview.com/api/v1
```

Replace `<your-subdomain>` with whatever your team uses to log into the dashboard. If your dashboard is `acme.configview.com`, your API base URL is `https://acme.configview.com/api/v1`.

***

## Authentication

Every request must send an API key as a Bearer token:

```
Authorization: Bearer cv_live_a1b2c3d4_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx
```

Keys look like `cv_live_<prefix>_<secret>`. The `<prefix>` is a short non-secret identifier shown in the admin UI so you can tell keys apart; the `<secret>` portion is shown **only once**, at creation.

A request with a missing or invalid key returns `401`:

```json theme={null}
{ "error": "invalid_or_missing_api_key" }
```

<Warning>
  Treat the full key like a password. Configview stores only a hash of it — if you lose it, you can't recover it, only revoke it and mint a new one. Never commit a key to source control or embed it in client-side code.
</Warning>

***

## Create an API key

API keys are minted from the dashboard by an admin.

<Steps>
  <Step title="Open the API Keys page">
    Click your avatar in the top-right of the dashboard, then **API Keys**.
  </Step>

  <Step title="Create a key">
    Click **New key** and fill in:

    * **Name** — a label so you can recognize it later (e.g. *"CI nightly SOC2 audit"*).
    * **Allowed queries** — search and select the saved queries this key may run. This is **deny-by-default**: a key with an empty allowlist can run nothing.
    * **Rate limit** — maximum requests per minute for this key (default **60**).
    * **Expiry** *(optional)* — number of days until the key automatically stops working. Leave blank for a non-expiring key.
  </Step>

  <Step title="Copy the key now">
    The full `cv_live_…` key is shown **exactly once**. Copy it into your secret manager immediately — you won't be able to see it again. If you lose it, revoke the key and create a new one.
  </Step>
</Steps>

To retire a key, click **Revoke** on the API Keys page. Revocation takes effect immediately on the next request.

***

## Endpoints

All endpoints are relative to your base URL and require the `Authorization` header.

### List queries

```
GET /api/v1/queries
```

Returns the queries **this key is allowed to run** (not your entire catalog).

```json theme={null}
{
  "queries": [
    {
      "seed_key": "okta.users_without_mfa",
      "title": "Okta users without MFA",
      "description": "Active Okta users with no enrolled MFA factor.",
      "category": "Identity",
      "labels": ["security", "okta"],
      "parameters": [],
      "max_age_seconds": 3600
    }
  ]
}
```

`max_age_seconds` is how long a cached result for that query stays fresh (see [Caching](#caching)). `parameters` describes any inputs the query accepts.

### Get one query

```
GET /api/v1/queries/{seed_key}
```

Returns the metadata for a single query. Useful for discovering its parameter schema before running it. Returns `403 query_not_in_scope` if the key isn't allowed to run it, or `404 query_not_found` if no such query exists.

### Run a query

```
POST /api/v1/queries/{seed_key}/run
```

Runs the query and returns the result rows. The request body is optional:

| Field        | Type    | Default | Description                                                 |
| ------------ | ------- | ------- | ----------------------------------------------------------- |
| `params`     | object  | `{}`    | Values for the query's parameters, keyed by parameter name. |
| `force_live` | boolean | `false` | Bypass the cache and run against live data.                 |

Example body:

```json theme={null}
{
  "params": { "days": 90 },
  "force_live": false
}
```

Response:

```json theme={null}
{
  "seed_key": "okta.users_without_mfa",
  "title": "Okta users without MFA",
  "columns": ["email", "status", "created"],
  "rows": [
    ["jane@acme.com", "ACTIVE", "2026-01-04"],
    ["raj@acme.com", "ACTIVE", "2026-02-19"]
  ],
  "row_count": 2,
  "from_cache": true,
  "cache_age_seconds": 142,
  "ran_at": "2026-06-18T14:21:00+00:00"
}
```

`columns` and `rows` are positional — `rows[i][j]` is the value for `columns[j]`. When `from_cache` is `false`, the response includes `run_duration_ms` instead of `cache_age_seconds` and `ran_at`.

***

## Caching

By default, running a query returns the most recent cached result if it's still fresh — fast, and easy on your integrations. Each query defines its own freshness window (`max_age_seconds`).

* `from_cache: true` — you got a stored result; `cache_age_seconds` tells you how old it is.
* `from_cache: false` — the cache was empty or stale, so the query ran live.
* Pass `"force_live": true` to always run against live data (slower; use it sparingly).

***

## Rate limits

Each key has a per-minute request limit (default 60, set when the key is created). Exceeding it returns `429`:

```json theme={null}
{ "error": "rate_limit_exceeded" }
```

Back off and retry after the current minute. If you have a legitimate high-volume use case, mint the key with a higher limit or split work across keys.

***

## Errors

All errors return a JSON body with an `error` field and the appropriate HTTP status.

| Status | `error`                      | Meaning                                                               |
| ------ | ---------------------------- | --------------------------------------------------------------------- |
| `400`  | `params_must_be_object`      | `params` was sent but isn't a JSON object.                            |
| `400`  | `invalid_parameters`         | A parameter value was missing or the wrong type. `detail` explains.   |
| `401`  | `invalid_or_missing_api_key` | No `Authorization` header, or the key is unknown / expired / revoked. |
| `403`  | `query_not_in_scope`         | The key is valid but not allowed to run this query.                   |
| `404`  | `query_not_found`            | No query exists for that `seed_key` (or it was deprecated).           |
| `429`  | `rate_limit_exceeded`        | Per-minute limit for this key exceeded.                               |
| `500`  | `query_execution_failed`     | The query ran but errored. `detail` has the message.                  |

***

## Call it from your application

Set the base URL and key once, then call the endpoints. Store the key in your secret manager or an environment variable — never hard-code it.

<CodeGroup>
  ```bash cURL theme={null}
  # List the queries this key can run
  curl -s https://acme.configview.com/api/v1/queries \
    -H "Authorization: Bearer $CONFIGVIEW_API_KEY"

  # Run a query with a parameter
  curl -s -X POST \
    https://acme.configview.com/api/v1/queries/okta.users_without_mfa/run \
    -H "Authorization: Bearer $CONFIGVIEW_API_KEY" \
    -H "Content-Type: application/json" \
    -d '{"params": {"days": 90}}'
  ```

  ```python Python theme={null}
  import os
  import requests

  BASE = "https://acme.configview.com/api/v1"
  KEY = os.environ["CONFIGVIEW_API_KEY"]
  session = requests.Session()
  session.headers["Authorization"] = f"Bearer {KEY}"

  # List allowed queries
  queries = session.get(f"{BASE}/queries").json()["queries"]

  # Run one and turn positional rows into dicts
  resp = session.post(
      f"{BASE}/queries/okta.users_without_mfa/run",
      json={"params": {"days": 90}},
  )
  resp.raise_for_status()
  result = resp.json()

  records = [dict(zip(result["columns"], row)) for row in result["rows"]]
  print(f"{result['row_count']} rows (from_cache={result['from_cache']})")
  for r in records:
      print(r["email"], r["status"])
  ```

  ```javascript Node.js theme={null}
  const BASE = "https://acme.configview.com/api/v1";
  const KEY = process.env.CONFIGVIEW_API_KEY;
  const headers = { Authorization: `Bearer ${KEY}` };

  // Run a query
  const res = await fetch(`${BASE}/queries/okta.users_without_mfa/run`, {
    method: "POST",
    headers: { ...headers, "Content-Type": "application/json" },
    body: JSON.stringify({ params: { days: 90 } }),
  });

  if (!res.ok) {
    throw new Error(`Configview API ${res.status}: ${JSON.stringify(await res.json())}`);
  }

  const { columns, rows, row_count, from_cache } = await res.json();
  const records = rows.map((row) =>
    Object.fromEntries(columns.map((c, i) => [c, row[i]])),
  );
  console.log(`${row_count} rows (from_cache=${from_cache})`);
  ```
</CodeGroup>

***

## Security best practices

<CardGroup cols={2}>
  <Card title="Scope every key narrowly" icon="key">
    Grant a key only the queries it needs. A CI audit key that runs three reports should be allowed exactly those three.
  </Card>

  <Card title="One key per consumer" icon="layers">
    Use a separate key per application or job. Revoking one then doesn't disrupt the others, and the activity log attributes runs to the right caller.
  </Card>

  <Card title="Set an expiry" icon="clock">
    For temporary or contractor access, set an expiry so the key dies on its own even if no one remembers to revoke it.
  </Card>

  <Card title="Rotate by revoke-and-reissue" icon="refresh-cw">
    There's no in-place rotation. To rotate, mint a new key, switch your app over, then revoke the old one.
  </Card>
</CardGroup>

Every API call is recorded in the dashboard **Activity** log, attributed to the key that made it.
