# Use the OpenAI SDK

The Azta API speaks OpenAI's Responses format, so the official OpenAI SDKs work with a different base URL and key. Two things differ from a plain OpenAI call: `depth` is an Azta field the SDK does not know, and the SDK's automatic retries need an `Idempotency-Key` that you set yourself.

## Install

```bash tab="Python"
pip install openai==3.37.0
```

```bash tab="Node"
npm install --save-exact openai@7.43.0
```

Python 3.12 and Node 24 are the tested versions.

## Point the client at Azta

The base URL is `https://api.azta.ai/v1` and the key is your Azta API key. Read both from the environment, never from source code.

The full examples below build the client this way: `OpenAI(base_url=…, api_key=…)` in Python and `new OpenAI({ baseURL, apiKey })` in Node.

## Create, retrieve and cancel

Create with `background: true`, pass `depth` through the SDK's escape hatch for extra fields (`extra_body` in Python, a widened body type in Node), and poll with `responses.retrieve`. `POST /v1/responses/{id}/cancel` stops a run; call it with `client.responses.cancel`.

```python tab="Python"
"""Ask Azta one question through the OpenAI Python SDK and print the sources."""

import os
import sys
import time
import uuid

from openai import OpenAI

api_key = os.environ.get("AZTA_API_KEY")
if not api_key:
    sys.exit("Set AZTA_API_KEY to your Azta API key")

client = OpenAI(
    base_url=os.environ.get("AZTA_BASE_URL", "https://api.azta.ai/v1"),
    api_key=api_key,
)
QUESTION = "Mikor alkalmazandó a fordított adózás építőipari szolgáltatásnál?"

# One key per question. The SDK retries a failed create on its own and sends
# no idempotency header of its own, but it re-sends this per-call header, so
# every retry is the same request and is charged once.
idempotency_key = str(uuid.uuid4())

response = client.responses.create(
    model="azta-agent",
    input=QUESTION,
    background=True,
    extra_body={"depth": "standard"},
    extra_headers={"Idempotency-Key": idempotency_key},
)

while response.status in ("queued", "in_progress"):
    time.sleep(5)
    response = client.responses.retrieve(response.id)

if response.status != "completed":
    code = response.error.code if response.error else response.status
    sys.exit(f"Error: {code}")

print(response.output_text)
print()
print("Sources:")
for source in (response.model_extra or {}).get("sources", []):
    print(f"[{source['number']}] {source['title']} {source['url']}")
```

```ts tab="Node"
// Ask Azta one question through the OpenAI Node SDK and print the sources.
// Run with Node 24 or later: node main.ts
import { randomUUID } from 'node:crypto';
import { setTimeout as sleep } from 'node:timers/promises';
import OpenAI from 'openai';
import type { ResponseCreateParamsNonStreaming } from 'openai/resources/responses/responses';

const apiKey = process.env.AZTA_API_KEY;
if (!apiKey) {
  console.error('Set AZTA_API_KEY to your Azta API key');
  process.exit(1);
}

const client = new OpenAI({
  baseURL: process.env.AZTA_BASE_URL ?? 'https://api.azta.ai/v1',
  apiKey,
});
const question =
  'Mikor alkalmazandó a fordított adózás építőipari szolgáltatásnál?';

type SourceRecord = { number: number; title: string; url: string };
type AztaCreateParams = ResponseCreateParamsNonStreaming & {
  depth: 'standard' | 'deep';
};

// One key per question. The SDK retries a failed create on its own and sends
// no idempotency header of its own, but it re-sends this per-call header, so
// every retry is the same request and is charged once.
const idempotencyKey = randomUUID();

const body: AztaCreateParams = {
  model: 'azta-agent',
  input: question,
  background: true,
  depth: 'standard',
};
let response = await client.responses.create(body, {
  headers: { 'Idempotency-Key': idempotencyKey },
});

while (response.status === 'queued' || response.status === 'in_progress') {
  await sleep(5000);
  response = await client.responses.retrieve(response.id);
}

if (response.status !== 'completed') {
  console.error(`Error: ${response.error?.code ?? response.status}`);
  process.exit(1);
}

console.log(response.output_text);
console.log();
console.log('Sources:');
const { sources } = response as unknown as { sources: SourceRecord[] };
for (const source of sources) {
  console.log(`[${source.number}] ${source.title} ${source.url}`);
}
```

## Streaming

Pass `stream=True` (`stream: true` in Node) and iterate the events. `response.output_text.delta` events carry the text as it is written; `response.output_text.done` carries the whole text and is the authoritative one, so replace what you built from deltas with it. Annotations and sources arrive only in the closing frames. See [Responses](/guides/responses) for the stream's deadline and what dropping the connection does.

## Retries and the Idempotency-Key

The OpenAI SDKs retry a failed request on their own: a connection error, a `408`, `409`, `429` or any `5xx`, up to two more times. They send no idempotency header on those retries, so on their own a retried create could run, and be charged, twice.

Set the key per call, once per question, as `extra_headers={"Idempotency-Key": key}` in Python and `{ headers: { 'Idempotency-Key': key } }` in Node, exactly as the examples above do. The SDK re-sends a per-call header on every automatic retry, so all attempts carry the same key and Azta treats them as one request.

This site's CI proves it: a test makes the first create fail with a `500`, lets the SDK retry, and checks that both requests carry the same `Idempotency-Key`. Create a new key for each new question, and reuse it only when you retry that same question yourself. See [Responses](/guides/responses) for how the key is scoped.

## Read the sources

`sources[]` is an Azta field, so the SDK keeps it as an extra: `response.model_extra["sources"]` in Python, and a cast in Node. Each annotation's `source_number` points at the source with the same `number`. [Sources](/guides/sources) describes the record and how to render citations from the annotation offsets.

## Verify webhooks

The OpenAI SDKs verify Standard Webhooks signatures, which is the scheme Azta uses. Pass the raw request body, the headers and the endpoint secret (`whsec_…`). Both samples refuse to run without `AZTA_WEBHOOK_SECRET`:

```python tab="Python"
"""Verify and parse an Azta webhook with the OpenAI Python SDK."""

import os

from openai import OpenAI


def unwrap_event(payload: str, headers: dict[str, str]):
    """Return the event, or raise if the signature or timestamp is wrong.

    Pass the raw request body, exactly as received.
    """
    secret = os.environ.get("AZTA_WEBHOOK_SECRET")
    if not secret:
        raise RuntimeError("Set AZTA_WEBHOOK_SECRET to your endpoint's whsec_ secret")
    client = OpenAI(api_key=os.environ.get("AZTA_API_KEY", "unused"))
    return client.webhooks.unwrap(payload, headers, secret=secret)
```

```ts tab="Node"
// Verify and parse an Azta webhook with the OpenAI Node SDK.
import OpenAI from 'openai';

/**
 * Return the event, or throw if the signature or timestamp is wrong.
 * Pass the raw request body, exactly as received.
 */
export async function unwrapEvent(
  payload: string,
  headers: Record<string, string>,
) {
  const secret = process.env.AZTA_WEBHOOK_SECRET;
  if (!secret) {
    throw new Error("Set AZTA_WEBHOOK_SECRET to your endpoint's whsec_ secret");
  }
  const client = new OpenAI({ apiKey: process.env.AZTA_API_KEY ?? 'unused' });
  return client.webhooks.unwrap(payload, headers, secret);
}
```

`unwrap` throws if the signature or timestamp is wrong. The event is thin, so fetch the answer with `responses.retrieve(event.data.id)`. [Webhooks](/guides/webhooks) has the payload, the retry schedule, and a verifier that does not need the SDK (`examples/webhooks/`).

## What is not supported

- `previous_response_id` and `conversation` are refused with `400` `unsupported_parameter`, because the API keeps no conversation. Send the whole conversation as `input`, as described in [Responses](/guides/responses).
- `tools` are not supported. Azta looks up the provisions it cites itself.
