# Introduction Azta answers legislation questions (tax, accounting, legal, employment and company law), and every answer links to the provisions it cites. These docs show how to reach Azta from your AI tools and from your own code. ## Start here - [Use Azta in ChatGPT, Claude and coding agents](/mcp): connect the MCP server once, then ask from the tools you already use. - [Build with the API](/quickstart): send a question from your own product and render the answer with its sources. - [Build with a coding agent](/agents): give Claude Code, Codex, Cursor or Copilot what it needs to write the integration. ## For AI agents - [llms.txt](https://docs.azta.ai/llms.txt): an index of every page. - [llms-full.txt](https://docs.azta.ai/llms-full.txt): every page in one file. - Every page as Markdown: append `.md` to its URL, or send `Accept: text/markdown`. - MCP server: `https://mcp.azta.ai/mcp` - [Azta API skill](https://docs.azta.ai/skills/azta-api): teaches a coding agent the API. --- # Build with a coding agent A coding agent writes a correct Azta integration when it has two things: the Azta API skill and one prompt. Both work the same in every agent. ## Step 1: install the skill The [azta-api skill](/skills/azta-api) gives your agent the API contract, the `Idempotency-Key` rule, source rendering, errors and webhook verification. Install it with the command for your agent from its page. ## Step 2: paste the prompt Open your project, start the agent and paste the prompt from your agent's page. It asks the agent to read `https://docs.azta.ai/llms.txt`, make a first sandbox call, render sources, verify webhooks and write tests. ## Pick your agent - [Claude Code](/agents/claude-code) - [Cursor](/agents/cursor) - [Codex](/agents/codex) - [GitHub Copilot](/agents/copilot) > **Note:** Start with a sandbox key. See [Keys and the sandbox](/guides/keys-and-sandbox). If you would rather write the code yourself, begin with the [Quickstart](/quickstart). --- # Claude Code ## Install the skill ```bash npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent claude-code ``` Add `-g` to install for every project. Other ways to install are on [Azta API skill](/skills/azta-api). ## Paste the prompt Run `claude` in your project and paste the prompt below as your first message. ## The prompt ```text Integrate the Azta API into this codebase. Azta answers legal and regulatory questions, and every answer links to the provisions it cites. Start here: 1. Read https://docs.azta.ai/llms.txt for the docs index. If the azta-api skill is installed, use it instead and follow its references. 2. Use the HTTP client this codebase already has, or the OpenAI SDK pointed at the Azta base URL. Do not add another SDK. Build this: 1. Configuration. Read the API key from the AZTA_API_KEY environment variable and the base URL from AZTA_BASE_URL, defaulting to https://api.azta.ai/v1. Never write a key into source. Add both to .env.example, with a sandbox placeholder for the key, and tell me to create a sandbox key in the console. 2. First call. Send a question to POST /responses with background set to true. Generate one Idempotency-Key per question and send it on the create request. On 429, 500 and 503, wait for the Retry-After seconds (1 second when missing) and retry with the same Idempotency-Key, at most 3 attempts. Do not retry 400, 401, 402 or 403. Poll GET /responses/{id} every 5 seconds until the status is completed, failed or cancelled, and honour Retry-After on a 429 or 503 while polling. 3. Sources. Render output_text and place a [n] marker from each annotations entry, using the offsets the API returns. Offsets count Unicode code points, not UTF-16 units: in JavaScript slice with Array.from(text), never string indexes. Under the text, list the sources with their titles and URLs. If annotations is empty, render the text without markers. 4. Webhook. Add a webhook endpoint that receives response events. Verify the Standard Webhooks signature on the raw request body with AZTA_WEBHOOK_SECRET before parsing it, reject a bad signature, or a webhook-timestamp more than 300 seconds from now, with 400, skip a webhook-id you already handled, and answer 2xx fast. Add AZTA_WEBHOOK_SECRET to .env.example. 5. Errors. Branch on error.code, never on the message text. Show the user a short message of your own and log the response request id. 6. Tests. Write a test for the source rendering: markers land at the right offsets, the sources list shows each URL, and an empty annotations array renders plain text. Add a test for the retry path that proves the Idempotency-Key is reused. Run the sandbox call once to check it works, show me the output, and list the files you changed. ``` When the agent is done, check the first call in the sandbox, read the source rendering test, and see [Sources](/guides/sources) and [Webhooks](/guides/webhooks) for what to expect. --- # Codex ## Install the skill ```bash npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent codex ``` Add `-g` to install for every project. Other ways to install are on [Azta API skill](/skills/azta-api). ## Paste the prompt Run `codex` in your project and paste the prompt below as your first message. ## The prompt ```text Integrate the Azta API into this codebase. Azta answers legal and regulatory questions, and every answer links to the provisions it cites. Start here: 1. Read https://docs.azta.ai/llms.txt for the docs index. If the azta-api skill is installed, use it instead and follow its references. 2. Use the HTTP client this codebase already has, or the OpenAI SDK pointed at the Azta base URL. Do not add another SDK. Build this: 1. Configuration. Read the API key from the AZTA_API_KEY environment variable and the base URL from AZTA_BASE_URL, defaulting to https://api.azta.ai/v1. Never write a key into source. Add both to .env.example, with a sandbox placeholder for the key, and tell me to create a sandbox key in the console. 2. First call. Send a question to POST /responses with background set to true. Generate one Idempotency-Key per question and send it on the create request. On 429, 500 and 503, wait for the Retry-After seconds (1 second when missing) and retry with the same Idempotency-Key, at most 3 attempts. Do not retry 400, 401, 402 or 403. Poll GET /responses/{id} every 5 seconds until the status is completed, failed or cancelled, and honour Retry-After on a 429 or 503 while polling. 3. Sources. Render output_text and place a [n] marker from each annotations entry, using the offsets the API returns. Offsets count Unicode code points, not UTF-16 units: in JavaScript slice with Array.from(text), never string indexes. Under the text, list the sources with their titles and URLs. If annotations is empty, render the text without markers. 4. Webhook. Add a webhook endpoint that receives response events. Verify the Standard Webhooks signature on the raw request body with AZTA_WEBHOOK_SECRET before parsing it, reject a bad signature, or a webhook-timestamp more than 300 seconds from now, with 400, skip a webhook-id you already handled, and answer 2xx fast. Add AZTA_WEBHOOK_SECRET to .env.example. 5. Errors. Branch on error.code, never on the message text. Show the user a short message of your own and log the response request id. 6. Tests. Write a test for the source rendering: markers land at the right offsets, the sources list shows each URL, and an empty annotations array renders plain text. Add a test for the retry path that proves the Idempotency-Key is reused. Run the sandbox call once to check it works, show me the output, and list the files you changed. ``` When the agent is done, check the first call in the sandbox, read the source rendering test, and see [Sources](/guides/sources) and [Webhooks](/guides/webhooks) for what to expect. --- # GitHub Copilot ## Install the skill ```bash npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent github-copilot ``` Add `-g` to install for every project. Other ways to install are on [Azta API skill](/skills/azta-api). ## Paste the prompt **Agent mode in VS Code.** Open Copilot Chat, switch to agent mode and paste the prompt below. **Copilot coding agent.** Create an issue in your repository, paste the prompt below as the issue body and assign the issue to Copilot. It works in the background and opens a pull request for you to review. ## The prompt ```text Integrate the Azta API into this codebase. Azta answers legal and regulatory questions, and every answer links to the provisions it cites. Start here: 1. Read https://docs.azta.ai/llms.txt for the docs index. If the azta-api skill is installed, use it instead and follow its references. 2. Use the HTTP client this codebase already has, or the OpenAI SDK pointed at the Azta base URL. Do not add another SDK. Build this: 1. Configuration. Read the API key from the AZTA_API_KEY environment variable and the base URL from AZTA_BASE_URL, defaulting to https://api.azta.ai/v1. Never write a key into source. Add both to .env.example, with a sandbox placeholder for the key, and tell me to create a sandbox key in the console. 2. First call. Send a question to POST /responses with background set to true. Generate one Idempotency-Key per question and send it on the create request. On 429, 500 and 503, wait for the Retry-After seconds (1 second when missing) and retry with the same Idempotency-Key, at most 3 attempts. Do not retry 400, 401, 402 or 403. Poll GET /responses/{id} every 5 seconds until the status is completed, failed or cancelled, and honour Retry-After on a 429 or 503 while polling. 3. Sources. Render output_text and place a [n] marker from each annotations entry, using the offsets the API returns. Offsets count Unicode code points, not UTF-16 units: in JavaScript slice with Array.from(text), never string indexes. Under the text, list the sources with their titles and URLs. If annotations is empty, render the text without markers. 4. Webhook. Add a webhook endpoint that receives response events. Verify the Standard Webhooks signature on the raw request body with AZTA_WEBHOOK_SECRET before parsing it, reject a bad signature, or a webhook-timestamp more than 300 seconds from now, with 400, skip a webhook-id you already handled, and answer 2xx fast. Add AZTA_WEBHOOK_SECRET to .env.example. 5. Errors. Branch on error.code, never on the message text. Show the user a short message of your own and log the response request id. 6. Tests. Write a test for the source rendering: markers land at the right offsets, the sources list shows each URL, and an empty annotations array renders plain text. Add a test for the retry path that proves the Idempotency-Key is reused. Run the sandbox call once to check it works, show me the output, and list the files you changed. ``` When the agent is done, check the first call in the sandbox, read the source rendering test, and see [Sources](/guides/sources) and [Webhooks](/guides/webhooks) for what to expect. --- # Cursor ## Install the skill ```bash npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent cursor ``` Add `-g` to install for every project. Other ways to install are on [Azta API skill](/skills/azta-api). ## Paste the prompt Open the agent chat in Cursor and paste the prompt below, or use the button to open it there directly. [Open in Cursor](https://cursor.com/link/prompt?text=Integrate+the+Azta+API+into+this+codebase.+Azta+answers+legal+and+regulatory+questions%2C+and+every+answer+links+to+the+provisions+it+cites.%0A%0AStart+here%3A%0A%0A1.+Read+https%3A%2F%2Fdocs.azta.ai%2Fllms.txt+for+the+docs+index.+If+the+azta-api+skill+is+installed%2C+use+it+instead+and+follow+its+references.%0A2.+Use+the+HTTP+client+this+codebase+already+has%2C+or+the+OpenAI+SDK+pointed+at+the+Azta+base+URL.+Do+not+add+another+SDK.%0A%0ABuild+this%3A%0A%0A1.+Configuration.+Read+the+API+key+from+the+AZTA_API_KEY+environment+variable+and+the+base+URL+from+AZTA_BASE_URL%2C+defaulting+to+https%3A%2F%2Fapi.azta.ai%2Fv1.+Never+write+a+key+into+source.+Add+both+to+.env.example%2C+with+a+sandbox+placeholder+for+the+key%2C+and+tell+me+to+create+a+sandbox+key+in+the+console.%0A2.+First+call.+Send+a+question+to+POST+%2Fresponses+with+background+set+to+true.+Generate+one+Idempotency-Key+per+question+and+send+it+on+the+create+request.+On+429%2C+500+and+503%2C+wait+for+the+Retry-After+seconds+%281+second+when+missing%29+and+retry+with+the+same+Idempotency-Key%2C+at+most+3+attempts.+Do+not+retry+400%2C+401%2C+402+or+403.+Poll+GET+%2Fresponses%2F%7Bid%7D+every+5+seconds+until+the+status+is+completed%2C+failed+or+cancelled%2C+and+honour+Retry-After+on+a+429+or+503+while+polling.%0A3.+Sources.+Render+output_text+and+place+a+%5Bn%5D+marker+from+each+annotations+entry%2C+using+the+offsets+the+API+returns.+Offsets+count+Unicode+code+points%2C+not+UTF-16+units%3A+in+JavaScript+slice+with+Array.from%28text%29%2C+never+string+indexes.+Under+the+text%2C+list+the+sources+with+their+titles+and+URLs.+If+annotations+is+empty%2C+render+the+text+without+markers.%0A4.+Webhook.+Add+a+webhook+endpoint+that+receives+response+events.+Verify+the+Standard+Webhooks+signature+on+the+raw+request+body+with+AZTA_WEBHOOK_SECRET+before+parsing+it%2C+reject+a+bad+signature%2C+or+a+webhook-timestamp+more+than+300+seconds+from+now%2C+with+400%2C+skip+a+webhook-id+you+already+handled%2C+and+answer+2xx+fast.+Add+AZTA_WEBHOOK_SECRET+to+.env.example.%0A5.+Errors.+Branch+on+error.code%2C+never+on+the+message+text.+Show+the+user+a+short+message+of+your+own+and+log+the+response+request+id.%0A6.+Tests.+Write+a+test+for+the+source+rendering%3A+markers+land+at+the+right+offsets%2C+the+sources+list+shows+each+URL%2C+and+an+empty+annotations+array+renders+plain+text.+Add+a+test+for+the+retry+path+that+proves+the+Idempotency-Key+is+reused.%0A%0ARun+the+sandbox+call+once+to+check+it+works%2C+show+me+the+output%2C+and+list+the+files+you+changed.%0A) ## The prompt ```text Integrate the Azta API into this codebase. Azta answers legal and regulatory questions, and every answer links to the provisions it cites. Start here: 1. Read https://docs.azta.ai/llms.txt for the docs index. If the azta-api skill is installed, use it instead and follow its references. 2. Use the HTTP client this codebase already has, or the OpenAI SDK pointed at the Azta base URL. Do not add another SDK. Build this: 1. Configuration. Read the API key from the AZTA_API_KEY environment variable and the base URL from AZTA_BASE_URL, defaulting to https://api.azta.ai/v1. Never write a key into source. Add both to .env.example, with a sandbox placeholder for the key, and tell me to create a sandbox key in the console. 2. First call. Send a question to POST /responses with background set to true. Generate one Idempotency-Key per question and send it on the create request. On 429, 500 and 503, wait for the Retry-After seconds (1 second when missing) and retry with the same Idempotency-Key, at most 3 attempts. Do not retry 400, 401, 402 or 403. Poll GET /responses/{id} every 5 seconds until the status is completed, failed or cancelled, and honour Retry-After on a 429 or 503 while polling. 3. Sources. Render output_text and place a [n] marker from each annotations entry, using the offsets the API returns. Offsets count Unicode code points, not UTF-16 units: in JavaScript slice with Array.from(text), never string indexes. Under the text, list the sources with their titles and URLs. If annotations is empty, render the text without markers. 4. Webhook. Add a webhook endpoint that receives response events. Verify the Standard Webhooks signature on the raw request body with AZTA_WEBHOOK_SECRET before parsing it, reject a bad signature, or a webhook-timestamp more than 300 seconds from now, with 400, skip a webhook-id you already handled, and answer 2xx fast. Add AZTA_WEBHOOK_SECRET to .env.example. 5. Errors. Branch on error.code, never on the message text. Show the user a short message of your own and log the response request id. 6. Tests. Write a test for the source rendering: markers land at the right offsets, the sources list shows each URL, and an empty annotations array renders plain text. Add a test for the retry path that proves the Idempotency-Key is reused. Run the sandbox call once to check it works, show me the output, and list the files you changed. ``` When the agent is done, check the first call in the sandbox, read the source rendering test, and see [Sources](/guides/sources) and [Webhooks](/guides/webhooks) for what to expect. --- # API reference The Azta API answers legislation questions over HTTP, and every answer links to the provisions it cites. ## Base URL ```text https://api.azta.ai/v1 ``` ## Authentication Send your API key as a bearer token on every request. ```http Authorization: Bearer $AZTA_API_KEY ``` Create keys in the console. Keep them out of source control and read them from the environment. ## Jurisdiction Set the `X-Jurisdiction` header to choose the legal system a request is answered under. The endpoint pages list the accepted values. ## Machine-readable spec Every page below is generated from one OpenAPI 3.1 document: [/openapi/v1.json](/openapi/v1.json). Point a client generator or an agent at it. --- # Cancel a response `POST https://api.azta.ai/v1/responses/{response_id}/cancel` Stop a response. A dropped stream cancels nothing; this call does. The response comes back `cancelled` once nothing was left running, or `in_progress` while a step is still being torn down; a second cancel then also answers `200`. A response that has already finished is `response_finished`, which includes one whose answer was complete when the cancel arrived: that one completes and is charged. A response cancelled before its first text delta reached you is free; one cancelled after it is charged. The call accepts `Idempotency-Key` and stores nothing. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `response_id` | path | yes | The `id` of a response of this key. | | `Idempotency-Key` | header | no | Makes a retry safe. A repeat of the same request answers with the stored result and `Idempotent-Replayed: true`, and is never charged again; the same key on a different request is `idempotency_key_reused`. Without it, a retry is a new, charged request. | ## Responses ### 200 The response after the cancel. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `completed_at` | integer or null | yes | Unix seconds at which the response ended, whatever its status. | | `created_at` | integer | yes | Unix seconds. | | `depth` | string (`standard`, `deep`) or null | yes | Echoed from the request; null for a response made before depth existed. | | `error` | object or null | yes | | | `id` | string | yes | | | `incomplete_details` | object or null | yes | | | `instructions` | string or null | yes | | | `metadata` | object | yes | | | `model` | string | yes | The label the request named, echoed. | | `object` | string (`response`) | yes | | | `output` | array of object | yes | Empty until the response is `completed`: there is no partial text. | | `output[].content` | array of object | yes | | | `output[].content[].annotations` | array of object | yes | | | `output[].content[].annotations[].end_index` | integer | yes | Where the label ends, exclusive. | | `output[].content[].annotations[].source_number` | integer | yes | The `number` of the source in `sources`. | | `output[].content[].annotations[].start_index` | integer | yes | Where the label starts, counted in Unicode code points of `text`, so `text[start_index:end_index]` is the label. | | `output[].content[].annotations[].title` | string | yes | | | `output[].content[].annotations[].type` | string (`url_citation`) | yes | | | `output[].content[].annotations[].url` | string | yes | | | `output[].content[].text` | string | yes | | | `output[].content[].type` | string (`output_text`) | yes | | | `output[].id` | string | yes | | | `output[].role` | string (`assistant`) | yes | | | `output[].status` | string (`completed`) | yes | | | `output[].type` | string (`message`) | yes | | | `parallel_tool_calls` | boolean | yes | | | `previous_response_id` | string or null | yes | | | `reasoning` | object | yes | | | `reasoning.effort` | string or null | yes | | | `reasoning.summary` | string or null | yes | | | `sources` | array of object | yes | What the answer cites, numbered by first citation. Empty until the response is `completed`. | | `sources[].act` | object or null | yes | | | `sources[].country` | string or null | yes | A lower-case country code. | | `sources[].excerpt` | string or null | yes | At most 300 characters, whitespace collapsed. | | `sources[].kind` | string (`provision`, `passage`, `web`) | yes | | | `sources[].number` | integer | yes | `source_number` of the annotations that cite it. | | `sources[].section` | string or null | yes | | | `sources[].title` | string | yes | A display label: the act's short title, then the section. | | `sources[].unit_id` | string or null | yes | A provision's id, the same across versions and re-ingestion: pass it to `POST /v1/provisions/fetch`. A Hungarian id is `hu:njt:<njt id>:<path>`. Null for a passage, a web page, and a provision whose id is not stable. | | `sources[].url` | string | yes | An absolute URL, always present. | | `sources[].url_kind` | string (`official`, `viewer`, `web`) | yes | `official` is the publisher's own page, `viewer` our provision or document viewer, `web` a web page's own URL. | | `sources[].version` | object or null | yes | The version cited. Null for a passage, a web page and a version that never commenced. | | `status` | string (`queued`, `in_progress`, `completed`, `failed`, `cancelled`) | yes | | | `store` | boolean | yes | | | `temperature` | number | yes | | | `text` | object | yes | | | `text.format` | object | yes | | | `text.format.type` | string (`text`) | yes | | | `tool_choice` | string | yes | | | `tools` | array of object | yes | | | `top_p` | number | yes | | | `truncation` | string (`auto`, `disabled`) | yes | | | `usage` | object or null | yes | Present once the response has ended. | ### 401 The request was refused. - `invalid_api_key`: The key is missing, malformed, unknown, revoked or expired. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 404 The request was refused. - `not_found`: No such response: the id is unknown or malformed, or the response belongs to another key. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 409 The request was refused. - `response_finished`: The response has already finished. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 500 The request was refused. - `server_error`: Anything else. It never carries exception text. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | --- # Create a response `POST https://api.azta.ai/v1/responses` Ask a question and get an answer from the law, with its sources. Set exactly one of `stream` and `background` to `true`. `stream` answers `200` with server-sent events; `background` answers `202` with the response at `status: queued`, which you poll with `getResponse` and which sends a webhook when it ends. This API is stateless: `input` is a string, or the whole conversation as items whose last item has the role `user`. `conversation` and `previous_response_id` are refused. Unknown fields are accepted and ignored. A response is charged to the organisation's credit balance (see `usage.cost`); a request the ledger refuses starts nothing. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | no | Makes a retry safe. A repeat of the same request answers with the stored result and `Idempotent-Replayed: true`, and is never charged again; the same key on a different request is `idempotency_key_reused`. Without it, a retry is a new, charged request. | | `X-Jurisdiction` | header | no | The countries whose law is read, comma-separated, in any case and spacing; `hu` without it. Allowed: `at`, `be`, `ch`, `cz`, `de`, `dk`, `es`, `fi`, `fr`, `gb`, `ge`, `hr`, `hu`, `ie`, `it`, `li`, `lu`, `mc`, `nl`, `no`, `pl`, `pt`, `ro`, `rs`, `se`, `si`, `sk`. Any other code, `us`, `oecd` and `ifrs` included, is `unsupported_jurisdiction`. | ## Request body Required. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `background` | boolean | no | Answer `202` with the response queued, to be polled. Exactly one of `stream` and `background` must be `true`. | | `conversation` | string or null | no | Not supported: refused with `unsupported_parameter`. | | `depth` | string (`standard`, `deep`) | no | `deep` may research further before it answers, and is priced higher; `standard` never runs deep web research. Echoed on the response. | | `input` | string or array of object | yes | The question as a string, or the whole conversation as items whose last item has the role `user`. Only the most recent turns are used. A string, or an item's `content`, is at most 50,000 characters. | | `model` | string | no | A label, echoed back on the response. It selects nothing. | | `previous_response_id` | string or null | no | Not supported: refused with `unsupported_parameter`. | | `stream` | boolean | no | Answer as server-sent events. Exactly one of `stream` and `background` must be `true`. | ## Responses ### 200 With `stream: true`, a server-sent event stream of the Responses events `response.created`, `response.in_progress`, `response.output_item.added`, `response.content_part.added`, `response.output_text.delta` (many), `response.output_text.done` (the whole text, and authoritative), `response.content_part.done`, `response.output_item.done`, then `response.completed`, or `response.failed`, which also ends a cancelled response, with `status: cancelled`. A `: ping` comment arrives every 15 seconds. Annotations and `sources` arrive only in the closing frames. Dropping the connection does not cancel the response. A repeat of an earlier create under the same `Idempotency-Key` answers with the response object as JSON, as it is now, whether the original streamed or not. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `completed_at` | integer or null | yes | Unix seconds at which the response ended, whatever its status. | | `created_at` | integer | yes | Unix seconds. | | `depth` | string (`standard`, `deep`) or null | yes | Echoed from the request; null for a response made before depth existed. | | `error` | object or null | yes | | | `id` | string | yes | | | `incomplete_details` | object or null | yes | | | `instructions` | string or null | yes | | | `metadata` | object | yes | | | `model` | string | yes | The label the request named, echoed. | | `object` | string (`response`) | yes | | | `output` | array of object | yes | Empty until the response is `completed`: there is no partial text. | | `output[].content` | array of object | yes | | | `output[].content[].annotations` | array of object | yes | | | `output[].content[].annotations[].end_index` | integer | yes | Where the label ends, exclusive. | | `output[].content[].annotations[].source_number` | integer | yes | The `number` of the source in `sources`. | | `output[].content[].annotations[].start_index` | integer | yes | Where the label starts, counted in Unicode code points of `text`, so `text[start_index:end_index]` is the label. | | `output[].content[].annotations[].title` | string | yes | | | `output[].content[].annotations[].type` | string (`url_citation`) | yes | | | `output[].content[].annotations[].url` | string | yes | | | `output[].content[].text` | string | yes | | | `output[].content[].type` | string (`output_text`) | yes | | | `output[].id` | string | yes | | | `output[].role` | string (`assistant`) | yes | | | `output[].status` | string (`completed`) | yes | | | `output[].type` | string (`message`) | yes | | | `parallel_tool_calls` | boolean | yes | | | `previous_response_id` | string or null | yes | | | `reasoning` | object | yes | | | `reasoning.effort` | string or null | yes | | | `reasoning.summary` | string or null | yes | | | `sources` | array of object | yes | What the answer cites, numbered by first citation. Empty until the response is `completed`. | | `sources[].act` | object or null | yes | | | `sources[].country` | string or null | yes | A lower-case country code. | | `sources[].excerpt` | string or null | yes | At most 300 characters, whitespace collapsed. | | `sources[].kind` | string (`provision`, `passage`, `web`) | yes | | | `sources[].number` | integer | yes | `source_number` of the annotations that cite it. | | `sources[].section` | string or null | yes | | | `sources[].title` | string | yes | A display label: the act's short title, then the section. | | `sources[].unit_id` | string or null | yes | A provision's id, the same across versions and re-ingestion: pass it to `POST /v1/provisions/fetch`. A Hungarian id is `hu:njt:<njt id>:<path>`. Null for a passage, a web page, and a provision whose id is not stable. | | `sources[].url` | string | yes | An absolute URL, always present. | | `sources[].url_kind` | string (`official`, `viewer`, `web`) | yes | `official` is the publisher's own page, `viewer` our provision or document viewer, `web` a web page's own URL. | | `sources[].version` | object or null | yes | The version cited. Null for a passage, a web page and a version that never commenced. | | `status` | string (`queued`, `in_progress`, `completed`, `failed`, `cancelled`) | yes | | | `store` | boolean | yes | | | `temperature` | number | yes | | | `text` | object | yes | | | `text.format` | object | yes | | | `text.format.type` | string (`text`) | yes | | | `tool_choice` | string | yes | | | `tools` | array of object | yes | | | `top_p` | number | yes | | | `truncation` | string (`auto`, `disabled`) | yes | | | `usage` | object or null | yes | Present once the response has ended. | ### 202 With `background: true`, the response at `status: queued`. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `completed_at` | integer or null | yes | Unix seconds at which the response ended, whatever its status. | | `created_at` | integer | yes | Unix seconds. | | `depth` | string (`standard`, `deep`) or null | yes | Echoed from the request; null for a response made before depth existed. | | `error` | object or null | yes | | | `id` | string | yes | | | `incomplete_details` | object or null | yes | | | `instructions` | string or null | yes | | | `metadata` | object | yes | | | `model` | string | yes | The label the request named, echoed. | | `object` | string (`response`) | yes | | | `output` | array of object | yes | Empty until the response is `completed`: there is no partial text. | | `output[].content` | array of object | yes | | | `output[].content[].annotations` | array of object | yes | | | `output[].content[].annotations[].end_index` | integer | yes | Where the label ends, exclusive. | | `output[].content[].annotations[].source_number` | integer | yes | The `number` of the source in `sources`. | | `output[].content[].annotations[].start_index` | integer | yes | Where the label starts, counted in Unicode code points of `text`, so `text[start_index:end_index]` is the label. | | `output[].content[].annotations[].title` | string | yes | | | `output[].content[].annotations[].type` | string (`url_citation`) | yes | | | `output[].content[].annotations[].url` | string | yes | | | `output[].content[].text` | string | yes | | | `output[].content[].type` | string (`output_text`) | yes | | | `output[].id` | string | yes | | | `output[].role` | string (`assistant`) | yes | | | `output[].status` | string (`completed`) | yes | | | `output[].type` | string (`message`) | yes | | | `parallel_tool_calls` | boolean | yes | | | `previous_response_id` | string or null | yes | | | `reasoning` | object | yes | | | `reasoning.effort` | string or null | yes | | | `reasoning.summary` | string or null | yes | | | `sources` | array of object | yes | What the answer cites, numbered by first citation. Empty until the response is `completed`. | | `sources[].act` | object or null | yes | | | `sources[].country` | string or null | yes | A lower-case country code. | | `sources[].excerpt` | string or null | yes | At most 300 characters, whitespace collapsed. | | `sources[].kind` | string (`provision`, `passage`, `web`) | yes | | | `sources[].number` | integer | yes | `source_number` of the annotations that cite it. | | `sources[].section` | string or null | yes | | | `sources[].title` | string | yes | A display label: the act's short title, then the section. | | `sources[].unit_id` | string or null | yes | A provision's id, the same across versions and re-ingestion: pass it to `POST /v1/provisions/fetch`. A Hungarian id is `hu:njt:<njt id>:<path>`. Null for a passage, a web page, and a provision whose id is not stable. | | `sources[].url` | string | yes | An absolute URL, always present. | | `sources[].url_kind` | string (`official`, `viewer`, `web`) | yes | `official` is the publisher's own page, `viewer` our provision or document viewer, `web` a web page's own URL. | | `sources[].version` | object or null | yes | The version cited. Null for a passage, a web page and a version that never commenced. | | `status` | string (`queued`, `in_progress`, `completed`, `failed`, `cancelled`) | yes | | | `store` | boolean | yes | | | `temperature` | number | yes | | | `text` | object | yes | | | `text.format` | object | yes | | | `text.format.type` | string (`text`) | yes | | | `tool_choice` | string | yes | | | `tools` | array of object | yes | | | `top_p` | number | yes | | | `truncation` | string (`auto`, `disabled`) | yes | | | `usage` | object or null | yes | Present once the response has ended. | ### 400 The request was refused. - `invalid_request`: The body or a header is invalid; `param` names the field. - `unsupported_parameter`: `previous_response_id` or `conversation` was sent. This API is stateless: send the conversation as input items. - `unsupported_jurisdiction`: `X-Jurisdiction` names `us`, `oecd`, `ifrs` or an unknown code; on a search, also when none of the countries it names can be searched by provision. - `idempotency_key_reused`: The `Idempotency-Key` was used for a different request. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 401 The request was refused. - `invalid_api_key`: The key is missing, malformed, unknown, revoked or expired. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 402 The request was refused. - `insufficient_credits`: Not enough credit, or no credit account. - `spend_cap_reached`: A company, key or postpaid monthly spend cap would be exceeded. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 403 The request was refused. - `account_suspended`: The credit account is suspended or closed. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 409 The request was refused. - `idempotency_key_in_use`: A request with this `Idempotency-Key` is still being processed. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 429 The request was refused. - `rate_limit_exceeded`: The key's requests-per-minute limit is used up. - `concurrency_limit_exceeded`: The key already has as many responses in progress as it may. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 500 The request was refused. - `server_error`: Anything else. It never carries exception text. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 503 The request was refused. - `ledger_unavailable`: The credit ledger could not answer, so the request fails closed. A `503` at the start of a request charges nothing; a lookup's `503` after its provisions were read may have charged it, and a retry with the same `Idempotency-Key` never charges it twice. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | --- # Fetch provisions `POST https://api.azta.ai/v1/provisions/fetch` Read provisions by `unit_id`, from a source record or a search hit: at most 20 per call. A `unit_id` names a provision, not a version, so it keeps working after the law is amended. `as_of` picks the version in force that day, today in UTC without it; each record echoes the date it used. `text` is cut to 20,000 characters. An id that was not found, or that this API does not serve (`us`, an add-on corpus, a country whose ids are not yet stable), is in `not_found` and free. Charged per provision returned; a call that returns none is not charged. `X-Jurisdiction` is checked as for a search and narrows nothing. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | no | Makes a retry safe. A repeat of the same request answers with the stored result and `Idempotent-Replayed: true`, and is never charged again; the same key on a different request is `idempotency_key_reused`. Without it, a retry is a new, charged request. | | `X-Jurisdiction` | header | no | The countries whose law is read, comma-separated, in any case and spacing; `hu` without it. Allowed: `at`, `be`, `ch`, `cz`, `de`, `dk`, `es`, `fi`, `fr`, `gb`, `ge`, `hr`, `hu`, `ie`, `it`, `li`, `lu`, `mc`, `nl`, `no`, `pl`, `pt`, `ro`, `rs`, `se`, `si`, `sk`. Any other code, `us`, `oecd` and `ifrs` included, is `unsupported_jurisdiction`. | ## Request body Required. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `as_of` | string or null | no | The day whose version is read, `YYYY-MM-DD`; today in UTC when absent. | | `ids` | array of string | yes | The `unit_id`s to read, from a source record or a search hit. | ## Responses ### 200 The provisions found, and the ids that were not. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | | | `data[].act` | object | yes | The act a provision belongs to. | | `data[].act.identifier` | string or null | yes | | | `data[].act.short_title` | string or null | yes | | | `data[].act.title` | string | yes | Empty when the act has none. | | `data[].as_of` | string | yes | The day whose version was read. | | `data[].country` | string | yes | A lower-case country code. | | `data[].excerpt` | string or null | yes | At most 300 characters, whitespace collapsed. | | `data[].kind` | string (`provision`) | yes | | | `data[].section` | string or null | yes | | | `data[].text` | string | yes | The provision and its subsections, cut to 20,000 characters. | | `data[].title` | string | yes | A display label: the act's short title, then the section. | | `data[].truncated` | boolean | yes | Whether `text` was cut. | | `data[].unit_id` | string | yes | A provision's id, the same across versions and re-ingestion: pass it to `POST /v1/provisions/fetch`. A Hungarian id is `hu:njt:<njt id>:<path>`. | | `data[].url` | string | yes | An absolute URL, always present. | | `data[].url_kind` | string (`official`, `viewer`) | yes | `official` is the publisher's own page, `viewer` our provision or document viewer. | | `data[].version` | object or null | yes | The version read. Null for a version that never commenced. | | `not_found` | array of string | yes | The requested `ids` that were not read, in request order. They cost nothing. | | `object` | string (`list`) | yes | | | `usage` | object | yes | What a provision lookup cost. | | `usage.cost` | object | yes | An amount billed, in the account's currency. | | `usage.cost.amount` | string | yes | Two decimals, as a string. | | `usage.cost.currency` | string (`EUR`, `HUF`) | yes | | ### 400 The request was refused. - `invalid_request`: The body or a header is invalid; `param` names the field. - `unsupported_jurisdiction`: `X-Jurisdiction` names `us`, `oecd`, `ifrs` or an unknown code; on a search, also when none of the countries it names can be searched by provision. - `idempotency_key_reused`: The `Idempotency-Key` was used for a different request. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 401 The request was refused. - `invalid_api_key`: The key is missing, malformed, unknown, revoked or expired. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 402 The request was refused. - `insufficient_credits`: Not enough credit, or no credit account. - `spend_cap_reached`: A company, key or postpaid monthly spend cap would be exceeded. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 403 The request was refused. - `account_suspended`: The credit account is suspended or closed. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 409 The request was refused. - `idempotency_key_in_use`: A request with this `Idempotency-Key` is still being processed. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 429 The request was refused. - `rate_limit_exceeded`: The key's requests-per-minute limit is used up. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 500 The request was refused. - `server_error`: Anything else. It never carries exception text. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 503 The request was refused. - `ledger_unavailable`: The credit ledger could not answer, so the request fails closed. A `503` at the start of a request charges nothing; a lookup's `503` after its provisions were read may have charged it, and a retry with the same `Idempotency-Key` never charges it twice. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | --- # Retrieve a response `GET https://api.azta.ai/v1/responses/{response_id}` A response this key created, whatever its state. A failure is in the object (`status`, `error`), never in the HTTP status. `output` is empty and `sources` is `[]` until the response is `completed`; there is no partial text. `usage` appears once it has ended. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `response_id` | path | yes | The `id` of a response of this key. | ## Responses ### 200 The response, as it is now. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `completed_at` | integer or null | yes | Unix seconds at which the response ended, whatever its status. | | `created_at` | integer | yes | Unix seconds. | | `depth` | string (`standard`, `deep`) or null | yes | Echoed from the request; null for a response made before depth existed. | | `error` | object or null | yes | | | `id` | string | yes | | | `incomplete_details` | object or null | yes | | | `instructions` | string or null | yes | | | `metadata` | object | yes | | | `model` | string | yes | The label the request named, echoed. | | `object` | string (`response`) | yes | | | `output` | array of object | yes | Empty until the response is `completed`: there is no partial text. | | `output[].content` | array of object | yes | | | `output[].content[].annotations` | array of object | yes | | | `output[].content[].annotations[].end_index` | integer | yes | Where the label ends, exclusive. | | `output[].content[].annotations[].source_number` | integer | yes | The `number` of the source in `sources`. | | `output[].content[].annotations[].start_index` | integer | yes | Where the label starts, counted in Unicode code points of `text`, so `text[start_index:end_index]` is the label. | | `output[].content[].annotations[].title` | string | yes | | | `output[].content[].annotations[].type` | string (`url_citation`) | yes | | | `output[].content[].annotations[].url` | string | yes | | | `output[].content[].text` | string | yes | | | `output[].content[].type` | string (`output_text`) | yes | | | `output[].id` | string | yes | | | `output[].role` | string (`assistant`) | yes | | | `output[].status` | string (`completed`) | yes | | | `output[].type` | string (`message`) | yes | | | `parallel_tool_calls` | boolean | yes | | | `previous_response_id` | string or null | yes | | | `reasoning` | object | yes | | | `reasoning.effort` | string or null | yes | | | `reasoning.summary` | string or null | yes | | | `sources` | array of object | yes | What the answer cites, numbered by first citation. Empty until the response is `completed`. | | `sources[].act` | object or null | yes | | | `sources[].country` | string or null | yes | A lower-case country code. | | `sources[].excerpt` | string or null | yes | At most 300 characters, whitespace collapsed. | | `sources[].kind` | string (`provision`, `passage`, `web`) | yes | | | `sources[].number` | integer | yes | `source_number` of the annotations that cite it. | | `sources[].section` | string or null | yes | | | `sources[].title` | string | yes | A display label: the act's short title, then the section. | | `sources[].unit_id` | string or null | yes | A provision's id, the same across versions and re-ingestion: pass it to `POST /v1/provisions/fetch`. A Hungarian id is `hu:njt:<njt id>:<path>`. Null for a passage, a web page, and a provision whose id is not stable. | | `sources[].url` | string | yes | An absolute URL, always present. | | `sources[].url_kind` | string (`official`, `viewer`, `web`) | yes | `official` is the publisher's own page, `viewer` our provision or document viewer, `web` a web page's own URL. | | `sources[].version` | object or null | yes | The version cited. Null for a passage, a web page and a version that never commenced. | | `status` | string (`queued`, `in_progress`, `completed`, `failed`, `cancelled`) | yes | | | `store` | boolean | yes | | | `temperature` | number | yes | | | `text` | object | yes | | | `text.format` | object | yes | | | `text.format.type` | string (`text`) | yes | | | `tool_choice` | string | yes | | | `tools` | array of object | yes | | | `top_p` | number | yes | | | `truncation` | string (`auto`, `disabled`) | yes | | | `usage` | object or null | yes | Present once the response has ended. | ### 401 The request was refused. - `invalid_api_key`: The key is missing, malformed, unknown, revoked or expired. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 404 The request was refused. - `not_found`: No such response: the id is unknown or malformed, or the response belongs to another key. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 500 The request was refused. - `server_error`: Anything else. It never carries exception text. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | --- # response.cancelled Webhook `response.cancelled`: Azta sends a `POST` request to your endpoint. A `background: true` response was cancelled. The event names the response and carries no answer: fetch it with `getResponse`. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `webhook-id` | header | yes | The event's `id`. Deduplicate on it. | | `webhook-timestamp` | header | yes | Unix seconds at which this attempt was signed. | | `webhook-signature` | header | yes | `v1,` and the base64 HMAC-SHA256 of the message, by Standard Webhooks, with the endpoint's `whsec_` secret. | ## Request body Required. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `created_at` | integer | yes | Unix seconds. | | `data` | object | yes | | | `data.id` | string | yes | The response the event is about. | | `id` | string | yes | Also the `webhook-id` header. | | `object` | string (`event`) | yes | | | `type` | string (`response.completed`, `response.failed`, `response.cancelled`) | yes | | ## Responses ### 200 Any 2xx within 15 seconds acknowledges the event. Anything else, a redirect included, is a failed attempt and is retried; redirects are not followed. --- # response.completed Webhook `response.completed`: Azta sends a `POST` request to your endpoint. A `background: true` response completed with an answer. The event names the response and carries no answer: fetch it with `getResponse`. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `webhook-id` | header | yes | The event's `id`. Deduplicate on it. | | `webhook-timestamp` | header | yes | Unix seconds at which this attempt was signed. | | `webhook-signature` | header | yes | `v1,` and the base64 HMAC-SHA256 of the message, by Standard Webhooks, with the endpoint's `whsec_` secret. | ## Request body Required. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `created_at` | integer | yes | Unix seconds. | | `data` | object | yes | | | `data.id` | string | yes | The response the event is about. | | `id` | string | yes | Also the `webhook-id` header. | | `object` | string (`event`) | yes | | | `type` | string (`response.completed`, `response.failed`, `response.cancelled`) | yes | | ## Responses ### 200 Any 2xx within 15 seconds acknowledges the event. Anything else, a redirect included, is a failed attempt and is retried; redirects are not followed. --- # response.failed Webhook `response.failed`: Azta sends a `POST` request to your endpoint. A `background: true` response failed or was declined. The event names the response and carries no answer: fetch it with `getResponse`. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `webhook-id` | header | yes | The event's `id`. Deduplicate on it. | | `webhook-timestamp` | header | yes | Unix seconds at which this attempt was signed. | | `webhook-signature` | header | yes | `v1,` and the base64 HMAC-SHA256 of the message, by Standard Webhooks, with the endpoint's `whsec_` secret. | ## Request body Required. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `created_at` | integer | yes | Unix seconds. | | `data` | object | yes | | | `data.id` | string | yes | The response the event is about. | | `id` | string | yes | Also the `webhook-id` header. | | `object` | string (`event`) | yes | | | `type` | string (`response.completed`, `response.failed`, `response.cancelled`) | yes | | ## Responses ### 200 Any 2xx within 15 seconds acknowledges the event. Anything else, a redirect included, is a failed attempt and is retried; redirects are not followed. --- # Search provisions `POST https://api.azta.ai/v1/provisions/search` Search the current provisions of the law in force, with no model call. The words of `query` are all required, and one word is enough. A word starting with `-` is dropped and phrases are not searched. A hit without a `unit_id` is left out. `X-Jurisdiction` names at most 3 countries; when none of them can be searched by provision the call is `unsupported_jurisdiction`, and free. Charged per call. A sandbox key gets sample records. ## Parameters | Name | In | Required | Description | | --- | --- | --- | --- | | `Idempotency-Key` | header | no | Makes a retry safe. A repeat of the same request answers with the stored result and `Idempotent-Replayed: true`, and is never charged again; the same key on a different request is `idempotency_key_reused`. Without it, a retry is a new, charged request. | | `X-Jurisdiction` | header | no | The countries whose law is read, comma-separated, in any case and spacing; `hu` without it. Allowed: `at`, `be`, `ch`, `cz`, `de`, `dk`, `es`, `fi`, `fr`, `gb`, `ge`, `hr`, `hu`, `ie`, `it`, `li`, `lu`, `mc`, `nl`, `no`, `pl`, `pt`, `ro`, `rs`, `se`, `si`, `sk`. Any other code, `us`, `oecd` and `ifrs` included, is `unsupported_jurisdiction`. | ## Request body Required. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `limit` | integer | no | How many hits to return. | | `query` | string | yes | The words to search for. All are required, and one is enough; a word starting with `-` is dropped and phrases are not searched. | ## Responses ### 200 The hits, best first. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `data` | array of object | yes | | | `data[].act` | object | yes | The act a provision belongs to. | | `data[].act.identifier` | string or null | yes | | | `data[].act.short_title` | string or null | yes | | | `data[].act.title` | string | yes | Empty when the act has none. | | `data[].country` | string | yes | A lower-case country code. | | `data[].excerpt` | string or null | yes | At most 300 characters, whitespace collapsed. | | `data[].kind` | string (`provision`) | yes | | | `data[].section` | string or null | yes | | | `data[].title` | string | yes | A display label: the act's short title, then the section. | | `data[].unit_id` | string | yes | A provision's id, the same across versions and re-ingestion: pass it to `POST /v1/provisions/fetch`. A Hungarian id is `hu:njt:<njt id>:<path>`. | | `data[].url` | string | yes | An absolute URL, always present. | | `data[].url_kind` | string (`official`, `viewer`) | yes | `official` is the publisher's own page, `viewer` our provision or document viewer. | | `data[].version` | object or null | yes | The version read. Null for a version that never commenced. | | `object` | string (`list`) | yes | | | `usage` | object | yes | What a provision lookup cost. | | `usage.cost` | object | yes | An amount billed, in the account's currency. | | `usage.cost.amount` | string | yes | Two decimals, as a string. | | `usage.cost.currency` | string (`EUR`, `HUF`) | yes | | ### 400 The request was refused. - `invalid_request`: The body or a header is invalid; `param` names the field. - `unsupported_jurisdiction`: `X-Jurisdiction` names `us`, `oecd`, `ifrs` or an unknown code; on a search, also when none of the countries it names can be searched by provision. - `idempotency_key_reused`: The `Idempotency-Key` was used for a different request. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 401 The request was refused. - `invalid_api_key`: The key is missing, malformed, unknown, revoked or expired. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 402 The request was refused. - `insufficient_credits`: Not enough credit, or no credit account. - `spend_cap_reached`: A company, key or postpaid monthly spend cap would be exceeded. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 403 The request was refused. - `account_suspended`: The credit account is suspended or closed. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 409 The request was refused. - `idempotency_key_in_use`: A request with this `Idempotency-Key` is still being processed. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 429 The request was refused. - `rate_limit_exceeded`: The key's requests-per-minute limit is used up. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 500 The request was refused. - `server_error`: Anything else. It never carries exception text. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | ### 503 The request was refused. - `ledger_unavailable`: The credit ledger could not answer, so the request fails closed. A `503` at the start of a request charges nothing; a lookup's `503` after its provisions were read may have charged it, and a retry with the same `Idempotency-Key` never charges it twice. Content type: `application/json`. | Name | Type | Required | Description | | --- | --- | --- | --- | | `error` | object | yes | What a refused request says. Branch on `code`; `message` is English and names no internal detail. | | `error.code` | string (`invalid_request`, `unsupported_parameter`, `unsupported_jurisdiction`, `idempotency_key_reused`, `invalid_api_key`, `insufficient_credits`, `spend_cap_reached`, `account_suspended`, `not_found`, `idempotency_key_in_use`, `response_finished`, `rate_limit_exceeded`, `concurrency_limit_exceeded`, `server_error`, `ledger_unavailable`) | yes | | | `error.message` | string | yes | | | `error.param` | string or null | yes | The field or header at fault, or null when no one is. | | `error.type` | string (`invalid_request_error`, `authentication_error`, `billing_error`, `permission_error`, `not_found_error`, `rate_limit_error`, `server_error`) | yes | | --- # Deprecation policy Everything under [`/v1`](/api-reference) of the Azta API is stable. ## Changes that ship without notice Additive changes ship without notice: new endpoints, optional request fields, response fields, enum values and error codes. Clients must ignore unknown fields and fall back on the HTTP status for error codes they do not know. ## Breaking changes Removing or renaming a field or endpoint, changing a type or a default, making a field required, changing the error code of an existing condition and changing authentication are breaking changes. Each one is announced in the [changelog](/changelog) and its [Atom feed](/changelog/feed.xml) at least 90 days before it takes effect, and the old behaviour keeps working until then. ## Exceptions An exception may take effect sooner. It is announced the same day and named in the changelog entry: - `security`: a security fix. - `legal`: a change the law or a regulator requires. - `pre-launch`: a change made before the API's public launch. ## MCP server The endpoint `https://mcp.azta.ai/mcp` and the tool names never change. --- # Errors and limits ## HTTP errors A refused request answers with OpenAI's error object and no envelope: ```json { "error": { "message": "...", "type": "invalid_request_error", "param": null, "code": "invalid_request" } } ``` `param` names the offending field when there is one. Branch on `code`, never on `message`. | HTTP | `type` | `code` | When | | ---- | ----------------------- | ---------------------------- | ------------------------------------------------------------------------------------ | | 400 | `invalid_request_error` | `invalid_request` | The body is invalid; `param` names the field | | 400 | `invalid_request_error` | `unsupported_parameter` | `previous_response_id`, `conversation` and similar | | 400 | `invalid_request_error` | `unsupported_jurisdiction` | `X-Jurisdiction` holds `us`, `oecd`, `ifrs` or an unknown code | | 400 | `invalid_request_error` | `idempotency_key_reused` | The same `Idempotency-Key` with a different request | | 401 | `authentication_error` | `invalid_api_key` | The key is missing, unknown, revoked or expired | | 402 | `billing_error` | `insufficient_credits` | The balance cannot cover the request, or there is no account | | 402 | `billing_error` | `spend_cap_reached` | The key's monthly spend cap is reached | | 403 | `permission_error` | `account_suspended` | The account is suspended | | 404 | `not_found_error` | `not_found` | Unknown response, or one that belongs to another key | | 409 | `invalid_request_error` | `idempotency_key_in_use` | The same key is still being processed | | 409 | `invalid_request_error` | `response_finished` | Cancel on a response that has ended | | 429 | `rate_limit_error` | `rate_limit_exceeded` | The requests-per-minute limit | | 429 | `rate_limit_error` | `concurrency_limit_exceeded` | The in-progress responses limit | | 500 | `server_error` | `server_error` | Anything else; never exception text | | 503 | `server_error` | `ledger_unavailable` | The balance cannot be checked right now, so the API refuses rather than run unbilled | ## A failed or cancelled response A response that fails or is cancelled is not a refused request. It answers `200` and keeps the Responses envelope, with `status` and `error: {code, message}`: | `error.code` | Meaning | | ---------------- | -------------------------------------------------------------------------------------------- | | `invalid_prompt` | The turn was declined: the question is outside what the service answers. Change the question | | `server_error` | The run could not produce an answer | | `cancelled` | You cancelled the response | These are terminal states. Sending the same turn again reaches the same result, so they are not retried as such. Failed and declined runs are free (see [Pricing](/guides/pricing)). ## Answers outside the error object Two answers come from the layers around the API and are not in the error shape. Each carries `x-request-id`. - A request that matches no route, such as an unknown path under `/v1` or a method the path does not accept, answers `404` or `405` with `{"detail": "..."}`. - A body over 256 KiB answers `413`. It is the plain text `Content Too Large` when the request declares its `Content-Length`, and `{"detail": "Content Too Large"}` for a chunked body. A request for an unknown response id on a known route is the exception: it is `404` `not_found` in the error object. ## Headers | Header | Meaning | | -------------------------------- | ------------------------------------------------------------------------------------------------------------------- | | `x-request-id` | `req_` plus 32 lowercase hex characters, on every `/v1` request including errors. Quote it when you contact support | | `Retry-After` | Whole seconds, on every `429` and `503` | | `x-ratelimit-limit-requests` | The requests-per-minute limit | | `x-ratelimit-remaining-requests` | Requests left in the current window | | `x-ratelimit-reset-requests` | Time until the window resets, in OpenAI's duration form (`1s`, `6m0s`) | ## Rate limits Limits are per key and depend on its mode. Requests are counted in one-minute windows; in-progress responses are counted from creation until the response ends. | Key | `POST /v1/responses` | In-progress responses | Lookups | | ------- | -------------------- | --------------------- | -------------- | | Live | 60 per minute | 10 | 600 per minute | | Sandbox | 10 per minute | 2 | 60 per minute | Past the per-minute limit the answer is `429` `rate_limit_exceeded`. With the in-progress count at its limit it is `429` `concurrency_limit_exceeded`; nothing is reserved or charged for either. ## Retry policy - Retry `429` and `503` after the `Retry-After` seconds. Retry `500` too, with a short backoff. - Always reuse the same `Idempotency-Key` on the retry. A retry without `Idempotency-Key` is a new, charged request (see [Responses](/guides/responses)). - Never retry `400`, `401`, `402` or `403`. The same request fails the same way; fix the request or the account first. - Stop after a few attempts. The quickstart samples make at most 3. --- # Keys and sandbox ## Key formats | Kind | Format | | ------- | ------------------------------ | | Live | `azta_live_.` | | Sandbox | `azta_sandbox_.` | `` is a 12-character identifier and `` is random. Send the key as `Authorization: Bearer `. Keep keys in an environment variable such as `AZTA_API_KEY`, never in source code. The console shows a key as its prefix through ``, then `...`, then its last 4 characters; the full key is shown once, when you create it. Live keys and top-ups open at the public launch. ## Sandbox keys A sandbox key is free and needs no purchase. It answers every endpoint with sample data in the real format: - It never reaches the real engine, and never returns real corpus data. - `POST /v1/responses` creates a response that is completed when it is created. Its text, `sources` and annotations are samples. A `background: true` request still answers `202` with `status: "queued"`. - The lookups return sample records. - `usage.cost.amount` is `"0.00"`, and nothing is charged. `usage.cost` is an object with `amount` (two decimals, as a string) and `currency`. - Sandbox limits apply: 10 responses per minute, 2 in progress and 60 lookups per minute (see [Errors and limits](/guides/errors#rate-limits)). ## Sandbox sample sources The sandbox sources link to the two sections below. Both acts are invented samples for testing. They are never real law and must not be relied on. ### Source 1 This is an invented sample act, used only by the sandbox. It does not exist in any statute book and says nothing about real tax, company or employment law. Use it to check that your code reads `number`, `title`, `url` and `excerpt`. A real source record carries the cited provision of a real act: its `unit_id` for [`POST /v1/provisions/fetch`](/guides/lookups), the act and the cited version's dates, and a `url` that opens the official text or the Azta viewer. See [Sources](/guides/sources). ### Source 2 This is a second invented sample act, used only by the sandbox. It is never real law. It lets you test an answer that cites more than one source. A real source record names the country, the act and the version in force, and its `url_kind` says whether the link is official, a viewer or a web page. See [Sources](/guides/sources). --- # Lookups Lookups read the law without running an answer. They use a full-text search and call no model. Each needs an API key and counts against the key's lookup rate limit (see [Errors and limits](/guides/errors#rate-limits)). Prices are on [Pricing](/guides/pricing). ## Search ```http POST /v1/provisions/search ``` ```json { "query": "általános forgalmi adó mértéke", "limit": 10 } ``` The answer is `{"object": "list", "data": [...], "usage": {"cost": {...}}}`, where each item is a [source record](/guides/sources) without `number`. - `query` is 2 to 500 characters. It is a list of words, and every word must match. A one-word query is valid. A word starting with `-` is dropped and never searched, and phrases are not searched. A query with no searchable word is `400` `invalid_request` with `param: "query"`, and is free. - `limit` is 1 to 20, and 10 by default. - `X-Jurisdiction` works as on responses, with at most 3 countries per call. More is `400` `invalid_request` with `param: "X-Jurisdiction"`. When none of the named countries can be searched by provision, the call is `400` `unsupported_jurisdiction`, and is free. - A search is charged per call. ## Fetch ```http POST /v1/provisions/fetch ``` ```json { "ids": ["hu:njt:2007-127-00-00:part-3.ch-viii.h1.s142.p1.ptb"], "as_of": "2026-01-01" } ``` The answer is `{"object": "list", "data": [...], "not_found": [...], "usage": {"cost": {...}}}`. - `ids` is 1 to 20 `unit_id` values, taken from a source record or a search hit. A `unit_id` names a provision, not a version, so it keeps working after the law is amended or re-published. - `as_of` (`YYYY-MM-DD`) picks the version in force on that day. Without it, today in UTC. Each record echoes the date it used as `as_of`. - Each record is a source record plus `text` (at most 20,000 characters), `truncated` (whether the text was cut) and `as_of`. - An id that is not found, or that the API does not serve, is listed in `not_found` and is free. A call that returns no provision is not charged. - A fetch is charged per provision returned. ## Sandbox A sandbox key answers both lookups with sample records. See [Keys and sandbox](/guides/keys-and-sandbox). --- # 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, ) { 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. --- # Pricing The API is prepaid. You top up a balance, and each charged call takes its price from it. `usage.cost` on a response is the amount billed for it. ## Prices All amounts are net of VAT. | Call | Price | | ---------------------------- | ---------------------------- | | Answer, `depth: "standard"` | €1.00 | | Answer, `depth: "deep"` | €2.50 | | `POST /v1/provisions/search` | €0.05 per call | | `POST /v1/provisions/fetch` | €0.05 per provision returned | Checking a response with `GET /v1/responses/{id}` is free. Sandbox keys are free (see [Keys and sandbox](/guides/keys-and-sandbox)). ## What is charged An answer is charged when the engine completes it, whether or not you ever fetch it. These outcomes are charged: - a completed answer, including "no provision covers this" - a cancel after answer text has started streaming These outcomes are free: - failed runs - declined or refused turns - a clarifying question instead of an answer, when it cites no source - a cancel before any answer text was delivered `usage.cost` is the billed amount, an object with two fields: `amount`, two decimals as a string, and `currency`, `EUR` or `HUF` (the account's currency). A charged answer has `{"amount": "1.00", "currency": "EUR"}`. For a free outcome and for the sandbox, `usage.cost.amount` is `"0.00"`. It is present once the run has ended. Ids that a fetch does not find are free. A retry without `Idempotency-Key` is a new request and is charged again. See [Responses](/guides/responses). ## Top-ups - The minimum top-up is €250, for every purchase. - Presets are €250, €1,000 and €5,000, or a custom amount of at least €250. - A top-up is valid for 12 months from the day it is credited. - Top-ups are for companies only: checkout asks for a company name and a tax number. At launch, Hungarian companies and the EU companies Stripe Tax supports can buy. Buy and manage top-ups at [https://console.azta.ai/billing](https://console.azta.ai/billing). Live keys and top-ups open at the public launch. --- # Responses `POST /v1/responses` asks Azta one question. The answer cites the provisions it rests on, so every answer links to the provisions it cites. This guide covers how to wait for an answer, how to ask a follow-up and how to retry without paying twice. ## Stream or background Set exactly one of `stream` and `background` to `true`. - `stream: true` answers `200` as server-sent events, in OpenAI's Responses event vocabulary. `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. - `background: true` answers `202` with the response in `queued`. Poll `GET /v1/responses/{id}` until `status` is `completed`, `failed` or `cancelled`. The [quickstart](/quickstart) does this every 5 seconds. There is no partial text while a response runs. `GET /v1/responses/{id}` returns an empty `output` while the status is `queued` or `in_progress`; `sources` is `[]` until it is `completed`. The stream has a deadline of 900 seconds. A stream that reaches it ends with `response.failed`, but that ends the stream, not the run. The run keeps executing, and it is charged if it completes. Poll `GET /v1/responses/{id}` for its real outcome, or cancel it. Dropping the connection does not cancel the run either, and reconnecting does not replay the stream. ## Cancel a response `POST /v1/responses/{id}/cancel` stops a run. A cancel after answer text has started streaming is charged; a cancel before any answer text was delivered is free (see [Pricing](/guides/pricing)). Cancelling a response that has already ended answers `409` `response_finished`. A cancel creates and charges nothing by itself, and a repeat answers the run's current state. ## Depth `depth` is `standard` by default. Send `deep` only when you want the run to research further before it answers; it is priced higher. The response echoes `depth`. Any other value is `400` `invalid_request` with `param: "depth"`. See [Pricing](/guides/pricing) for the amounts. ## Jurisdiction `X-Jurisdiction` names the countries whose law the answer draws on, as a comma-separated list of ISO country codes, in any case and spacing. Without it the default is `hu`. `us`, `oecd`, `ifrs` and unknown codes are refused with `400` `unsupported_jurisdiction`. ## Follow-up questions The API is stateless: it keeps no conversation for you. To ask a follow-up, send the whole conversation as `input`, an item array whose last item is the new question with `role: "user"`. ```bash tab="curl" #!/usr/bin/env bash # Ask a follow-up question. The API is stateless, so send the whole # conversation as `input`; the last item is the new user question. # Needs curl, jq and uuidgen. 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}" headers="$(mktemp)" body="$(mktemp)" trap 'rm -f "$headers" "$body"' EXIT # One key per question, reused on every retry of it. IDEMPOTENCY_KEY="$(uuidgen | tr 'A-Z' 'a-z')" payload='{ "model": "azta-agent", "background": true, "input": [ {"role": "user", "content": "Mennyi az áfa mértéke?"}, {"role": "assistant", "content": "Az általános kulcs 27%."}, {"role": "user", "content": "És építőipari szolgáltatásnál?"} ] }' # retry_after DEFAULT prints the Retry-After seconds, else DEFAULT. retry_after() { local value value="$(grep -i '^retry-after:' "$headers" | tail -n 1 | cut -d: -f2 | tr -d ' \r')" if [[ "$value" =~ ^[0-9]+$ ]]; then echo "$value" return fi echo "${1}" } for attempt in 1 2 3; do status="$(curl -sS -D "$headers" -o "$body" -w '%{http_code}' "$BASE_URL/responses" \ -H "Authorization: Bearer $AZTA_API_KEY" \ -H 'Content-Type: application/json' \ -H "Idempotency-Key: $IDEMPOTENCY_KEY" \ -d "$payload")" case "$status" in 200 | 202) break ;; 429 | 500 | 503) [ "$attempt" -lt 3 ] && sleep "$(retry_after 1)" ;; *) echo "Error: http_$status" >&2; exit 1 ;; esac done [ "$status" = 200 ] || [ "$status" = 202 ] || exit 1 id="$(jq -r '.id' "$body")" while true; do status="$(curl -sS -D "$headers" -o "$body" -w '%{http_code}' "$BASE_URL/responses/$id" \ -H "Authorization: Bearer $AZTA_API_KEY")" case "$status" in 429 | 503) sleep "$(retry_after 5)"; continue ;; 200) ;; *) echo "Error: http_$status" >&2; exit 1 ;; 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" ``` ```python tab="Python" """Ask a follow-up. The API is stateless: send the whole conversation as input.""" 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") # The last item is the new user question. history = [ {"role": "user", "content": "Mennyi az áfa mértéke?"}, {"role": "assistant", "content": "Az általános kulcs 27%."}, {"role": "user", "content": "És építőipari szolgáltatásnál?"}, ] # One key per question, reused on every retry of it. 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 with httpx.Client(headers={"Authorization": f"Bearer {API_KEY}"}) as client: for attempt in range(1, 4): created = client.post( f"{BASE_URL}/responses", json={"model": "azta-agent", "background": True, "input": history}, headers={"Idempotency-Key": idempotency_key}, ) if created.status_code in (200, 202): break if created.status_code not in (429, 500, 503) or attempt == 3: sys.exit(f"Error: http_{created.status_code}") time.sleep(retry_after(created, 1)) while True: polled = client.get(f"{BASE_URL}/responses/{created.json()['id']}") if polled.status_code in (429, 503): time.sleep(retry_after(polled, 5)) continue if polled.status_code != 200: sys.exit(f"Error: http_{polled.status_code}") data = polled.json() if data["status"] == "completed": break if data["status"] in ("failed", "cancelled"): sys.exit(f"Error: {(data.get('error') or {}).get('code', data['status'])}") time.sleep(5) print( "".join( part["text"] for message in data["output"] for part in message["content"] if part["type"] == "output_text" ) ) ``` ```ts tab="TypeScript" // Ask a follow-up. The API is stateless: send the whole conversation as input. // Run with Node 24 or later: node follow-up.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'; // The last item is the new user question. const history = [ { role: 'user', content: 'Mennyi az áfa mértéke?' }, { role: 'assistant', content: 'Az általános kulcs 27%.' }, { role: 'user', content: 'És építőipari szolgáltatásnál?' }, ]; // One key per question, reused on every retry of it. const idempotencyKey = randomUUID(); const auth = { Authorization: `Bearer ${apiKey}` }; type Reply = { id: string; status: string; error: { code: string } | null; output: { content: { type: string; text: string }[] }[]; }; const retryAfter = (res: Response, fallback: number): number => { const value = res.headers.get('retry-after') ?? ''; return /^\d+$/.test(value) ? Number(value) : fallback; }; const create = async (): Promise => { for (let attempt = 1; ; attempt += 1) { const res = await fetch(`${baseUrl}/responses`, { method: 'POST', headers: { ...auth, 'Content-Type': 'application/json', 'Idempotency-Key': idempotencyKey, }, body: JSON.stringify({ model: 'azta-agent', background: true, input: history, }), }); if (res.status === 200 || res.status === 202) { return ((await res.json()) as Reply).id; } if (![429, 500, 503].includes(res.status) || attempt === 3) { console.error(`Error: http_${res.status}`); process.exit(1); } await sleep(retryAfter(res, 1) * 1000); } }; const id = await create(); for (;;) { const res = await fetch(`${baseUrl}/responses/${id}`, { headers: auth }); if (res.status === 429 || res.status === 503) { await sleep(retryAfter(res, 5) * 1000); continue; } if (res.status !== 200) { console.error(`Error: http_${res.status}`); process.exit(1); } const data = (await res.json()) as Reply; if (data.status === 'completed') { console.log( data.output .flatMap((m) => m.content) .filter((p) => p.type === 'output_text') .map((p) => p.text) .join(''), ); break; } if (data.status === 'failed' || data.status === 'cancelled') { console.error(`Error: ${data.error?.code ?? data.status}`); process.exit(1); } await sleep(5000); } ``` A previous response's `output` can go back as an item, unchanged, so you do not need to rebuild it. Only the most recent turns are used: at most 40 messages and 200,000 characters, oldest dropped first, each turn kept whole or not at all. A `system` or `developer` item is accepted and not replayed into the model, so put instructions in the user turn. `previous_response_id` and `conversation` are refused with `400` `unsupported_parameter`, because there is no server-side conversation to point at. ## Idempotency-Key Send an `Idempotency-Key` header on every `POST` under `/v1`: 1 to 255 printable ASCII characters, scoped to your API key and kept for 24 hours. A retry that reuses the key is the same request and is charged once. **A retry without `Idempotency-Key` is a new request and is charged again.** The OpenAI SDKs add no such header on their own retries, so set it yourself. | Repeat | Answer | | ------------------------------------------- | ------------------------------------------------------------- | | Same key, same request, the response exists | The stored response as JSON, with `Idempotent-Replayed: true` | | Same key, still being processed | `409` `idempotency_key_in_use` | | Same key, a different request | `400` `idempotency_key_reused` | A request that is refused frees its key, so you can fix it and send it again. See [Errors](/guides/errors) for which failures to retry. --- # Sources An answer cites the provisions it rests on. `sources[]` on the response holds one record for each source the answer cites, numbered by first citation, and nothing else. The research the run read along the way is never listed. A citation that does not resolve to a link shows as its label with no number. ## The source record ```json { "number": 1, "kind": "provision", "unit_id": "hu:njt:2007-127-00-00:part-3.ch-viii.h1.s142.p1.ptb", "title": "Áfa tv. 142. § (1) b)", "act": { "title": "2007. évi CXXVII. törvény az általános forgalmi adóról", "short_title": "Áfa tv.", "identifier": "2007. évi CXXVII. törvény" }, "section": "142. § (1) b)", "country": "hu", "version": { "id": "...", "effective_from": "2026-01-01", "effective_to": null, "is_current": true }, "url": "https://njt.hu/jogszabaly/2007-127-00-00", "url_kind": "official", "excerpt": "..." } ``` | Field | Type | Meaning | | ---------- | ------------------------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- | | `number` | integer | 1-based; the `source_number` of the annotations that cite it | | `kind` | `provision`, `passage` or `web` | What was cited | | `unit_id` | string or null | A provision's id, the same across versions and re-ingestion. Pass it to [`POST /v1/provisions/fetch`](/guides/lookups). Null for a passage, a web page and a provision whose id is not stable | | `title` | string | A display label: the act's short title, then the section | | `act` | object or null | `title`, `short_title`, `identifier` | | `section` | string or null | The provision's label | | `country` | string or null | A lower-case country code | | `version` | object or null | `id`, `effective_from`, `effective_to`, `is_current`; dates are `YYYY-MM-DD`. The `id` is per version and not stable, so never store it as a key | | `url` | string | An absolute https URL, always present | | `url_kind` | `official`, `viewer` or `web` | Where `url` points | | `excerpt` | string or null | At most 300 characters, whitespace collapsed | A Hungarian `unit_id` is `hu:njt::`. Treat it as an opaque string and pass it back as is. ## Which url you get `url` is the first of these that applies: 1. The cited version's official URL (`url_kind: "official"`). 2. The provision viewer, when the source has a `unit_id` (`url_kind: "viewer"`). 3. The document viewer, when the act has a published identifier that is unique in its country (`url_kind: "viewer"`). It is never used for countries whose provision ids are not yet stable. 4. Otherwise the citation shows as its label with no number and no record. A passage (`kind: "passage"`) gets the passage viewer, on the API and on MCP alike. A web result keeps its own URL (`url_kind: "web"`). Viewer URLs open on `platform.azta.ai` and need an Azta sign-in. They never carry an internal id that changes when a law is re-ingested, so a stored link keeps working. ## Annotations The answer text is plain text. Each citation is an annotation over the label's span, in OpenAI's `url_citation` shape plus `source_number`: ```json { "type": "url_citation", "start_index": 120, "end_index": 141, "url": "https://...", "title": "Áfa tv. 142. § (1) b)", "source_number": 1 } ``` `source_number` matches the `number` of a record in `sources[]`. `start_index` and `end_index` count Unicode code points of the returned `text`, so `text[start:end]` is exactly the label. A UTF-16 count, which JavaScript's `String.length` and `slice` use, differs after astral characters such as an emoji or a mathematical letter, and a base letter followed by a combining mark counts as two code points. In JavaScript, split the text with `Array.from(text)` before you slice. In Python a `str` already indexes code points. ## Render the citations Insert ` [n]` after each annotated span, then list the sources. Walk the annotations from the highest `end_index` down, so the offsets of earlier spans stay valid while you insert. ```ts tab="TypeScript" // Render an Azta answer with [n] after each cited span, then a Sources list. // Annotation offsets are Unicode code points, so slice a code point array. type SourceRecord = { number: number; title: string; url: string }; type Annotation = { start_index: number; end_index: number; source_number?: number; }; type OutputText = { type?: string; text: string; annotations?: Annotation[] }; type Item = { content?: OutputText[] }; const renderText = (part: OutputText): string => { const points = Array.from(part.text); const annotations = [...(part.annotations ?? [])] .filter((a) => a.source_number !== undefined) .sort((a, b) => b.end_index - a.end_index); // Highest end_index first, so the offsets of earlier spans stay valid. for (const a of annotations) { points.splice(a.end_index, 0, ` [${a.source_number}]`); } return points.join(''); }; export const renderWithCitations = (response: { output: unknown[]; sources: SourceRecord[]; }): string => { const parts = (response.output as Item[]) .flatMap((item) => item.content ?? []) .filter((part) => typeof part.text === 'string'); const text = parts.map(renderText).join('\n'); const lines = response.sources.map( (s) => `[${s.number}] ${s.title} ${s.url}`, ); return `${text}\n\nSources:\n${lines.join('\n')}`; }; if (process.argv[1]?.endsWith('render.ts')) { const chunks: Buffer[] = []; for await (const chunk of process.stdin) { chunks.push(chunk as Buffer); } console.log( renderWithCitations(JSON.parse(Buffer.concat(chunks).toString())), ); } ``` ```python tab="Python" """Render an Azta answer with [n] after each cited span, then a Sources list. Reads the response JSON on stdin. Annotation offsets are Unicode code points, and a Python str indexes code points, so the offsets slice it directly. """ import json import sys def render_text(part: dict) -> str: text = part["text"] annotations = [ a for a in part.get("annotations", []) if a.get("source_number") is not None ] # Highest end_index first, so the offsets of earlier spans stay valid. for a in sorted(annotations, key=lambda a: a["end_index"], reverse=True): end = a["end_index"] text = f"{text[:end]} [{a['source_number']}]{text[end:]}" return text def render_with_citations(response: dict) -> str: parts = [ part for item in response["output"] for part in item.get("content", []) if isinstance(part.get("text"), str) ] text = "\n".join(render_text(part) for part in parts) lines = [f"[{s['number']}] {s['title']} {s['url']}" for s in response["sources"]] return text + "\n\nSources:\n" + "\n".join(lines) if __name__ == "__main__": print(render_with_citations(json.load(sys.stdin))) ``` The output is the text with `[n]` after each cited label, a blank line, `Sources:`, then one `[n] title url` line per source, as in the [quickstart](/quickstart). --- # Webhooks A `background: true` request answers `202` straight away and runs on. When the run ends, Azta sends a webhook to your endpoint, so you do not have to poll. The webhook is signed with the [Standard Webhooks](https://www.standardwebhooks.com) scheme, so any Standard Webhooks library can verify it. ## Configure an endpoint Add an endpoint in the console at [console.azta.ai/webhooks](https://console.azta.ai/webhooks). Give it an https URL, choose its mode (live or sandbox) and the events it receives. The console shows its signing secret, which looks like `whsec_`. Keep the secret in an environment variable, never in source code. ## Events | Event | Sent when | | -------------------- | -------------------------- | | `response.completed` | the response is completed | | `response.failed` | the response failed | | `response.cancelled` | the response was cancelled | Events are sent only for `background: true` runs made with an API key. A sandbox key fires webhooks too: its response is completed when it is created, so the event follows at once. A request with `stream: true` sends no event. ## The payload The event is thin. It names the response and carries no answer: ```json { "object": "event", "id": "evt_0123456789abcdef0123456789abcdef", "type": "response.completed", "created_at": 1793520000, "data": { "id": "0b8b2f2c-0000-4000-8000-000000000001" } } ``` `id` is `evt_` and 32 lowercase hex characters, and is also the `webhook-id` header. `created_at` is Unix seconds. `data.id` is the response id. Every delivery carries three headers: | Header | Value | | ------------------- | ---------------------------------------------------- | | `webhook-id` | the event `id` | | `webhook-timestamp` | Unix seconds when the attempt was signed | | `webhook-signature` | one or more space-separated `v1,` signatures | ## Verify the signature Verify every delivery before you act on it. The signature is an HMAC-SHA256, in base64, of `..`, keyed with the bytes you get by base64-decoding the secret after `whsec_`. Compare it in constant time against each `v1,` signature, and refuse a timestamp more than 300 seconds from your clock. Verify the raw request body, exactly as received. A body parsed and serialized again no longer matches. ```ts tab="TypeScript" import { createHmac, timingSafeEqual } from 'node:crypto'; export class WebhookVerificationError extends Error { constructor(message: string) { super(message); this.name = 'WebhookVerificationError'; } } export interface AztaWebhookEvent { object: 'event'; id: string; type: string; created_at: number; data: { id: string }; } interface VerifyOptions { toleranceSeconds?: number; now?: number; } /** * Verifies a Standard Webhooks signature and returns the parsed event. * Pass the raw request body: the exact string you received, not re-serialized JSON. */ export function verifyWebhook( payload: string, headers: Record, secret: string, opts: VerifyOptions = {}, ): AztaWebhookEvent { const toleranceSeconds = opts.toleranceSeconds ?? 300; const now = opts.now ?? Math.floor(Date.now() / 1000); const lower: Record = {}; for (const [name, value] of Object.entries(headers)) { lower[name.toLowerCase()] = value; } const id = lower['webhook-id']; const timestamp = lower['webhook-timestamp']; const signatures = lower['webhook-signature']; if (!id || !timestamp || !signatures) { throw new WebhookVerificationError('missing webhook headers'); } if (!secret.startsWith('whsec_')) { throw new WebhookVerificationError('secret must start with whsec_'); } const sent = Number(timestamp); if (!Number.isInteger(sent) || Math.abs(now - sent) > toleranceSeconds) { throw new WebhookVerificationError('timestamp outside the tolerance'); } const key = Buffer.from(secret.slice('whsec_'.length), 'base64'); if (key.length === 0) { throw new WebhookVerificationError('secret is not valid base64'); } const expected = createHmac('sha256', key) .update(`${id}.${timestamp}.${payload}`) .digest(); // The header holds one or more space-separated "v1," signatures. let valid = false; for (const entry of signatures.split(' ')) { const [version, value] = entry.split(','); if (version !== 'v1' || !value) { continue; } const candidate = Buffer.from(value, 'base64'); if ( candidate.length === expected.length && timingSafeEqual(candidate, expected) ) { valid = true; } } if (!valid) { throw new WebhookVerificationError('no matching signature'); } return JSON.parse(payload) as AztaWebhookEvent; } ``` ```python tab="Python" """Verify a Standard Webhooks signature on an Azta webhook delivery.""" import base64 import hashlib import hmac import json import time from typing import Mapping class WebhookVerificationError(Exception): pass def verify_webhook( payload: bytes, headers: Mapping[str, str], secret: str, *, tolerance_seconds: int = 300, now: int | None = None, ) -> dict: """Return the parsed event, or raise WebhookVerificationError. Pass the raw request body bytes, not re-serialized JSON. """ if now is None: now = int(time.time()) lower = {name.lower(): value for name, value in headers.items()} webhook_id = lower.get("webhook-id") timestamp = lower.get("webhook-timestamp") signatures = lower.get("webhook-signature") if not webhook_id or not timestamp or not signatures: raise WebhookVerificationError("missing webhook headers") if not secret.startswith("whsec_"): raise WebhookVerificationError("secret must start with whsec_") try: sent = int(timestamp) except ValueError: raise WebhookVerificationError("timestamp outside the tolerance") from None if abs(now - sent) > tolerance_seconds: raise WebhookVerificationError("timestamp outside the tolerance") try: key = base64.b64decode(secret[len("whsec_") :], validate=True) except ValueError: raise WebhookVerificationError("secret is not valid base64") from None content = f"{webhook_id}.{timestamp}.".encode() + payload expected = hmac.new(key, content, hashlib.sha256).digest() # The header holds one or more space-separated "v1," signatures. valid = False for entry in signatures.split(" "): version, _, value = entry.partition(",") if version != "v1" or not value: continue try: candidate = base64.b64decode(value) except ValueError: continue if hmac.compare_digest(candidate, expected): valid = True if not valid: raise WebhookVerificationError("no matching signature") return json.loads(payload) ``` Both are tested in this site's CI against the same vectors. The OpenAI SDK also verifies and parses these webhooks with `client.webhooks.unwrap`; see [Use the OpenAI SDK](/guides/openai-sdk). ## Answer, then fetch the response Answer with a 2xx status within 15 seconds, and do the work afterwards. Then fetch the answer with [`GET /v1/responses/{id}`](/api-reference/getResponse), using `data.id` from the event. ## Deduplicate on `webhook-id` A delivery can arrive more than once, so store each `webhook-id` you have handled and skip it when it comes again. The same event keeps the same `webhook-id` on every retry. ## Retries A 2xx answer is delivered. Any other answer, a timeout after 15 seconds, or a connection error is a failed attempt. A 3xx answer is a failed attempt too: redirects are not followed, so register the final URL. Each wait starts when the previous attempt fails: the first attempt is immediate, then the waits are 5 seconds, 5 minutes, 30 minutes, 2 hours, 5 hours, 10 hours and 10 hours. That is eight attempts in all, the last about 27.6 hours after the first. After the last one the delivery is not tried again; fetch the response with `GET /v1/responses/{id}` if you missed it. The console lists every delivery attempt for an endpoint. Events and deliveries are kept for 90 days. --- # Use Azta in ChatGPT, Claude and coding agents ## The server URL ```text https://mcp.azta.ai/mcp ``` Azta answers legislation questions, and every answer links to the provisions it cites. Azta covers Hungary and 22 other European countries. Sign in with your Azta account when your client asks. Answers need an Azta plan with credits; check yours any time with the `my_account` tool. ## Cursor [Install in Cursor](cursor://anysphere.cursor-deeplink/mcp/install?name=azta&config=eyJ1cmwiOiJodHRwczovL21jcC5henRhLmFpL21jcCJ9) Or add this to `~/.cursor/mcp.json`: ```json { "mcpServers": { "azta": { "url": "https://mcp.azta.ai/mcp" } } } ``` ## VS Code [Install in VS Code](vscode:mcp/install?%7B%22name%22%3A%22azta%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.azta.ai%2Fmcp%22%7D) [Install in VS Code Insiders](vscode-insiders:mcp/install?%7B%22name%22%3A%22azta%22%2C%22type%22%3A%22http%22%2C%22url%22%3A%22https%3A%2F%2Fmcp.azta.ai%2Fmcp%22%7D) Or run this in a terminal: ```bash code --add-mcp '{"name":"azta","type":"http","url":"https://mcp.azta.ai/mcp"}' ``` ## Claude Code ```bash claude mcp add --transport http azta https://mcp.azta.ai/mcp ``` Add `--scope user` to make Azta available in every project: ```bash claude mcp add --transport http --scope user azta https://mcp.azta.ai/mcp ``` Then run `/mcp` inside Claude Code and choose `azta` to sign in. ## Codex ```bash codex mcp add azta --url https://mcp.azta.ai/mcp codex mcp login azta ``` Or add this to `~/.codex/config.toml`: ```toml [mcp_servers.azta] url = "https://mcp.azta.ai/mcp" ``` ## Gemini CLI ```bash gemini mcp add --transport http azta https://mcp.azta.ai/mcp ``` Then sign in from inside Gemini CLI: ```text /mcp auth azta ``` ## Claude 1. In Claude, open Customize, then Connectors. On Team and Enterprise plans an owner adds it under Organization settings, then Connectors. 2. Choose Add, then Add custom connector. 3. Name it Azta and enter the URL `https://mcp.azta.ai/mcp`, then continue. 4. Connect, and sign in with your Azta account when Claude asks. Azta is not listed in the Claude directory yet. Once it is, you can add it from the directory instead. ## ChatGPT 1. In ChatGPT on the web, turn on Developer mode. It is in Settings; depending on your account it sits under Apps (Advanced settings) or under Security. 2. Create an MCP app (under Apps or Plugins, Create MCP App) named Azta, with the MCP server URL `https://mcp.azta.ai/mcp` and OAuth authentication. 3. Sign in with your Azta account when ChatGPT asks. Azta is not listed in the ChatGPT directory yet. Once it is, you can add it from the directory instead. ## Other clients A client that only speaks local servers can reach Azta through `mcp-remote`. In Claude Desktop, add this to the local config file: ```json { "mcpServers": { "azta": { "command": "npx", "args": [ "-y", "mcp-remote@0.15.2", "https://mcp.azta.ai/mcp" ] } } } ``` ## The tools | Tool | What it does | | ------------ | ------------------------ | | `ask` | Ask Azta | | `get_answer` | Check an Azta answer | | `search` | Search legislation | | `fetch` | Read a provision | | `my_account` | Azta account and credits | ## How Azta keeps you in control - **Every numbered source links to its text.** A legal source has an official or viewer URL, so the user checks the law itself; a web source links to the page it came from. A reference Azta cannot link stays unnumbered. - **An AI disclosure on every answer.** It says Azta's AI wrote the answer from the listed sources, that AI can make mistakes, and that the user should check the cited provisions. - **A professional note on every answer.** It tells the user to consult a qualified adviser before a decision with legal, tax or financial consequences. - **Nothing changes outside Azta.** No tool files, sends, signs or changes anything outside Azta. `ask` saves the conversation to the user's own Azta history and `get_answer` adds the finished answer to it; `ask`, `search` and `fetch` use the user's Azta credits. ## Manage access See or revoke the apps connected to your Azta account at [Connected apps](https://platform.azta.ai/settings/connected-apps). ## Troubleshooting - A sign-in loop: remove the Azta server from your client and add it again. - `unavailable` during rollout: Azta is not yet open to your account. - `503`: retry in a moment. Still stuck? Email [support@azta.ai](mailto:support@azta.ai). --- # 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 => { 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 => { 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 => { 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] <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. --- # Azta API skill The `azta-api` skill teaches your coding agent the Azta API at `https://api.azta.ai/v1`: authentication and key formats, creating and polling responses, the `Idempotency-Key` rule that prevents double charges, rendering sources and annotations, errors and retries, webhook verification and lookups. It holds a short `SKILL.md` and five references, one of them generated from this site's OpenAPI document. It states no prices. Costs are on [Pricing](/guides/pricing). ## Install with the skills CLI The skills CLI reads this site's discovery index at `https://docs.azta.ai/.well-known/agent-skills/index.json`. Pick your agent. Add `-g` to install for every project instead of the current one. ```bash tab="Claude Code" npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent claude-code ``` ```bash tab="Codex" npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent codex ``` ```bash tab="Cursor" npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent cursor ``` ```bash tab="GitHub Copilot" npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent github-copilot ``` For every project, for example: ```bash npx skills@1.7.4 add https://docs.azta.ai --skill azta-api --agent claude-code -g ``` ## Install by hand The skill is one archive. Extract it into the folder your agent reads skills from. ```bash tab="Claude Code (project)" mkdir -p .claude/skills/azta-api && curl -fsSL https://docs.azta.ai/skills/azta-api.tar.gz | tar -xz -C .claude/skills/azta-api ``` ```bash tab="Claude Code (global)" mkdir -p ~/.claude/skills/azta-api && curl -fsSL https://docs.azta.ai/skills/azta-api.tar.gz | tar -xz -C ~/.claude/skills/azta-api ``` ```bash tab="Codex (project)" mkdir -p .agents/skills/azta-api && curl -fsSL https://docs.azta.ai/skills/azta-api.tar.gz | tar -xz -C .agents/skills/azta-api ``` ```bash tab="Codex (global)" mkdir -p ~/.agents/skills/azta-api && curl -fsSL https://docs.azta.ai/skills/azta-api.tar.gz | tar -xz -C ~/.agents/skills/azta-api ``` ```bash tab="Cursor (project)" mkdir -p .agents/skills/azta-api && curl -fsSL https://docs.azta.ai/skills/azta-api.tar.gz | tar -xz -C .agents/skills/azta-api ``` ```bash tab="Cursor (global)" mkdir -p ~/.cursor/skills/azta-api && curl -fsSL https://docs.azta.ai/skills/azta-api.tar.gz | tar -xz -C ~/.cursor/skills/azta-api ``` ## Check the download `https://docs.azta.ai/.well-known/agent-skills/index.json` lists the skill with a `digest` of the form `sha256:<hex>`, the SHA-256 of the archive. The entry's `url` is relative, so resolve it against the index URL. ```bash curl -fsSL https://docs.azta.ai/skills/azta-api.tar.gz | shasum -a 256 ``` ## What it covers - Keys and the sandbox, and why to start with a sandbox key. - Create, stream, poll and cancel, `depth` and follow-up questions. - `Idempotency-Key` on every `POST`, and what a retry without it costs. - Rendering [sources](/guides/sources) from annotation offsets. - [Errors and limits](/guides/errors) and the retry policy. - [Webhooks](/guides/webhooks) and their signature check. - [Lookups](/guides/lookups) and moving from the [OpenAI SDK](/guides/openai-sdk). --- # Support Email [support@azta.ai](mailto:support@azta.ai). See or revoke the apps connected to your Azta account at [Connected apps](https://platform.azta.ai/settings/connected-apps). Install and sign-in problems: see [Troubleshooting](/mcp#troubleshooting). --- # Trust These pages describe how the Azta API and MCP server handle your data. They summarize facts and link the documents that bind Azta; where a page and a binding document differ, the binding document applies. - [Data processing agreement](/trust/dpa): available on the Enterprise plan, and where the published documents live. - [Data use](/trust/data-use): what Azta and its processors do and do not do with your requests. - [Hosting and processors](/trust/hosting): where data is stored, where models run and where searches go. - [Retention](/trust/retention): how long key runs, webhook deliveries and request logs are kept, and zero data retention on Enterprise. - [Status](/status): live availability of the platform and the API. ## Binding documents - [Privacy policy](https://azta.ai/legal/privacy-policy) - [Terms of service](https://azta.ai/legal/terms-of-service) - [Trust center](https://azta.eu.trust.site) Questions: [support@azta.ai](mailto:support@azta.ai) or the [support page](/support). --- # Data use ## Model training According to the [privacy policy](https://azta.ai/legal/privacy-policy), your questions, uploaded documents and generated answers are not used to train or fine-tune any AI model, neither by Azta Kft. nor by the processors it uses. The policy also states that the model gateway and the document-processing provider run with a zero data retention configuration, so neither input nor output is stored after the answer is generated. The privacy policy is the binding text. Read its section on processors and transfers, and its "No model training" paragraph, in full. ## Processors The current list of processors is in the [trust center](https://azta.eu.trust.site/resources). See [Hosting and processors](/trust/hosting) for the ones that handle an API request. ## Other documents - [Terms of service](https://azta.ai/legal/terms-of-service) - [Data processing agreement](/trust/dpa) - [Retention](/trust/retention) --- # Data processing agreement The privacy policy sets out which document applies to you depending on how you use Azta. For business use, the published documents live in the trust center: - [Azta trust center, Resources](https://azta.eu.trust.site/resources) The same page lists the processors Azta uses, and the privacy policy says registered users are notified in advance of a material change to that list. Read the [privacy policy](https://azta.ai/legal/privacy-policy) for the roles Azta takes (controller or processor) for each kind of use. A data processing agreement is available on the Enterprise plan. To request one, contact [sales@azta.ai](mailto:sales@azta.ai). This page does not restate the agreement. The signed or published text is the only binding version. --- # Hosting and processors ## Where data is stored The privacy policy lists AWS (cloud infrastructure and hosting) in Frankfurt, `eu-central-1`, and Supabase (database, authentication and file storage) in Frankfurt. The production infrastructure configuration points the database pooler at `aws-0-eu-central-1.pooler.supabase.com`, an `eu-central-1` host. ## Where models run and searches go > azta-agentic-core runs its models on OpenRouter's EU route (eu.openrouter.ai, zero data retention), with AWS Bedrock in eu-central-1 as the outage fallback, and sends web searches to Exa (api.exa.ai, US). The direct Anthropic API is not a processor: no agent uses it in production, as of the production build checked on 2026-10-28. ## Processors The first five rows apply to the API and to the MCP server. The last three apply to the MCP server only. | Processor | What it does | Where | EU | Applies to | | ---------------------------------------- | ---------------------------------------------------------------------------------------- | ------------------------- | --------------------------------------------------------------- | ---------- | | AWS | Hosts the application and the engine | Frankfurt, `eu-central-1` | Yes | API, MCP | | Supabase | Database, authentication and file storage | Frankfurt | Yes | API, MCP | | OpenRouter, via eu.openrouter.ai | Runs the language models, zero data retention | EU route | Yes, per the privacy policy. OpenRouter, Inc. is a US company | API, MCP | | AWS Bedrock | Runs the models if the primary route has an outage | `eu-central-1` | Yes | API, MCP | | Exa (api.exa.ai) | Web search, when web search is on for your key or account | United States | No, Exa is a US company and requests go outside the EU | API, MCP | | OpenRouter, global route (openrouter.ai) | Writes the chat title on an MCP turn, with accountiq's own chat-title model | Global route | No, not on the EU route | MCP only | | Cégadat (api.cegadatapi.hu) | Company lookup, when the user has company lookup on | Not stated | Not established here; see the trust center | MCP only | | The MCP host you connect | The client you authorize (OpenAI, Anthropic or another vendor) receives the tool results | Set by that vendor | Depends on the vendor; most client operators are outside the EU | MCP only | Exa processes your request outside the EU when a run uses web search. Web search is switched on for API keys once the account is unlocked. On an MCP turn, accountiq's own chat-title model adds "accountiq's chat-title model on OpenRouter's global route (openrouter.ai, not EU)". The REST API has no title model. ## Binding text The [privacy policy](https://azta.ai/legal/privacy-policy) and the [trust center](https://azta.eu.trust.site/resources) hold the binding list of processors and transfer safeguards. --- # Retention ## API data Key runs, webhook deliveries and request-log rows are kept for 90 days, then purged. Zero data retention is available on the Enterprise plan. To enable it, contact [sales@azta.ai](mailto:sales@azta.ai). ## Other data Retention for account, billing and application data is set by the [privacy policy](https://azta.ai/legal/privacy-policy) and the [data processing agreement](/trust/dpa), which are the binding texts.