> ## 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 usage patterns

> Use AIRCTRL API requests safely beyond the happy path.

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 generated API Reference shows the exact path, fields and response schema for every operation. This page explains the rules that apply across operations and shows how to handle failures safely.

For complete task sequences, continue to the [API cookbook](/reference/api-cookbook).

## Choose the correct identity

A human PAT starts with `sk-actrl-pat-`. It can call all public operations when RBAC permits them. A Service Account token starts with `sk-actrl-sa-` and can call only the operations listed in [Feature availability](/reference/availability).

```bash theme={}
curl --request GET \
  --url https://api.airctrl.dev/v1/accounts \
  --header "Authorization: Bearer $AIRCTRL_PAT"
```

Do not send both identity types or place a token in a query string.

## Send account context

Operations that work inside one account accept `x-account-id`. Some create requests also carry `accountId` in the body. The account ID selects context; it never grants membership or permission.

```bash theme={}
curl --request GET \
  --url "https://api.airctrl.dev/v1/projects?accountId=$ACCOUNT_ID&includeArchived=false" \
  --header "Authorization: Bearer $AIRCTRL_PAT" \
  --header "x-account-id: $ACCOUNT_ID"
```

If an ID belongs to another account, <AirctrlWordmark /> denies the request even when the caller can access both accounts separately.

## Read lists in bounded pages

List operations that expose `limit` and `offset` start at offset `0`. Keep pages small enough for your process to handle.

```bash theme={}
curl --request GET \
  --url "https://api.airctrl.dev/v1/records?accountId=$ACCOUNT_ID&limit=50&offset=0&includeArchived=false" \
  --header "Authorization: Bearer $AIRCTRL_PAT" \
  --header "x-account-id: $ACCOUNT_ID"
```

An empty `data` result means no visible resources matched. It does not mean <AirctrlWordmark /> expanded the query to another scope.

## Distinguish metadata from plaintext

Record endpoints return encrypted material and safe metadata. The API never returns decrypted secret fields. Use `@airctrl/sdk`, the CLI or MCP on a trusted machine when you need plaintext.

```bash theme={}
curl --request GET \
  --url "https://api.airctrl.dev/v1/records/$RECORD_ID" \
  --header "Authorization: Bearer $AIRCTRL_TOKEN"
```

The response can contain `ciphertext` and wrapper metadata. Do not treat either as plaintext.

## Use idempotency keys for writes

Create one UUID for one intended write. Reuse that key only when retrying the same body.

```bash theme={}
IDEMPOTENCY_KEY="$(uuidgen)"

curl --request POST \
  --url https://api.airctrl.dev/v1/projects \
  --header "Authorization: Bearer $AIRCTRL_PAT" \
  --header "Content-Type: application/json" \
  --header "Idempotency-Key: $IDEMPOTENCY_KEY" \
  --data "{\"accountId\":\"$ACCOUNT_ID\",\"name\":\"Payments\",\"description\":\"Payment service credentials\"}"
```

A retry with the same key and body reuses the result. The same key with different input returns `409` and `idempotency_key_reused_with_different_params`.

## Update only with current state

Some update operations replace their complete editable input; others patch only supplied fields. Read the operation description before sending the request.

```bash theme={}
curl --request PATCH \
  --url "https://api.airctrl.dev/v1/projects/$PROJECT_ID" \
  --header "Authorization: Bearer $AIRCTRL_PAT" \
  --header "x-account-id: $ACCOUNT_ID" \
  --header "Content-Type: application/json" \
  --data '{"name":"Payments production","description":"Production credentials","isActive":true}'
```

For a complete replacement contract such as this one, omitting a value is not the same as preserving it.

## Treat destructive operations as two steps

First read the resource and display its name and ID. Execute the destructive request only after a human confirms that target.

```bash theme={}
curl --request DELETE \
  --url "https://api.airctrl.dev/v1/gateways/$GATEWAY_ID" \
  --header "Authorization: Bearer $AIRCTRL_PAT" \
  --header "x-account-id: $ACCOUNT_ID"
```

Deleting a gateway disables it and revokes its gateway tokens. It does not revoke shared provider credentials.

## Read the error envelope

```json theme={}
{
  "ok": false,
  "error": {
    "code": "forbidden",
    "message": "insufficient_permission"
  },
  "requestId": "req_example"
}
```

Use `error.code` for the HTTP category and `error.message` for the specific reason. Store `requestId` with diagnostics, but never store bearer tokens or plaintext fields.

## Retry only temporary failures

Retry `429`, `500` and `503` with exponential backoff, jitter and a maximum attempt count. Do not retry `400`, `403` or `404` unchanged.

```ts theme={}
const retryable = new Set([429, 500, 503])
const maximumAttempts = 4

for (let attempt = 1; attempt <= maximumAttempts; attempt += 1) {
  const response = await fetch(url, request)
  if (response.ok) break
  if (!retryable.has(response.status) || attempt === maximumAttempts) {
    throw new Error(`AIRCTRL request failed with ${response.status}`)
  }
  await new Promise((resolve) => setTimeout(resolve, 250 * 2 ** (attempt - 1) + Math.random() * 100))
}
```

For a retried write, keep the same idempotency key and exact request body.

## One-time values

Gateway token creation returns plaintext once. Provider credential creation accepts plaintext once. Send those values only between the trusted caller and the approved custody destination.

```bash theme={}
curl --request POST \
  --url "https://api.airctrl.dev/v1/gateways/$GATEWAY_ID/tokens" \
  --header "Authorization: Bearer $AIRCTRL_PAT" \
  --header "x-account-id: $ACCOUNT_ID" \
  --header "Content-Type: application/json" \
  --data '{"name":"Local development","scopes":["run","read_logs"],"expiresInDays":30}'
```

Later list requests return only token metadata and prefix.

## Validate before automation

Before placing a request in a job:

1. Run it once with a test project and least-privilege identity.
2. Confirm the success envelope and expected audit event.
3. Exercise one validation failure and one permission failure.
4. Confirm retry behavior with a fixed idempotency key.
5. Remove test resources through the documented lifecycle operation.
