> ## 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.

# REST client recipes

> Call the AIRCTRL API from JavaScript, TypeScript, Python, Go, Java, C#, Ruby, or PHP.

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>;
}

Use these recipes when you need <AirctrlWordmark /> from a language that does not use the official SDK. They all use
the same HTTPS API. Pick the language your application already uses.

The only official <AirctrlWordmark /> SDK is `@airctrl/sdk` for JavaScript and TypeScript. These recipes are direct
REST clients, not alternative SDKs.

## Shared request rules

Pass your API URL, personal access token, and account ID from your application's secure configuration. Do
not put them in source code, a checked-in file, or a log. Each request sends the token in `Authorization`
and selects its account with `x-account-id`.

Start with `listProjects`. It is read-only and confirms that the selected identity and account are correct.
It uses `limit` and `offset` so your application can continue with the next page when necessary.

The `createProject` examples show a write. Generate one idempotency key for one intended project. Reuse
the same key only when retrying the same request body.

For every endpoint, use the generated [API Reference](/api-reference) for its exact path, fields and
response schema. The [API cookbook](/reference/api-cookbook) explains larger workflows, and
[Errors and retries](/reference/errors) explains the error envelope and retry rules.

## JavaScript

Node.js includes `fetch`, so this recipe needs no HTTP package.

```js theme={}
async function request(url, { token, accountId, method = 'GET', body, idempotencyKey }) {
  const response = await fetch(url, {
    method,
    headers: {
      Authorization: `Bearer ${token}`,
      'x-account-id': accountId,
      ...(body ? { 'Content-Type': 'application/json' } : {}),
      ...(idempotencyKey ? { 'Idempotency-Key': idempotencyKey } : {}),
    },
    body: body ? JSON.stringify(body) : undefined,
  })

  const result = await response.json()
  if (!response.ok) throw Object.assign(new Error(result.error.message), { result, status: response.status })
  return result.data
}

export function listProjects(config) {
  const query = new URLSearchParams({ includeArchived: 'false', limit: '50', offset: '0' })
  return request(`${config.baseUrl}/v1/projects?${query}`, config)
}

export function createProject(config, project) {
  return request(`${config.baseUrl}/v1/projects`, {
    ...config,
    method: 'POST',
    body: project,
    idempotencyKey: crypto.randomUUID(),
  })
}
```

## TypeScript

TypeScript uses the same built-in HTTP client. These types make the required context explicit.

```ts theme={}
type AirctrlConfig = { baseUrl: string; token: string; accountId: string }
type ProjectInput = { accountId: string; name: string; description: string }

async function request<T>(url: string, config: AirctrlConfig, init: RequestInit = {}): Promise<T> {
  const response = await fetch(url, {
    ...init,
    headers: { Authorization: `Bearer ${config.token}`, 'x-account-id': config.accountId, ...init.headers },
  })
  const result = await response.json() as { ok: boolean; data: T; error?: { message: string } }
  if (!response.ok) throw new Error(result.error?.message ?? `AIRCTRL returned ${response.status}`)
  return result.data
}

export function listProjects(config: AirctrlConfig) {
  return request<unknown[]>(`${config.baseUrl}/v1/projects?includeArchived=false&limit=50&offset=0`, config)
}

export function createProject(config: AirctrlConfig, project: ProjectInput) {
  return request<unknown>(`${config.baseUrl}/v1/projects`, config, {
    method: 'POST',
    headers: { 'Content-Type': 'application/json', 'Idempotency-Key': crypto.randomUUID() },
    body: JSON.stringify(project),
  })
}
```

## Python

This example uses the Python standard library. No third-party HTTP package is required.

```python theme={}
import json
import uuid
from urllib.error import HTTPError
from urllib.request import Request, urlopen

def request(url, config, method="GET", body=None, idempotency_key=None):
    headers = {"Authorization": f"Bearer {config['token']}", "x-account-id": config["account_id"]}
    if body is not None:
        headers["Content-Type"] = "application/json"
    if idempotency_key:
        headers["Idempotency-Key"] = idempotency_key
    try:
        with urlopen(Request(url, data=json.dumps(body).encode() if body else None, headers=headers, method=method)) as response:
            return json.load(response)["data"]
    except HTTPError as error:
        detail = json.load(error)
        raise RuntimeError(detail["error"]["message"]) from error

def list_projects(config):
    return request(f"{config['base_url']}/v1/projects?includeArchived=false&limit=50&offset=0", config)

def create_project(config, project):
    return request(f"{config['base_url']}/v1/projects", config, "POST", project, str(uuid.uuid4()))
```

## Go

This example uses `net/http` from the Go standard library.

```go theme={}
func request(client *http.Client, url string, config Config, method string, body io.Reader, idempotencyKey string) (json.RawMessage, error) {
    req, err := http.NewRequest(method, url, body)
    if err != nil { return nil, err }
    req.Header.Set("Authorization", "Bearer "+config.Token)
    req.Header.Set("x-account-id", config.AccountID)
    if body != nil { req.Header.Set("Content-Type", "application/json") }
    if idempotencyKey != "" { req.Header.Set("Idempotency-Key", idempotencyKey) }
    response, err := client.Do(req)
    if err != nil { return nil, err }
    defer response.Body.Close()
    var result struct { Data json.RawMessage; Error *struct{ Message string } }
    if err := json.NewDecoder(response.Body).Decode(&result); err != nil { return nil, err }
    if response.StatusCode < 200 || response.StatusCode >= 300 { return nil, errors.New(result.Error.Message) }
    return result.Data, nil
}

func ListProjects(client *http.Client, config Config) (json.RawMessage, error) {
    return request(client, config.BaseURL+"/v1/projects?includeArchived=false&limit=50&offset=0", config, http.MethodGet, nil, "")
}
```

Use `uuid.NewString()` from a UUID package your application already uses when you add a create request, and
send that value in `Idempotency-Key` for retries of the same body.

## Java

Java 11 or later includes `java.net.http.HttpClient`.

```java theme={}
static HttpRequest.Builder request(String url, Config config) {
  return HttpRequest.newBuilder(URI.create(url))
      .header("Authorization", "Bearer " + config.token())
      .header("x-account-id", config.accountId());
}

static String listProjects(HttpClient client, Config config) throws IOException, InterruptedException {
  HttpResponse<String> response = client.send(
      request(config.baseUrl() + "/v1/projects?includeArchived=false&limit=50&offset=0", config)
          .GET().build(),
      HttpResponse.BodyHandlers.ofString());
  if (response.statusCode() < 200 || response.statusCode() >= 300) {
    throw new IOException("AIRCTRL returned " + response.statusCode());
  }
  return response.body();
}

static HttpRequest createProject(Config config, String body) {
  return request(config.baseUrl() + "/v1/projects", config)
      .header("Content-Type", "application/json")
      .header("Idempotency-Key", UUID.randomUUID().toString())
      .POST(HttpRequest.BodyPublishers.ofString(body)).build();
}
```

Parse the JSON response with the JSON library your application already uses. On a failed response, read
`error.code`, `error.message`, and `requestId` before deciding whether a retry is safe.

## C\#

Use `HttpClient` from the .NET runtime and keep one instance for your application lifetime.

```csharp theme={}
static async Task<JsonDocument> ListProjects(HttpClient client, Config config)
{
    using var request = new HttpRequestMessage(
        HttpMethod.Get, $"{config.BaseUrl}/v1/projects?includeArchived=false&limit=50&offset=0");
    request.Headers.Authorization = new("Bearer", config.Token);
    request.Headers.Add("x-account-id", config.AccountId);
    using var response = await client.SendAsync(request);
    var result = JsonDocument.Parse(await response.Content.ReadAsStringAsync());
    if (!response.IsSuccessStatusCode) throw new HttpRequestException(result.RootElement.ToString());
    return result;
}

static HttpRequestMessage CreateProject(Config config, string json)
{
    var request = new HttpRequestMessage(HttpMethod.Post, $"{config.BaseUrl}/v1/projects");
    request.Headers.Authorization = new("Bearer", config.Token);
    request.Headers.Add("x-account-id", config.AccountId);
    request.Headers.Add("Idempotency-Key", Guid.NewGuid().ToString());
    request.Content = new StringContent(json, Encoding.UTF8, "application/json");
    return request;
}
```

Keep the same `Idempotency-Key` when your code retries `CreateProject` with the unchanged JSON body.

## Ruby

Ruby includes `Net::HTTP`, so this recipe has no gem dependency.

```ruby theme={}
require "json"
require "net/http"
require "securerandom"

def request(uri, config, request_class = Net::HTTP::Get, body: nil, idempotency_key: nil)
  request = request_class.new(uri)
  request["Authorization"] = "Bearer #{config[:token]}"
  request["x-account-id"] = config[:account_id]
  request["Content-Type"] = "application/json" if body
  request["Idempotency-Key"] = idempotency_key if idempotency_key
  request.body = JSON.generate(body) if body
  response = Net::HTTP.start(uri.hostname, uri.port, use_ssl: uri.scheme == "https") { |http| http.request(request) }
  result = JSON.parse(response.body)
  raise result.dig("error", "message") unless response.is_a?(Net::HTTPSuccess)
  result.fetch("data")
end

def list_projects(config)
  request(URI("#{config[:base_url]}/v1/projects?includeArchived=false&limit=50&offset=0"), config)
end

def create_project(config, project)
  request(URI("#{config[:base_url]}/v1/projects"), config, Net::HTTP::Post,
          body: project, idempotency_key: SecureRandom.uuid)
end
```

## PHP

This example uses the cURL extension included with common PHP installations.

```php theme={}
function request(string $url, array $config, string $method = 'GET', ?array $body = null, ?string $idempotencyKey = null): array {
    $headers = ["Authorization: Bearer {$config['token']}", "x-account-id: {$config['account_id']}"];
    if ($body !== null) $headers[] = 'Content-Type: application/json';
    if ($idempotencyKey !== null) $headers[] = "Idempotency-Key: $idempotencyKey";
    $handle = curl_init($url);
    curl_setopt_array($handle, [CURLOPT_CUSTOMREQUEST => $method, CURLOPT_HTTPHEADER => $headers,
        CURLOPT_RETURNTRANSFER => true, CURLOPT_POSTFIELDS => $body ? json_encode($body) : null]);
    $raw = curl_exec($handle);
    $status = curl_getinfo($handle, CURLINFO_RESPONSE_CODE);
    curl_close($handle);
    $result = json_decode($raw, true, flags: JSON_THROW_ON_ERROR);
    if ($status < 200 || $status >= 300) throw new RuntimeException($result['error']['message']);
    return $result['data'];
}

function listProjects(array $config): array {
    return request("{$config['base_url']}/v1/projects?includeArchived=false&limit=50&offset=0", $config);
}

function createProject(array $config, array $project): array {
    return request("{$config['base_url']}/v1/projects", $config, 'POST', $project, bin2hex(random_bytes(16)));
}
```

Encode an idempotency key as a UUID or another printable request ID before sending it in an HTTP header.

## Handle responses safely

All examples read the `data` property after a successful response. On a failure, preserve `requestId` for
support and make a decision from the HTTP status plus `error.code` and `error.message`.

* Fix `400`, `403`, and `404` before making another request.
* Refresh state after `409`.
* Retry only `429`, `500`, or `503` with bounded exponential backoff and jitter.
* For a retried write, preserve the original idempotency key and exactly the same request body.

Never log the token, plaintext record fields, or one-time credential values. For record decryption and
encrypted writes, use the [SDK](/sdk/overview), [CLI](/cli/overview), or [MCP](/mcp/setup) on a trusted
machine instead of reimplementing <AirctrlWordmark /> cryptography in a REST client.
