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

# API cookbook

> Practical REST workflows with account context, safe writes, encrypted records and clear failure handling.

export function AirctrlWordmark() {
  return <span className="airctrl-wordmark" aria-label="AIRCTRL">
      <span aria-hidden="true" className="airctrl-wordmark-air">AIR</span>
      <span aria-hidden="true" className="airctrl-wordmark-ctrl">CTRL</span>
    </span>;
}

The endpoint pages describe one request at a time. This cookbook shows how those requests fit together.
It uses a human PAT because account discovery and writes are human-only. A Service Account can call only
the operations listed in [Feature availability](/reference/availability).

## Prepare a safe shell

Load values through your runtime or secret storage. Do not paste a real token into a shared terminal log.

```bash theme={}
export AIRCTRL_API_URL="https://api.airctrl.dev"
export AIRCTRL_TOKEN="<human-personal-access-token>"
export AIRCTRL_ACCOUNT_ID="00000000-0000-4000-8000-000000000001"
```

Every control-plane request uses the bearer header:

```bash theme={}
--header "Authorization: Bearer $AIRCTRL_TOKEN"
```

Requests inside one account also use:

```bash theme={}
--header "x-account-id: $AIRCTRL_ACCOUNT_ID"
```

The account header selects context. It cannot grant access that the PAT does not already have.

## Discover account and project context

List accounts before accepting an account ID from a user or configuration file:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/accounts" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN"
```

Then list active projects in the selected account:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/projects?includeArchived=false" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID"
```

Keep account and project IDs as separate values. A project ID never replaces `x-account-id`.

## Create a project without duplicate writes

Generate one UUID for the intended create operation and keep it for retries of that same operation.

```bash theme={}
export IDEMPOTENCY_KEY="00000000-0000-4000-8000-000000000101"

curl --fail-with-body --silent --show-error \
  --request POST \
  --url "$AIRCTRL_API_URL/v1/projects" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data "{\"accountId\":\"$AIRCTRL_ACCOUNT_ID\",\"name\":\"Payments\",\"description\":\"Credentials used by the payments service\"}"
```

A success response contains `data.project_id`. Retrying the same request with the same key returns the
same result. Reusing the key with a different name or description returns
`idempotency_key_reused_with_different_params`.

Project updates replace the editable values. Send every value you want to preserve:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --request PATCH \
  --url "$AIRCTRL_API_URL/v1/projects/$PROJECT_ID" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID" \
  --header "Content-Type: application/json" \
  --data '{"name":"Payments production","description":"Production credentials","isActive":true}'
```

## Read record metadata before plaintext

Start with metadata. This request does not decrypt record fields:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/records/by-project/$PROJECT_ID?includeArchived=false&limit=50&offset=0" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID"
```

Use the returned record ID only after checking its name, format, tags and project. To request selected
encrypted sections:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --request POST \
  --url "$AIRCTRL_API_URL/v1/records/$RECORD_ID/read" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{"scope":["meta","fields"],"environment":"production"}'
```

The REST result is still encrypted material. Use the SDK, CLI or local MCP server when you need plaintext;
those clients perform decryption locally. Do not send a vault passphrase to a REST endpoint.

## Change metadata without changing the value

Only supplied metadata fields change:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --request PATCH \
  --url "$AIRCTRL_API_URL/v1/records/$RECORD_ID" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "Content-Type: application/json" \
  --data '{"tags":["payments","production"],"secretDueAt":"2026-12-01T00:00:00Z","rotationIntervalDays":90}'
```

This does not rotate the data key or create a value version. For value changes, rotation, rollback and
sharing, prefer the SDK. Those workflows must unwrap or re-seal record keys in the trusted client.

## Review record history and audit

Read value-version metadata:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/records/$RECORD_ID/versions" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN"
```

Read the record's audit feed:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/records/$RECORD_ID/audit?limit=25" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN"
```

For a project-wide review, use `/v1/audit/record-anomalies`, `/record-activity`, `/record-alerts` and
`/record-access` with `scopeType=project`, `scopeId=$PROJECT_ID` and the account header. Audit responses
describe events and actors; they do not contain plaintext records.

## Create a Gateway control plane

List providers first, then create or select a provider credential. Provider secret values are write-only.
For safer shell handling, use the SDK, CLI `--api-key-env`, or MCP `apiKeyEnv` instead of placing a key in
raw cURL JSON.

Create a gateway after you have the provider and credential IDs:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --request POST \
  --url "$AIRCTRL_API_URL/v1/gateways" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID" \
  --header "Content-Type: application/json" \
  --data "{\"projectId\":\"$PROJECT_ID\",\"name\":\"Payments AI\",\"slug\":\"payments-ai\",\"defaultProviderId\":\"$PROVIDER_ID\",\"defaultModel\":\"gpt-5\",\"credentialIds\":[\"$PROVIDER_CREDENTIAL_ID\"]}"
```

The control-plane bearer configures <AirctrlWordmark />. Applications do not use it for model traffic; they use a
separate gateway token against the data-plane URL returned by the gateway configuration.

## Create and replace a gateway token safely

Create a short-lived token with only the scopes and models the workload needs:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --request POST \
  --url "$AIRCTRL_API_URL/v1/gateways/$GATEWAY_ID/tokens" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID" \
  --header "Content-Type: application/json" \
  --data '{"name":"CI replacement","scopes":["run"],"expiresInDays":30,"allowedModels":["gpt-5"],"rateLimitPerMin":60}'
```

The plaintext token appears once in `data.plaintext`. Store and deploy it before revoking the old token.
Then revoke the old token:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --request DELETE \
  --url "$AIRCTRL_API_URL/v1/gateways/$GATEWAY_ID/tokens/$OLD_TOKEN_ID/revoke" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID"
```

Revocation stops authentication but keeps metadata for review. Deleting the token row is a separate action.

## Diagnose a Gateway without reading bodies

Start with project metrics and gateway log metadata:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/usage/metrics?projectId=$PROJECT_ID&since=2026-09-01T00:00:00Z&bucket=day" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN"

curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/gateways/$GATEWAY_ID/logs?status=500&limit=25&offset=0" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN"
```

Use the key-access feed to distinguish provider-key denials from provider failures. Read a log detail only
when request or response bodies are necessary and the caller has `usage:read-bodies`. Never copy sensitive
bodies into general logs or support tickets.

## Configure OpenTelemetry without reading secrets

Read the safe export state, then update only the intended fields:

```bash theme={}
curl --fail-with-body --silent --show-error \
  --request PATCH \
  --url "$AIRCTRL_API_URL/v1/gateways/$GATEWAY_ID/otel-export" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID" \
  --header "Content-Type: application/json" \
  --data '{"enabled":true,"endpoint":"https://otel.example.com/v1/traces","exportMetrics":true,"exportTraces":true}'
```

Stored header values are write-only. A later GET confirms the endpoint and switches but never returns the
secret header values.

## Work with Service Accounts without exposing lifecycle

The public API can list safe Service Account metadata and let an authorized human grant an encrypted
record key. It cannot create, pause, provision, rotate, revoke or delete a Service Account credential.

```bash theme={}
curl --fail-with-body --silent --show-error \
  --url "$AIRCTRL_API_URL/v1/service-accounts?accountId=$AIRCTRL_ACCOUNT_ID" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN" \
  --header "x-account-id: $AIRCTRL_ACCOUNT_ID"
```

Do not hand-build `wrappedDek` for `/service-accounts/{id}/grants`. Use
`shareRecordWithServiceAccount()`, `airctrl record share-sa`, or `share_record_service_account`; each uses
the shared crypto implementation to prepare the correct wrapper locally.

## Handle failures without broadening access

Use `--fail-with-body` so cURL returns a non-zero status while preserving the <AirctrlWordmark /> error envelope.

1. Keep `requestId`.
2. Read `error.code` and `error.message`.
3. Fix `400` input errors locally.
4. Do not retry `403` with other IDs or wider scopes.
5. Refresh resource state after `409`.
6. Retry only `429`, `500` and `503`, with a short bounded backoff.

See the [API error matrix](/reference/api-errors) for failures associated with every public operation.
