Skip to main content
Tungo AI

Tungo SDK for Python

The official Python library for the Tungo Platform API

PyPI CI License: MIT

English · Русский · Қазақша

tungo.kz · Quick start · API reference · Changelog · Issues


The official Python library for the Tungo Platform API (https://api.tungo.kz): the AI accountant, 1C integration, bank statements, e-invoices, tax forms and automations of Tungo AI.

It is generated from the API's OpenAPI contract, so its methods and models always match the contract it was built from.

Requirements

Python 3.10 or newer. The library depends on httpx and pydantic 2 (plus anyio and typing-extensions, which those two already bring in). It ships type information (py.typed) and is checked with mypy --strict and pyright in strict mode.

Installation

pip install tungoai

The import package has the same name: import tungoai.

Quick start

from tungoai import Tungo

with Tungo() as client:  # reads TUNGO_API_KEY from the environment
    companies = client.companies.list()
    for company in companies.data:
        print(company.id, company.name)

    turn = client.ask(companies.data[0].id, "How much VAT is due this quarter?")
    print(turn.answer)

Authentication and configuration

Create one client and share it: it is immutable and safe to use from several threads or tasks. Every option can be passed explicitly; three of them fall back to environment variables.

Option Environment variable Default
API key TUNGO_API_KEY none
Base URL TUNGO_BASE_URL https://api.tungo.kz
Webhook secret TUNGO_WEBHOOK_SECRET none
Timeout 60 s (600 s for non-streaming chat turns)
Maximum retries 2
Default headers, HTTP client none, built-in

The API key is sent only to operations that require it; public endpoints and incoming hooks never receive it, and it never appears in error messages or debug output. Calling an authenticated operation without a key fails locally, before any request is made.

Every method also accepts per-request options: timeout, maximum retries, extra headers, an idempotency key and cancellation.

import os

import httpx

from tungoai import Tungo

http_client = httpx.Client(limits=httpx.Limits(max_connections=10))  # proxies, mTLS, custom transports
client = Tungo(
    api_key=os.environ["TUNGO_API_KEY"],  # default: the TUNGO_API_KEY environment variable
    base_url=os.environ.get("TUNGO_BASE_URL"),  # default: TUNGO_BASE_URL, then https://api.tungo.kz
    webhook_secret=os.environ.get("TUNGO_WEBHOOK_SECRET"),  # default: TUNGO_WEBHOOK_SECRET
    timeout=30.0,  # seconds for a whole ordinary request; default 60
    long_running_timeout=900.0,  # seconds for a non-streaming chat turn; default 600
    max_retries=3,  # default 2
    default_headers={"X-App-Name": "billing"},
    http_client=http_client,
)
company_id = client.companies.list().data[0].id

# Options of one request.
company = client.companies.get(
    company_id,
    request_options={"timeout": 5.0, "max_retries": 0, "headers": {"X-Trace-Id": "trace-1"}},
)

# A copy of the client with other settings; it shares the connection pool.
patient = client.with_options(timeout=120.0)
print(patient.companies.get(company_id).name)

# Status, headers, request id and rate limit of a response.
response = client.companies.with_raw_response.get(company_id)
print(response.status_code, response.request_id, response.rate_limit.remaining)
print(response.parse().name)

client.close()
http_client.close()  # the SDK never closes a client it did not create

Tungo is the synchronous client and AsyncTungo the asynchronous one. Both accept the same options (http_client is an httpx.Client or an httpx.AsyncClient), are immutable after construction and release their connection pool with close() / aclose() or a with block. In asynchronous code a call is cancelled the usual way, by cancelling its task; the timeout stays in force.

timeout covers ordinary calls and long_running_timeout the non-streaming chat turns (chat.create, chat.answer, ask): setting timeout=30 does not cut a chat turn to 30 s. A timeout in request_options wins over both. A malformed API key (a line break, a space, a control or non-ASCII character inside it) is refused when the client is created, with a ConfigError that does not show the key.

Errors

Every failure is a subtype of the SDK's base error. An HTTP error response becomes an API error chosen by the error.type field of the response body, falling back to the HTTP status when the body is not the API's error envelope:

error.type Status
invalid_request_error 400
authentication_error 401
payment_required_error 402
permission_error 403
not_found_error 404
conflict_error 409
gone_error 410
limit_error 429
api_error 500
unavailable_error 503

An API error carries the status, type, code, message, request id, field errors, the parsed retry-after delay, the response headers and the raw body. Problems that happen without a response have their own types: connection error, timeout, decode error (a successful response whose body is not valid), and configuration or validation error (raised before any request).

import tungoai
from tungoai import Tungo

client = Tungo()
try:
    client.companies.get("co_missing")
except tungoai.NotFoundError as error:
    print(error.status, error.type, error.code, error.message, error.request_id)
except tungoai.InvalidRequestError as error:
    for item in error.fields:
        print(item.path, item.code, item.message)
except tungoai.LimitError as error:
    print("retry after", error.retry_after, "s; the limit resets at", error.extra.get("resets_at"))
except tungoai.APIError as error:  # any other error response
    print(error.status, error.headers.get("x-request-id"), error.body)
except tungoai.APITimeoutError:
    print("the request timed out")
except tungoai.APIConnectionError as error:
    print("network problem:", error.__cause__)
except tungoai.DecodeError as error:
    print("unexpected response body:", error.body)
except tungoai.ConfigError as error:  # raised before any request, e.g. no API key
    print(error)
finally:
    client.close()

Every error derives from tungoai.TungoError. APITimeoutError and WaitTimeoutError also derive from the built-in TimeoutError; RequestValidationError and MoneyError from ValueError. The original exception, when there is one, is available as __cause__.

An error that a chat turn reports inside a successful response (an error event of a stream, or errors of ask) is raised by the helpers that collect a turn as ChatStreamError. It is not an APIError and has no HTTP status; its type is the same error.type (limit_error, api_error, ...), next to code, message, request_id, extra, and result or turn with what was received. The TypeScript SDK throws the APIError subclass of that error.type (for example LimitError) with an undefined status for the same situation.

Retries and timeouts

A request is retried up to 2 times (3 attempts) on connection errors, timeouts and the statuses 408, 429, 500, 502, 503 and 504, but only when repeating it is safe: GET, HEAD, PUT and DELETE requests, and POST or PATCH requests that carry an Idempotency-Key. For the operations whose contract declares that header the SDK generates a random key and reuses it for every attempt; you can pass your own.

The delay between attempts is min(0.5 s × 2^attempt, 8 s) with random jitter. A positive retry-after-ms or Retry-After response header overrides it; if the server asks for more than 60 s the SDK does not wait and raises the error with the delay filled in.

The timeout covers the whole request including reading the body. Streaming calls have no total timeout, only a connect timeout and an idle timeout of 120 s. Cancellation supplied by the caller is combined with the timeout.

from tungoai import Tungo

client = Tungo(max_retries=5, timeout=20.0)  # for every request of this client
company_id = client.companies.list().data[0].id
proposal_id = client.proposals.list(company_id).data[0].id

# For one request. approve declares Idempotency-Key, so the SDK would generate a key anyway;
# pass your own to make a repeated call from your side safe too.
approved = client.proposals.approve(
    company_id,
    proposal_id,
    note="Checked against the contract",
    request_options={"idempotency_key": f"approve-{proposal_id}", "max_retries": 1, "timeout": 10.0},
)
print(approved.to_dict())

# A POST that does not declare the header is never retried unless you give it a key.
client.proposals.reject(
    company_id, proposal_id, reason="Duplicate", request_options={"idempotency_key": f"reject-{proposal_id}"}
)

client.close()

A request is retried only when repeating it is safe: GET, HEAD, PUT and DELETE, or any request that carries an Idempotency-Key (generated for the operations that declare it, passed as idempotency_key, or put into the headers by you). A Retry-After of zero, or a date that has already passed, does not trigger an immediate retry: the computed backoff applies.

The timeout is one deadline for the whole request. The asynchronous client enforces it exactly. The synchronous client gives each phase (connecting, sending, waiting for the headers, reading the body) only the time that is left and checks the deadline after every chunk of the body; a server that goes silent in the middle of a body is cut off when the time that was left at the start of the body runs out.

Pagination

List operations use cursor pagination ({ data, next_cursor }). The plain method returns one typed page that knows whether there is a next page and can fetch it. The auto-paging variant is a lazy iterator: it requests the next page only when needed, carries your filters over, and stops fetching as soon as you stop iterating.

from tungoai import Tungo

client = Tungo()
company_id = client.companies.list().data[0].id

# One page: the items, the cursor and the whole typed response.
page = client.proposals.list(company_id, status="awaiting_approval", limit=50)
print(len(page.data), page.next_cursor, page.body.total)
if page.has_next_page():
    page = page.get_next_page()  # same filters, next cursor

# Every item: iterating a page requests the following pages only when they are needed.
for proposal in client.proposals.list(company_id, status="awaiting_approval", limit=50):
    print(proposal.id, proposal.title, proposal.total.formatted)
    if proposal.blocker_count > 0:
        break  # no further page is requested

client.close()

A cursor that the server has already returned in the same walk (the previous one or any earlier one) raises PaginationError instead of looping forever.

Streaming

Streaming operations use server-sent events and deliver typed events as they arrive. Leaving the loop early or closing the stream closes the connection. A stream request is retried only before its first byte; an error response is raised as the same typed API error as for ordinary calls. A chat stream that ends before its final event is reported as an error, never as a finished answer.

from tungoai import ChatStreamError, DecodeError, Tungo
from tungoai.types import ChatEventDone, ChatEventText, ChatEventTool

client = Tungo()
company_id = client.companies.list().data[0].id

# Events as they arrive. Leaving the "with" block closes the connection.
with client.chat.create_stream(company_id, message="How much VAT is due this quarter?") as stream:
    print("dialog", stream.dialog_id, "turn", stream.turn_no)
    for event in stream:
        if isinstance(event, ChatEventText):
            print(event.delta, end="", flush=True)
        elif isinstance(event, ChatEventTool):
            print(f"\n[{event.name}: {event.status}]")
        elif isinstance(event, ChatEventDone):
            print("\nstop reason:", event.stop_reason)

# Or let the SDK collect the turn. An "error" event of the turn is raised as ChatStreamError,
# and a stream that was cut before its "done" event as DecodeError: never a half answer.
try:
    result = client.chat.create_stream(company_id, message="And for the year?").get_final_result()
    print(result.text, result.stop_reason, result.cost_tiyn)
except ChatStreamError as error:
    print(error.type, error.code, error.message, error.request_id)
except DecodeError as error:
    print("the stream was cut:", error.result.text if error.result else "")

client.close()

Every streaming operation of the API is available: chat.create_stream, chat.answer_stream, chat.list_events, feed.list_events and batches.list_events. Events are typed models; an event type this version does not know arrives as UnknownVariant, and heart-beats are skipped. stream.iter_sse() gives the raw server-sent events. request_options={"stream_idle_timeout": 300} changes the idle timeout of one call.

Leaving a for loop over a stream early (break, return, an exception) closes the connection: at once on CPython with the synchronous client, at the next step of the event loop with the asynchronous one. A with block, or close() / aclose(), makes the moment explicit on every interpreter. If you pull events with next(stream) instead of a loop, close the stream yourself.

get_final_result() never returns a half answer: a stream that ends without its done event raises DecodeError (error.result holds what arrived). A successful answer to a streaming call that is not text/event-stream raises DecodeError too, instead of looking like an empty stream. Plain iteration simply ends when the stream ends.

Webhooks

Outgoing webhooks are signed according to the Standard Webhooks specification (Svix). The verify helper takes the raw request body and the headers (webhook-* or svix-*), checks the HMAC-SHA256 signature in constant time and the timestamp within 300 s of the system clock, and returns the typed event. Any problem is reported as a webhook verification error. If no secret is passed, the client's webhook secret is used.

The send-hook helper posts an event to an incoming hook (/hooks/{token}) and, when given a secret, signs the exact bytes it sends in the X-Signature: sha256=<hex> header. The API key is never sent to hooks.

from collections.abc import Mapping

import tungoai
from tungoai import Tungo, WebhookVerificationError
from tungoai.types import WebhookEventProposalWritten

client = Tungo()  # the webhook secret comes from TUNGO_WEBHOOK_SECRET, or pass webhook_secret=...


def handle_webhook(raw_body: bytes, headers: Mapping[str, str]) -> int:
    """Call it from your HTTP handler with the raw request body and the request headers.

    Returns the HTTP status to answer with.
    """
    try:
        event = client.verify_webhook(raw_body, headers)
    except WebhookVerificationError as error:
        print("rejected:", error.reason)
        return 400
    if isinstance(event, WebhookEventProposalWritten):
        print("written to 1C:", event.data.proposal_id)
    return 204


# Send an event to an incoming hook. With a secret the exact bytes sent are signed (X-Signature).
accepted = client.send_hook(
    "th_example",
    {"id": "order_1532", "amount_tiyn": 4_500_000},
    secret="hksec_example",
    event_id="order_1532",
)
print(accepted.to_dict())

# Check a hook signature yourself (for example in a test double of the hook endpoint).
body = b'{"id":"order_1532"}'
assert tungoai.verify_hook_signature(body, tungoai.hook_signature_header(body, "hksec_example"), "hksec_example")

tungoai.verify_webhook(body, headers, secret) is the same check without a client. The reason of a failure is in WebhookVerificationError.reason. client.automations.post_hook and client.send_hook are the same call.

Money

The API expresses amounts in tiyn (1/100 of a tenge) as 64-bit integers. The money helper converts decimal strings and exact decimal values to tiyn without floating-point error, rounding half away from zero ("1.005" → 101), and formats tiyn for display (4500000 → 45 000,00 ₸). Invalid input raises a typed error.

from decimal import Decimal

from tungoai import MoneyError, TungoMoney, is_valid_bin_iin

print(TungoMoney.to_tiyn("45000.50"))  # 4500050
print(TungoMoney.to_tiyn(Decimal("1.005")))  # 101: half away from zero, no float error
print(TungoMoney.to_tiyn(1.005))  # 101: a float goes through its shortest decimal form
print(TungoMoney.format_tiyn(4_500_000))  # 45 000,00 ₸
print(TungoMoney.format_tiyn(-50, "USD"))  # -0,50 $

try:
    TungoMoney.to_tiyn("12,5")
except MoneyError as error:
    print(error)

print(is_valid_bin_iin("990140000017"))  # True

TungoMoney.to_tiyn accepts str, decimal.Decimal, int (whole tenge) and float. Tiyn are 64-bit integers in the API: format_tiyn raises MoneyError for a value outside that range, like every Tungo SDK.

Async usage

import asyncio

from tungoai import AsyncTungo
from tungoai.types import ChatEventText


async def main() -> None:
    async with AsyncTungo() as client:
        companies = await client.companies.list()
        company_id = companies.data[0].id

        proposals = await client.proposals.list(company_id, limit=50)
        async for proposal in proposals:  # requests the following pages as needed
            print(proposal.id, proposal.title)

        async with await client.chat.create_stream(company_id, message="What is new today?") as stream:
            async for event in stream:
                if isinstance(event, ChatEventText):
                    print(event.delta, end="")


asyncio.run(main())

Types

Responses are pydantic models from tungoai.types: read fields as attributes, model.to_dict() returns the JSON object as the API sent it. Models are forward compatible: unknown fields are kept in model.model_extra, enumerations accept values added later, and a tagged union (for example ChatEvent or WebhookEvent) yields UnknownVariant for a tag this version does not know, so narrow with isinstance. A field that is absent from the response and one that is null are both None; model.model_fields_set tells them apart.

Responses are decoded leniently, requests strictly. If a response does not match the contract (a field that must not be null is null, a field changed its type), the SDK does not fail the call: the model is built without validation, every part that does match is still a typed model, the part that does not is left as the API sent it, and to_dict() returns the original JSON. So a value can differ from its annotation after such a drift; the tungoai logger reports it at DEBUG level. The same holds for the events of a stream: one odd event does not end the turn. A body that is not JSON, or JSON of the wrong kind altogether (an array where an object is expected), still raises DecodeError.

Request parameters are keyword arguments. Leave a parameter out to not send it; pass None to send JSON null where the API allows it. Nested objects are plain dictionaries typed by tungoai.types.params.

To call an endpoint this version does not cover, build a tungoai.RequestSpec and pass it to client.request.

Helpers

client.ask sends one chat message and waits for the whole turn. client.upload_bank_statement(company_id, file_name, content) takes the bytes of the file, or a string: the base64 of the file by default, or the statement text itself with text=True (the same rule as in the TypeScript SDK). client.wait_for_run, client.run_scenario_and_wait and client.wait_for_batch poll with a pause that grows from 1 s to 5 s until the run or job is final; they raise WaitTimeoutError after timeout seconds (10 minutes by default).

Logging and secrets

The SDK logs to the tungoai logger at DEBUG level: the method, the path, the status and the request id, never header values or the API key, and the path of an incoming hook is logged as /hooks/***.

httpx, the HTTP library underneath, writes its own line for every request to the httpx logger at INFO level, with the full URL. The URL of an incoming hook contains its token, which is a secret. For the requests the SDK sends, it replaces that URL with /hooks/*** in httpx's line (a filter on the httpx logger that touches nothing else). Anything else that records URLs on its own (a custom transport, a proxy, an APM agent) is outside the SDK's reach: treat hook URLs as secrets there.

API reference

Generated from the Tungo Platform API contract: 13 resources, 82 methods, 22 webhook event types.

Resource Description Methods
client.workspaces Organisations (workspaces) the API key can access list
client.companies Companies of an organisation and their profiles list · get
client.platform Platform balance, ledger, usage reports, limits, audit log and model rates list_usage_rates · get_account · get_audit · get_ledger · list_limits · list_requests · get_usage
client.api_keys The current API key and its usage statistics get_current · get_current_usage · get_usage
client.webhooks Outgoing webhooks of an organisation (Svix): endpoints, event catalogue, deliveries and signature check list_endpoints · create_endpoint · get_endpoint · update_endpoint · delete_endpoint · list_endpoint_attempts · rotate_endpoint_secret · test_endpoint · list_event_types · list_messages · list_message_attempts · resend_message · get_usage
client.chat Conversations with the AI accountant: questions, streaming chat (SSE), token counting and history list · create · create_stream · count_tokens · get · update · delete · answer · answer_stream · list_events · stop
client.proposals Documents proposed by the AI accountant: review, approval for posting to 1C, and rejection list · approve_batch · get · approve · reject
client.feed The company's feed of events and proposals (pages and a live SSE stream) get · list_events
client.batches Processing batches for bank statements, incoming e-invoices and DirectBank list · create_bank_statement · create_esf_purchases · get · list_events · stop · get_summary · list_statements · poll_directbank
client.esf Incoming and outgoing electronic invoices (the ESF system) get_incoming · get_outgoing
client.forms Tax forms (FNO 100.00 and others): building, ISNA checks and data gaps list · build · list_data_gaps · get_isna · list_notes
client.onec Status of the company's connection to its 1C database get_status
client.automations Company automations: incoming hooks, scenarios, step chains, runs and the AI autonomy level post_hook · list_hooks · create_hook · get_hook · update_hook · delete_hook · rotate_hook · test_hook · list_runs · get_run · cancel_run · retry_run · list_scenarios · create_scenario · get_scenario · put_scenario · update_scenario · delete_scenario · run_scenario · get_state · put_state · get_autonomy · put_autonomy

Other SDKs

The same API is available in ten languages. All SDKs are generated from one contract and behave the same way.

Language Repository Package
TypeScript tungo-sdk-typescript @tungoai/sdk
Python tungo-sdk-python tungoai
Go tungo-sdk-go github.com/TungoAI/tungo-sdk-go
Rust tungo-sdk-rust tungo-sdk
PHP tungo-sdk-php tungo/sdk
Java tungo-sdk-java kz.tungo:tungo-sdk
Kotlin tungo-sdk-kotlin kz.tungo:tungo-sdk-kotlin
Swift tungo-sdk-swift TungoSDK
Ruby tungo-sdk-ruby tungo-sdk
.NET tungo-sdk-dotnet Tungo.Sdk

Versioning and support

This library follows Semantic Versioning. While the major version is 0, a minor release may contain breaking changes; they are always listed in the changelog. This README describes version 0.2.0.

The code is generated: please report problems and ideas in the issue tracker rather than opening pull requests against generated files. To report a vulnerability, follow the security policy.

License

MIT

Metadata

Release files for tungoai 0.2.0

For a detailed explanation of source distributions (sdists) and built distributions (wheels), please see the package formats documentation.

Source distribution (sdist)

Source distribution for tungoai 0.2.0
File Size Uploaded Provenance Actions
tungoai-0.2.0.tar.gz 287.2 kB Without provenance Details for tungoai-0.2.0.tar.gz

Built distribution (wheel)

Table of built distributions (wheels) for tungoai 0.2.0
File Interpreter ABI Platform Actions
tungoai-0.2.0-py3-none-any.whl Python 3 none any Details for tungoai-0.2.0-py3-none-any.whl

Total release size: 516.6 kB

Release files / tungoai-0.2.0.tar.gz

Download URL tungoai-0.2.0.tar.gz
Size 287.2 kB
Tags Source
SHA-256 checksum
How to use checksums
a8cef05d312c0227613b538b6eb011e43211331eef611258e4df17b048f1f39f
BLAKE2b-256 checksum
How to use checksums
e97db96fbd296367715d1113c3b7475777cc587e450d7717be0120b084b3759b
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Release files / tungoai-0.2.0-py3-none-any.whl

Download URL tungoai-0.2.0-py3-none-any.whl
Size 229.4 kB
Tags Python 3
SHA-256 checksum
How to use checksums
b7a0b8e97fba40ae20a09438aec8812f6bff26ba4d82c24d1274833d7b57166a
BLAKE2b-256 checksum
How to use checksums
80dc48a5df4bb8745a5de3569853f4798eea6fbda270cfe93c8dc64596661e74
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
No
Uploaded via twine/7.0.0 CPython/3.13.16

Security for tungoai 0.2.0

Provenance

Provenance describes where a file came from. On PyPI, provenance is shared via attestations, which provide a verifiable record of the build or publishing details. View details, limitations and caveats.

No provenance found

None of the files in this release were uploaded with provenance metadata.

A note on package security

PyPI is a repository for distributing packages, not a service for auditing them. While third-party security researchers and our staff do their best to detect and remove malicious packages, PyPI cannot guarantee the quality, security or suitability of any given package.

To learn more about protecting yourself from malicious actors, visit our package security documentation.

Release history Release notifications | RSS feed

This release

0.2.0 This release

2 release files No provenance

Anthropic, PBC Visionary sponsor Bloomberg Visionary sponsor Hudson River Trading Visionary sponsor Meta Visionary sponsor NVIDIA Visionary sponsor Microsoft Sustainability sponsor Depot Continuous Integration AWS Cloud computing and Security Sponsor Datadog Monitoring Fastly CDN Google Download Analytics Sentry Error logging StatusPage Status page