{"openapi":"3.1.0","info":{"title":"Taxxa API","version":"1","description":"Search the Nordic tax and accounting knowledge base behind Taxxa, or get cited answers from it, with citations to official sources.\n\n## Authentication\n\nSend your API key as a bearer token. Workspace admins create keys in Taxxa under Control Room > API.\n\n```bash\ncurl https://api.taxxa.ai/v1/... \\\n  -H \"Authorization: Bearer $TAXXA_API_KEY\"\n```\n\nKeep keys on your server. Anyone with a key can use your workspace's API access. A revoked key stops working within two minutes.\n\n## Quickstart\n\nSearch the knowledge base. Every result carries the full passage and the official URL to cite.\n\n```bash\ncurl https://api.taxxa.ai/v1/search \\\n  -H \"Authorization: Bearer $TAXXA_API_KEY\" \\\n  -H \"Content-Type: application/json\" \\\n  -d '{\"query\": \"arvonlisäveron alarajahuojennus\", \"country\": \"FI\", \"top_k\": 5}'\n```\n\n## Filters\n\n`GET /v1/countries` lists the countries you can search. `GET /v1/facets?country=FI` lists that country's sources and categories. Copy the `ref` of each category you want into `filters`:\n\n```json\n{\n  \"query\": \"alarajahuojennus\",\n  \"country\": \"FI\",\n  \"filters\": [{ \"source_id\": \"vero\", \"category_id\": \"ohjeet\" }]\n}\n```\n\nWithout `filters`, the search uses the country's default sources. An unknown ref returns `400` with code `invalid_filter`; an unknown country returns `400` with code `invalid_country` and the supported list.\n\n## Legal date\n\nIn countries with layered retrieval (Finland and France), `as_of` (YYYY-MM-DD) gives the rules in force on that date: statutes and collective agreements as they stood then. Send it as `as_of` in a search, or as `taxxa.as_of` in an answer. Without it, a search uses today, and an answer uses the period the question names, or today when it names none. In other countries `as_of` returns `400` with code `invalid_as_of` and the countries that accept it.\n\n## Answers\n\n`POST /v1/chat/completions` answers from the knowledge base in the OpenAI Chat Completions format, so the official OpenAI SDKs work with `baseURL` set to `https://api.taxxa.ai/v1`. The models are `taxxa-fast` and `taxxa-thinking` (`GET /v1/models`).\n\n```ts\nimport OpenAI from 'openai';\n\nconst client = new OpenAI({ apiKey: process.env.TAXXA_API_KEY, baseURL: 'https://api.taxxa.ai/v1' });\nconst answer = await client.chat.completions.create({\n  model: 'taxxa-fast',\n  messages: [{ role: 'user', content: 'What is the general VAT rate in Finland?' }],\n});\n```\n\nEach `[n]` in the answer is `citations[n-1]`, a source URL. `search_results` gives the title, source and date of each citation. When streaming, both arrive in the chunk with `finish_reason`, and `stream_options.include_usage` adds a last chunk with `usage` and empty `choices`.\n\nThe answer uses only the public knowledge base: no web search and no workspace documents. It can also look up exchange rates from official institutions (ECB unless the question names another), when your workspace has exchange rates on. The API keeps no messages or answers.\n\nSupported fields: `model`, `messages` (text only; roles `system`, `developer`, `user`, `assistant`; the last message from the user), `stream`, `stream_options.include_usage`, `temperature`, `max_tokens` or `max_completion_tokens` (a cap on each model step), `user` (ignored) and `taxxa`:\n\n| Field | Meaning |\n|---|---|\n| `taxxa.country` | Country to answer for. Defaults to your workspace country, so send it when the workspace has none. |\n| `taxxa.language` | Answer language. Defaults to the language of the question. |\n| `taxxa.sources` | Refs from `GET /v1/facets`, as in search filters. |\n| `taxxa.as_of` | Legal date of the answer (YYYY-MM-DD). See Legal date. |\n\nOther fields, such as `tools` or image content, return `400 unsupported_parameter` with the field in `param`. With the OpenAI SDKs, send `taxxa` in the request body (`extra_body` in Python).\n\n## Errors\n\nEvery error has the same JSON body, so OpenAI SDKs read it unchanged.\n\n```json\n{\n  \"error\": {\n    \"message\": \"The API key is not valid.\",\n    \"type\": \"authentication_error\",\n    \"param\": null,\n    \"code\": \"invalid_api_key\"\n  }\n}\n```\n\n| Status | Type | When |\n|---|---|---|\n| 400 | `invalid_request_error` | The request does not match the schema. `param` names the field. |\n| 401 | `authentication_error` | The key is missing, revoked or not valid. |\n| 403 | `permission_error` | API access is not active for the workspace. |\n| 404 | `invalid_request_error` | The path does not exist. |\n| 429 | `rate_limit_error` | A rate limit or the spend cap was reached. |\n| 500 | `api_error` | Something failed on our side. Retry with backoff. |\n| 503 | `api_error` | Limits could not be checked. Retry with backoff. |\n\n## Limits\n\nEach key has a limit of 60 requests a minute, and each workspace has a limit of 300 requests a minute for all its keys. Each workspace also has a limit on requests open at the same time. A workspace can also have a monthly spend cap. When you reach one, the API returns `429` with a `Retry-After` header in seconds and one of these codes:\n\n| Code | Meaning |\n|---|---|\n| `rate_limit_exceeded` | The key or the workspace reached its per-minute limit. |\n| `concurrency_limit_exceeded` | Too many requests of the workspace are open. |\n| `spend_cap_reached` | The workspace reached its monthly spend cap. It resets at the start of the next month (UTC). |\n\nRequests that fail with an error are not billed.\n\n## Versions\n\nAll endpoints are under `/v1`. Changes inside `v1` only add fields; they never remove or rename them.\n\n## Legal\n\nUse of the API is under these terms:\n\n- [Developer API Terms](https://www.taxxa.ai/legal/developer-api-terms): keys, permitted use, prices, limits and AI output.\n- [Acceptable Use Policy](https://www.taxxa.ai/legal/developer-api-acceptable-use): what you must not do with the API.\n- [Data Processing Addendum](https://www.taxxa.ai/legal/developer-api-data-processing-addendum): personal data, storage and sub-processors.\n\nAnswers are not legal or tax advice and can be wrong. Show the citations with every answer that you show to your users."},"servers":[{"url":"/"}],"components":{"securitySchemes":{"bearerAuth":{"type":"http","scheme":"bearer"}},"schemas":{"Error":{"type":"object","properties":{"error":{"type":"object","properties":{"message":{"type":"string"},"type":{"type":"string","enum":["invalid_request_error","authentication_error","permission_error","rate_limit_error","api_error"]},"param":{"type":["string","null"]},"code":{"type":"string"}},"required":["message","type","param","code"]}},"required":["error"]},"SearchResponse":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"$ref":"#/components/schemas/SearchResult"}}},"required":["object","data"]},"SearchResult":{"type":"object","properties":{"id":{"type":"string"},"title":{"type":"string"},"url":{"type":"string"},"source":{"type":"string"},"country":{"type":"string"},"language":{"type":"string"},"document_type":{"type":["string","null"]},"published_at":{"type":["string","null"]},"valid_since":{"type":["string","null"]},"valid_until":{"type":["string","null"]},"score":{"type":["number","null"],"description":"Rerank relevance from 0 to 1, or null when the search was not reranked."},"content":{"type":"string"}},"required":["id","title","url","source","country","language","document_type","published_at","valid_since","valid_until","score","content"]},"SearchRequest":{"type":"object","properties":{"query":{"type":"string","minLength":1,"maxLength":2000},"country":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 code from `GET /v1/countries`.","example":"FI"},"language":{"type":"string","minLength":2,"maxLength":8,"description":"Language of the query. Defaults to the country language."},"filters":{"type":"array","items":{"$ref":"#/components/schemas/FilterRef"},"maxItems":200,"description":"Refs from `GET /v1/facets`. Omit for the country defaults."},"top_k":{"type":"integer","minimum":1,"maximum":50,"default":10},"as_of":{"type":"string","format":"date","description":"Legal date (YYYY-MM-DD): the answer uses the rules in force on this date. Only for countries with layered retrieval; elsewhere it returns `400 invalid_as_of`.","example":"2025-06-30"}},"required":["query","country"],"additionalProperties":false},"FilterRef":{"type":"object","properties":{"source_id":{"type":"string","minLength":1},"category_id":{"type":"string","minLength":1}},"required":["source_id","category_id"],"additionalProperties":false},"CountriesResponse":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"code":{"type":"string"},"name":{"type":"string"}},"required":["code","name"]}}},"required":["object","data"]},"FacetsResponse":{"type":"object","properties":{"country":{"type":"string"},"sources":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"categories":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"label":{"type":"string"},"ref":{"$ref":"#/components/schemas/FilterRef"}},"required":["id","label","ref"]}}},"required":["id","label","categories"]}}},"required":["country","sources"]},"ModelsResponse":{"type":"object","properties":{"object":{"type":"string","enum":["list"]},"data":{"type":"array","items":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["model"]},"created":{"type":"number"},"owned_by":{"type":"string"}},"required":["id","object","created","owned_by"]}}},"required":["object","data"]},"ChatCompletion":{"type":"object","properties":{"id":{"type":"string"},"object":{"type":"string","enum":["chat.completion"]},"created":{"type":"number"},"model":{"type":"string"},"choices":{"type":"array","items":{"type":"object","properties":{"index":{"type":"number"},"message":{"type":"object","properties":{"role":{"type":"string","enum":["assistant"]},"content":{"type":"string"}},"required":["role","content"]},"finish_reason":{"type":"string"}},"required":["index","message","finish_reason"]}},"usage":{"type":"object","properties":{"prompt_tokens":{"type":"number"},"completion_tokens":{"type":"number"},"total_tokens":{"type":"number"}},"required":["prompt_tokens","completion_tokens","total_tokens"]},"citations":{"type":"array","items":{"type":"string"},"description":"Source URLs; `[n]` in the text is `citations[n-1]`."},"search_results":{"type":"array","items":{"type":"object","properties":{"title":{"type":"string"},"url":{"type":"string"},"source":{"type":["string","null"]},"date":{"type":["string","null"]}},"required":["title","url","source","date"]}}},"required":["id","object","created","model","choices","usage","citations","search_results"]},"ChatCompletionRequest":{"type":"object","properties":{"model":{"type":"string","example":"taxxa-fast"},"messages":{"type":"array","items":{"type":"object","properties":{"role":{"type":"string"},"content":{"anyOf":[{"type":"string"},{"type":"array","items":{"type":"object","properties":{"type":{"type":"string"},"text":{"type":"string"}},"required":["type"],"additionalProperties":{}}},{"type":"null"}]}},"required":["role"],"additionalProperties":{}},"minItems":1,"maxItems":100},"stream":{"type":["boolean","null"]},"stream_options":{"type":["object","null"],"properties":{"include_usage":{"type":["boolean","null"]}}},"temperature":{"type":["number","null"],"minimum":0,"maximum":2},"max_tokens":{"type":["integer","null"],"minimum":1},"max_completion_tokens":{"type":["integer","null"],"minimum":1},"user":{"type":["string","null"],"description":"Accepted and ignored."},"taxxa":{"type":"object","properties":{"country":{"type":"string","minLength":2,"maxLength":2,"description":"Defaults to the workspace country.","example":"FI"},"language":{"type":"string","minLength":2,"maxLength":32,"description":"Answer language. Defaults to the question language."},"sources":{"type":"array","items":{"$ref":"#/components/schemas/FilterRef"},"maxItems":200,"description":"Refs from `GET /v1/facets`."},"as_of":{"type":"string","format":"date","description":"Legal date (YYYY-MM-DD): the answer uses the rules in force on this date. Only for countries with layered retrieval; elsewhere it returns `400 invalid_as_of`.","example":"2025-06-30"}},"additionalProperties":false}},"required":["model","messages"],"additionalProperties":{}}},"parameters":{}},"paths":{"/v1/search":{"post":{"tags":["Knowledge base"],"summary":"Search the knowledge base","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchRequest"}}}},"responses":{"200":{"description":"Matching passages with provenance.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/SearchResponse"}}}},"400":{"description":"The request is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The API key is missing or not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API access is not active for the workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate, concurrency or spend limit was reached. Retry after the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Limits could not be checked. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/countries":{"get":{"tags":["Knowledge base"],"summary":"List knowledge-base countries","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Countries you can search.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/CountriesResponse"}}}},"401":{"description":"The API key is missing or not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API access is not active for the workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/facets":{"get":{"tags":["Knowledge base"],"summary":"List sources and categories of a country","security":[{"bearerAuth":[]}],"parameters":[{"schema":{"type":"string","minLength":2,"maxLength":2,"description":"ISO 3166-1 alpha-2 code from `GET /v1/countries`.","example":"FI"},"required":true,"description":"ISO 3166-1 alpha-2 code from `GET /v1/countries`.","name":"country","in":"query"}],"responses":{"200":{"description":"Sources and categories, with the refs `filters` accepts.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/FacetsResponse"}}}},"400":{"description":"The request is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The API key is missing or not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API access is not active for the workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate, concurrency or spend limit was reached. Retry after the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Limits could not be checked. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/models":{"get":{"tags":["Answers"],"summary":"List models","security":[{"bearerAuth":[]}],"responses":{"200":{"description":"Models you can answer with.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ModelsResponse"}}}},"400":{"description":"The request is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The API key is missing or not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API access is not active for the workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate, concurrency or spend limit was reached. Retry after the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Limits could not be checked. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}},"/v1/chat/completions":{"post":{"tags":["Answers"],"summary":"Answer from the knowledge base (OpenAI Chat Completions)","security":[{"bearerAuth":[]}],"requestBody":{"required":true,"content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCompletionRequest"}}}},"responses":{"200":{"description":"A cited answer, or `chat.completion.chunk` server-sent events when `stream` is true.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/ChatCompletion"}},"text/event-stream":{"schema":{"type":"string"}}}},"400":{"description":"The request is not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"401":{"description":"The API key is missing or not valid.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"403":{"description":"API access is not active for the workspace.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"429":{"description":"A rate, concurrency or spend limit was reached. Retry after the `Retry-After` header.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"500":{"description":"The answer could not be completed.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}},"503":{"description":"Limits could not be checked. Retry with backoff.","content":{"application/json":{"schema":{"$ref":"#/components/schemas/Error"}}}}}}}},"webhooks":{}}