Skip to main content

Tino UserAPI — Python SDK

Zero-dependency client for https://api.tino.vn. Python 3.8+.

pip install tino-userapi

Not on PyPI yet? Install straight from the repository:

pip install "git+https://github.com/tinovn/tino-sdk.git#subdirectory=sdk/python"

Log in

from tino_userapi import TinoClient

client = TinoClient()

sent = client.request_login_otp("you@example.com")
print(f"Code sent via {sent['via']}")          # 'zalo' or 'email'

client.verify_login_otp("you@example.com", input("Code: "))
print(client.account.get_details()["client"]["email"])

Password login works too, and so does resuming a stored session:

client.login("you@example.com", password, remember=True)

client = TinoClient(token=stored_token, refresh_token=stored_refresh)

Persist rotating tokens

A refresh invalidates both the old refresh token and the old access token, so store whatever the callback hands you:

def persist(token: str, refresh: str) -> None:
    keyring.set_password("tino", "access", token)
    keyring.set_password("tino", "refresh", refresh)

client = TinoClient(
    token=keyring.get_password("tino", "access"),
    refresh_token=keyring.get_password("tino", "refresh"),
    on_token_refresh=persist,
)

When a call fails with unauthorized / token_expired, the client refreshes once and replays the request. If the refresh also fails it raises TinoAuthError.

Call the API

Every resource group is an attribute; method names are snake_case.

client.billing.get_balance()
client.billing.list_invoices(page=0, perpage=25, orderby="date|DESC")
client.domains.list()
client.domains.get(200001)
client.services.list(filters={"hide_cancelled": 1})
client.dns.create_record(service_id, zone_id, name="www", type="A", content="203.0.113.10")
client.support.create_ticket(dept_id=2, subject="Hello", body="…")
client.vms.reboot(service_id, vm_id)

Rules that hold everywhere:

  • Path parameters are positional, in the order they appear in the URL, and are percent-encoded for you.

  • Query parameters are keyword-only. Documented ones are named; anything else goes through **query. Because filter shadows a builtin it is spelled filters.

  • Write operations take a payload, either as keyword fields or as payload={...}. The explicit argument is called payload, not body, so it never shadows an API field of that name — POST /tickets really does have a field called body:

    client.support.create_ticket(dept_id=2, subject="Hi", body="the message")
    client.support.create_ticket(payload={"dept_id": 2, "subject": "Hi", "body": "…"})
    
  • Responses are plain dicts and lists — the raw JSON, unmodified. Nothing is re-shaped behind your back.

Handle errors

from tino_userapi import (
    TinoApiError, TinoAuthError, TinoNotFoundError,
    TinoValidationError, TinoTransportError,
)

try:
    client.domains.get(999999)
except TinoNotFoundError:
    ...                                   # unknown route or record
except TinoAuthError:
    ...                                   # re-authenticate
except TinoValidationError as exc:
    print(exc.errors)                     # ['Please enter taxid']
except TinoApiError as exc:
    print(exc.status, exc.errors, exc.path, exc.body)
except TinoTransportError:
    ...                                   # never reached the server

The API reports most failures with HTTP 200 and an error key in the body, so raising on the body — not the status — is the whole point of this layer.

Binary payloads

response = client.request("GET", "/clientarea/downloadprofile", raw=True)
open("profile.pdf", "wb").write(response.content)

Escape hatch

Anything the resource layer does not cover:

client.request("GET", "/some/new/endpoint", query={"page": 0})
client.request("POST", "/some/new/endpoint", body={"field": "value"})

Configuration

Option Default Purpose
base_url https://api.tino.vn Point at a staging deployment
token, refresh_token None Resume a stored session
timeout 60.0 Seconds per request
auto_refresh True Refresh once and retry on an auth error
user_agent tino-sdk-python/<version> Identify your integration
default_headers {} Forward X-Real-IP etc. when proxying
on_token_refresh None (token, refresh) -> None persistence hook

Reference

Metadata

Release files for tino-userapi 1.0.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 tino-userapi 1.0.0
File Size Uploaded
tino_userapi-1.0.0.tar.gz 32.4 kB Details

Built distribution (wheel)

Table of built distributions (wheels) for tino-userapi 1.0.0
File Interpreter ABI Platform
tino_userapi-1.0.0-py3-none-any.whl Python 3 none any Details

Total release size: 74.3 kB

Release files / tino_userapi-1.0.0.tar.gz

Download URL tino_userapi-1.0.0.tar.gz
Size 32.4 kB
Tags Source
SHA-256 checksum
How to use checksums
1035d65ed43108141c5ffb4c4e1cd3cf62c64016d9f6e694a534a87d95912974
BLAKE2b-256 checksum
How to use checksums
08a0d5532fec5850b963335c65c2e9c513616afb2528645a82781913a0bc1868
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 2, 2026.

Transparency log

Release files / tino_userapi-1.0.0-py3-none-any.whl

Download URL tino_userapi-1.0.0-py3-none-any.whl
Size 41.8 kB
Tags Python 3
SHA-256 checksum
How to use checksums
7adc15acc98a168def20b91063c6a0c51831627c3816f9773840a5dab3bcd6e4
BLAKE2b-256 checksum
How to use checksums
fb7ac79717a79555ad326504e0e74eeb95bcce50ea721a811952d3a553229fa8
Upload date
Uploaded using Trusted Publishing?
What is trusted publishing?
Yes
Uploaded via twine/7.0.0 CPython/3.13.14

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.

PyPI Publish Attestation

PyPI verified that this artifact, at this checksum, originated from the publisher listed below.

Signed by GitHub Actions, verified by PyPI on Aug 2, 2026.

Transparency log

Release history Release notifications | RSS feed

1.2.0

2 release files

1.1.0

2 release files

This release

1.0.0 This release

2 release files

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