# Quickstart

Ask one question, wait for the answer, and read the provisions it cites. The samples below do exactly that.

> **Note:** Sandbox keys come from the developer console, which opens with the API launch.

## 1. Get a key

Get a free sandbox key at [https://console.azta.ai](https://console.azta.ai). A sandbox key costs nothing and never reaches the engine.

Put it in your environment. The samples read it from `AZTA_API_KEY` and never contain a key:

```bash
export AZTA_API_KEY=your-key-here
```

To point the samples at another base URL, set `AZTA_BASE_URL`. It defaults to `https://api.azta.ai/v1`.

## 2. Run it

The curl sample needs `curl` and `jq`. The Python sample needs Python 3.12 and `pip install httpx`. The TypeScript sample needs Node 24 or later, which runs it as is.

```bash tab="curl"
#!/usr/bin/env bash
# Ask Azta one question, wait for the answer, print it with its sources.
# Needs curl, jq and uuidgen (or a kernel with /proc/sys/kernel/random/uuid).
set -euo pipefail

: "${AZTA_API_KEY:?Set AZTA_API_KEY to your Azta API key}"
BASE_URL="${AZTA_BASE_URL:-https://api.azta.ai/v1}"
QUESTION='Mikor alkalmazandó a fordított adózás építőipari szolgáltatásnál?'

headers="$(mktemp)"
body="$(mktemp)"
trap 'rm -f "$headers" "$body"' EXIT

# One key per question. A retry that reuses it is the same request, so it is
# charged once. A retry without it would be a new request, charged again.
IDEMPOTENCY_KEY="$(cat /proc/sys/kernel/random/uuid 2>/dev/null || uuidgen)"
IDEMPOTENCY_KEY="$(printf '%s' "$IDEMPOTENCY_KEY" | tr 'A-Z' 'a-z')"

# call METHOD URL [JSON_BODY] sets $status and leaves the reply in $body.
call() {
  local args=(-sS -X "$1" "$2" -H "Authorization: Bearer $AZTA_API_KEY"
    -D "$headers" -o "$body" -w '%{http_code}')
  if [ -n "${3:-}" ]; then
    args+=(-H 'Content-Type: application/json'
      -H "Idempotency-Key: $IDEMPOTENCY_KEY" -d "$3")
  fi
  status="$(curl "${args[@]}")"
}

# retry_after DEFAULT prints the Retry-After seconds, else DEFAULT.
retry_after() {
  local value
  value="$(awk -F': *' 'tolower($1) == "retry-after" { gsub(/\r/, "", $2); print $2 }' "$headers" | tail -n 1)"
  if [[ "$value" =~ ^[0-9]+$ ]]; then
    echo "$value"
    return
  fi
  echo "$1"
}

fail() {
  echo "Error: $(jq -r '.error.code // "unknown"' "$body")" >&2
  exit 1
}

payload="$(jq -n --arg input "$QUESTION" \
  '{model: "azta-agent", input: $input, background: true, depth: "standard"}')"

# Create the response: retry 429, 500 and 503 up to 3 attempts, same key.
attempt=1
while true; do
  call POST "$BASE_URL/responses" "$payload"
  if [ "$status" = 200 ] || [ "$status" = 202 ]; then
    break
  fi
  case "$status" in
    429 | 500 | 503)
      if [ "$attempt" -ge 3 ]; then
        fail
      fi
      sleep "$(retry_after 1)"
      attempt=$((attempt + 1))
      ;;
    *) fail ;;
  esac
done
id="$(jq -r '.id' "$body")"

# Poll every 5 seconds until the response ends; Retry-After wins on 429 and 503.
while true; do
  call GET "$BASE_URL/responses/$id"
  case "$status" in
    429 | 503)
      sleep "$(retry_after 5)"
      continue
      ;;
    200) ;;
    *) fail ;;
  esac
  state="$(jq -r '.status' "$body")"
  case "$state" in
    completed) break ;;
    failed | cancelled)
      echo "Error: $(jq -r '.error.code // .status' "$body")" >&2
      exit 1
      ;;
  esac
  sleep 5
done

jq -r '[.output[].content[] | select(.type == "output_text") | .text] | join("")' "$body"
echo
echo 'Sources:'
jq -r '.sources[] | "[\(.number)] \(.title) \(.url)"' "$body"
```

```python tab="Python"
"""Ask Azta one question, wait for the answer, print it with its sources."""

import os
import sys
import time
import uuid

import httpx

API_KEY = os.environ.get("AZTA_API_KEY")
if not API_KEY:
    sys.exit("Set AZTA_API_KEY to your Azta API key")
BASE_URL = os.environ.get("AZTA_BASE_URL", "https://api.azta.ai/v1")
QUESTION = "Mikor alkalmazandó a fordított adózás építőipari szolgáltatásnál?"

# One key per question. A retry that reuses it is the same request, so it is
# charged once. A retry without it would be a new request, charged again.
IDEMPOTENCY_KEY = str(uuid.uuid4())


def retry_after(response: httpx.Response, default: float) -> float:
    value = response.headers.get("retry-after", "")
    return float(value) if value.isdigit() else default


def fail(response: httpx.Response) -> None:
    try:
        code = response.json()["error"]["code"]
    except (ValueError, KeyError, TypeError):
        code = f"http_{response.status_code}"
    sys.exit(f"Error: {code}")


def create(client: httpx.Client) -> str:
    payload = {
        "model": "azta-agent",
        "input": QUESTION,
        "background": True,
        "depth": "standard",
    }
    for attempt in range(1, 4):
        response = client.post(
            f"{BASE_URL}/responses",
            json=payload,
            headers={"Idempotency-Key": IDEMPOTENCY_KEY},
        )
        if response.status_code in (200, 202):
            return response.json()["id"]
        if response.status_code not in (429, 500, 503) or attempt == 3:
            fail(response)
        time.sleep(retry_after(response, 1))
    raise AssertionError("unreachable")


def wait(client: httpx.Client, response_id: str) -> dict:
    while True:
        response = client.get(f"{BASE_URL}/responses/{response_id}")
        if response.status_code in (429, 503):
            time.sleep(retry_after(response, 5))
            continue
        if response.status_code != 200:
            fail(response)
        data = response.json()
        if data["status"] == "completed":
            return data
        if data["status"] in ("failed", "cancelled"):
            code = (data.get("error") or {}).get("code", data["status"])
            sys.exit(f"Error: {code}")
        time.sleep(5)


with httpx.Client(headers={"Authorization": f"Bearer {API_KEY}"}) as client:
    result = wait(client, create(client))

print(
    "".join(
        part["text"]
        for message in result["output"]
        for part in message["content"]
        if part["type"] == "output_text"
    )
)
print()
print("Sources:")
for source in result["sources"]:
    print(f"[{source['number']}] {source['title']} {source['url']}")
```

```ts tab="TypeScript"
// Ask Azta one question, wait for the answer, print it with its sources.
// Run with Node 24 or later: node quickstart.ts
import { randomUUID } from 'node:crypto';
import { setTimeout as sleep } from 'node:timers/promises';

const apiKey = process.env.AZTA_API_KEY;
if (!apiKey) {
  console.error('Set AZTA_API_KEY to your Azta API key');
  process.exit(1);
}
const baseUrl = process.env.AZTA_BASE_URL ?? 'https://api.azta.ai/v1';
const question =
  'Mikor alkalmazandó a fordított adózás építőipari szolgáltatásnál?';

// One key per question. A retry that reuses it is the same request, so it is
// charged once. A retry without it would be a new request, charged again.
const idempotencyKey = randomUUID();

type Source = { number: number; title: string; url: string };
type AztaResponse = {
  id: string;
  status: string;
  error: { code: string } | null;
  output: { content: { type: string; text: string }[] }[];
  sources: Source[];
};

const retryAfter = (res: Response, fallback: number): number => {
  const value = res.headers.get('retry-after') ?? '';
  return /^\d+$/.test(value) ? Number(value) : fallback;
};

const fail = async (res: Response): Promise<never> => {
  const body = (await res.json().catch(() => null)) as {
    error?: { code?: string };
  } | null;
  console.error(`Error: ${body?.error?.code ?? `http_${res.status}`}`);
  return process.exit(1);
};

const create = async (): Promise<string> => {
  const payload = JSON.stringify({
    model: 'azta-agent',
    input: question,
    background: true,
    depth: 'standard',
  });
  for (let attempt = 1; ; attempt += 1) {
    const res = await fetch(`${baseUrl}/responses`, {
      method: 'POST',
      headers: {
        Authorization: `Bearer ${apiKey}`,
        'Content-Type': 'application/json',
        'Idempotency-Key': idempotencyKey,
      },
      body: payload,
    });
    if (res.status === 200 || res.status === 202) {
      return ((await res.json()) as AztaResponse).id;
    }
    if (![429, 500, 503].includes(res.status) || attempt === 3) {
      return fail(res);
    }
    await sleep(retryAfter(res, 1) * 1000);
  }
};

const wait = async (id: string): Promise<AztaResponse> => {
  for (;;) {
    const res = await fetch(`${baseUrl}/responses/${id}`, {
      headers: { Authorization: `Bearer ${apiKey}` },
    });
    if (res.status === 429 || res.status === 503) {
      await sleep(retryAfter(res, 5) * 1000);
      continue;
    }
    if (res.status !== 200) {
      return fail(res);
    }
    const data = (await res.json()) as AztaResponse;
    if (data.status === 'completed') {
      return data;
    }
    if (data.status === 'failed' || data.status === 'cancelled') {
      console.error(`Error: ${data.error?.code ?? data.status}`);
      process.exit(1);
    }
    await sleep(5000);
  }
};

const result = await wait(await create());
console.log(
  result.output
    .flatMap((message) => message.content)
    .filter((part) => part.type === 'output_text')
    .map((part) => part.text)
    .join(''),
);
console.log();
console.log('Sources:');
for (const source of result.sources) {
  console.log(`[${source.number}] ${source.title} ${source.url}`);
}
```

Run `bash quickstart.sh`, `python quickstart.py` or `node quickstart.ts`.

## 3. Read the sources

The output is the answer text, a blank line, `Sources:`, then one line per source:

```text
Sources:
[1] <title> <url>
```

The number in `[n]` is the `source_number` on the answer's citations, so every answer links to the provisions it cites. See [Sources](/guides/sources) for the full record.

## What the samples do

Each sample sends the question with `background: true`, which answers `202` with the response in `queued`. It then polls `GET /v1/responses/{id}` every 5 seconds until the status is `completed`, `failed` or `cancelled`. On `failed` or `cancelled` it exits non-zero with the error code. See [Responses](/guides/responses).

### Retries and the Idempotency-Key

Every sample makes one `Idempotency-Key` per question and sends it on the create request. A retry without `Idempotency-Key` is a new request and is charged again.

If the create fails with `429`, `500` or `503`, the sample waits the `Retry-After` seconds (1 second when the header is missing) and sends the request again with the same `Idempotency-Key`, at most 3 attempts. The key makes the retry the same request, so it is charged once. The samples never retry a `400`, `401`, `402` or `403`, because a repeat would fail the same way.

While polling, a `429` or `503` waits `Retry-After` before the next poll.

See [Errors](/guides/errors) for every code.
