---
title: API keys
description: Programmatic access for your own services and CI — scoped to a role, optionally to a workspace, and shown exactly once.
label: API keys
---

An API key authenticates a **service or a pipeline**, not a person and not an
agent. Agents authenticate with a key pair at the gateway; people authenticate
with a session. API keys are for your deployment tooling, your CI, and your own
backend services talking to the control plane.

## Create one

```bash
curl -X POST https://veefore.example.com/v1/api-keys \
  -H "Authorization: Bearer $EXISTING_KEY" \
  -H 'content-type: application/json' \
  -d '{
    "name": "ci-policy-gate",
    "role_id": "role_01JAV…",
    "workspace_id": "wsp_01JAV…",
    "expires_in_days": 90
  }'
```

```json
{
  "data": {
    "api_key": { "id": "apk_01JAV…", "name": "ci-policy-gate", "created_at": "…" },
    "token": "aos_sk_…",
    "note": "Store this key now. It cannot be retrieved again."
  }
}
```

:::danger `token` is shown exactly once
Only a hash is stored. There is no endpoint that returns it again, and there
never will be. Capture it from this response or create a new key.

Note the field name: the secret is `token`, not `secret`. `api_key` is the
metadata record.
:::

:::params

:::param name | string | required
What this key is for. It appears in the audit trail on every action the key
takes, so make it specific — `ci-policy-gate` beats `ci`.
:::

:::param role_id | string | required
The role whose permissions the key carries. `GET /v1/roles` to list them.
:::

:::param workspace_id | string | optional
Scope the key to one workspace. Do this unless the key genuinely needs
organization-wide reach.
:::

:::param expires_in_days | number | optional
1 to 730. Set it. A key with no expiry is a key that outlives the reason it was
created.
:::

:::

## Use one

```bash
curl -s https://veefore.example.com/v1/agents \
  -H "Authorization: Bearer aos_sk_…" \
  -H 'x-veefore-workspace: wsp_01JAV…' \
  -H 'x-veefore-environment: env_01JAV…'
```

Same header as a session token. The scope headers are optional — omit them and
you get the organization-wide view, subject to the key's permissions and its
workspace scope.

## Pick the smallest role

An API key's permissions are its role's permissions. The roles and their
contents are in [Permissions and roles](/reference/permissions).

| Key purpose | Role |
| --- | --- |
| CI policy gate | `DEVELOPER`, or a custom role with `policy:read` + `policy:simulate` |
| Deployment pipeline | Custom: `policy:read`, `policy:activate`, `environment:set_mode` |
| Monitoring and dashboards | `VIEWER` |
| Audit export to a SIEM | Custom: `audit:read`, `audit:export` |
| Agent provisioning service | Custom: `agent:create`, `agent:read`, `tool:read` |

:::security Never `OWNER`
`OWNER` holds every permission including `organization:delete`. A key in a CI
environment variable is a key that is in your CI provider's storage, in build
logs if anyone echoes the environment, and in whatever a compromised build step
can read.

Create a custom role with exactly the permissions the key needs. It takes two
minutes and it bounds the damage.
:::

Two further protections apply automatically and are worth knowing about:

**High-impact permissions cannot come from a wildcard.** `agent:revoke`,
`policy:activate`, `security:emergency_action` and the rest must appear on the
role explicitly.

**Step-up permissions are unavailable to API keys.** Step-up requires a human
re-authenticating, and an API key has no human. A key holding a step-up
permission still cannot perform the operation — which means a CI key cannot
activate a policy on its own. That is deliberate: activation is a decision, not
a build step.

## Store it properly

| Where | Verdict |
| --- | --- |
| Your CI provider's encrypted secret store | Correct |
| Your cloud provider's secret manager | Correct |
| An environment variable injected at runtime | Acceptable |
| A file in the repository | Never |
| A `.env` that is gitignored | Risky — easy to commit by accident |
| Checked into a Dockerfile or image layer | Never |

`npm run quality:audit` fails the build on a committed secret, which catches
some of this. Do not rely on it as the control.

## Rotate and revoke

```bash
curl -s https://veefore.example.com/v1/api-keys \
  -H "Authorization: Bearer $EXISTING_KEY"

curl -X POST https://veefore.example.com/v1/api-keys/apk_01ABC/revoke \
  -H "Authorization: Bearer $EXISTING_KEY"
```

Revocation is immediate. To rotate, create the new key, deploy it, then revoke
the old one — in that order, so there is no window where nothing works.

Rotate on a schedule and immediately on any suspicion. Set `expires_in_days` so
expiry happens even when nobody remembers.

## Everything is attributed

Every action an API key takes is recorded against that key, so the audit trail
distinguishes "the CI gate read the policy" from "a developer read the policy".
`principal.type` is `API_KEY`, which is also a policy field:

```json
{ "kind": "condition", "id": "c-principal", "field": "principal.type", "operator": "eq", "value": "API_KEY" }
```

Useful for rules that should apply to automation but not to people, or the
reverse.

## Related

:::cards

:::card Permissions and roles | /reference/permissions
Every permission and which roles hold it. Generated.
:::

:::card Gate your pipeline | /build/ci-gate
What a CI key is actually for.
:::

:::card RBAC | /secure/rbac
Custom roles, scoped grants, high-impact rules.
:::

:::card Authentication | /reference/authentication
Sessions, keys and agent tokens compared.
:::

:::
