SDKs & examples
Tino's official SDKs for Python, Node.js and PHP are open source at tinovn/tino-sdk (MIT license). Each route has a function client.<group>.<action>(); the SDK encodes queries such as filter[key]=value, reads errors in the body (including HTTP 200 with error) and throws typed errors.
Updated: 2026-10-27.
Installation
| Language | Install command | Current release | Requirements |
|---|---|---|---|
| Python | pip install tino-userapi | 1.2.0 (PyPI) | Python 3.8+, no other dependencies |
| Node.js | npm install @tinovn/tino-sdk | 1.1.0 (npm) | Node.js 18+, ESM, TypeScript definitions included, no other dependencies |
| PHP | composer require tinovn/tino-sdk | 1.1.0 (Packagist) | PHP 7.4+, PSR-4, no other dependencies |
Creating a client
Point the SDK at the gateway https://api.tino.vn/v1 and pass a token: a personal API token (tinopat_) or an access token from Sign in with Tino (tinoat_). Read the token from the TINO_TOKEN environment variable; do not write it into source code.
import os
from tino_userapi import TinoClient
client = TinoClient(base_url="https://api.tino.vn/v1", token=os.environ["TINO_TOKEN"])
print(client.account.get_details())import { TinoClient } from "@tinovn/tino-sdk";
const client = new TinoClient({ baseUrl: "https://api.tino.vn/v1", token: process.env.TINO_TOKEN });
console.log(await client.account.getDetails());<?php
require "vendor/autoload.php";
$client = new Tino\UserApi\TinoClient(["baseUrl" => "https://api.tino.vn/v1", "token" => getenv("TINO_TOKEN")]);
print_r($client->account->getDetails());Do not pass an OAuth refresh token to the SDK: the SDK refreshes with its own sign-in method, not through /oauth/token. When the OAuth access token expires (24 hours by default), the app refreshes the token and then creates a client with the new token.
Calling a route
Each route page in the API Reference has an SDK call for that route, in the request examples column. Path parameters are positional arguments; the body is keyword arguments (Python), an object (Node.js) or an array (PHP). Example of adding a DNS record (scope dns:write):
client.dns.create_record("100003", "500001", name="www", type="A", content="203.0.113.10", ttl=3600, priority=0)await client.dns.createRecord("100003", "500001", { name: "www", type: "A", content: "203.0.113.10", ttl: 3600, priority: 0 });$client->dns->createRecord("100003", "500001", ["name" => "www", "type" => "A", "content" => "203.0.113.10", "ttl" => 3600, "priority" => 0]);Routes without a dedicated function
Some gateway routes do not have a function in the SDK yet (the route page says so). Call them with request, using the path without the /v1 prefix (the base URL already includes it):
records = client.request("GET", "/domain/200001/dns")const records = await client.request("GET", "/domain/200001/dns");$records = $client->request("GET", "/domain/200001/dns");Pass the body with body= (Python), { body: … } (Node.js) or ["body" => …] (PHP); pass the query with query.
Errors
| Situation | Python, Node.js | PHP |
|---|---|---|
HTTP 401 invalid_token (token wrong, expired, revoked) | TinoAuthError | TinoAuthException |
HTTP 403 insufficient_scope, ip_not_allowed | TinoApiError | TinoApiException |
| Not found | TinoNotFoundError | TinoNotFoundException |
| Invalid request data | TinoValidationError | TinoValidationException |
| Rate limited | TinoRateLimitError | TinoRateLimitException |
| Network error, timeout | TinoTransportError | TinoTransportException |
All errors inherit from TinoError (TinoException in PHP); errors from the API carry the original error code (for example insufficient_scope).
Example: services about to expire
List active services that expire within the next 30 days (scope services:read):
import os
from datetime import date, timedelta
from tino_userapi import TinoClient
client = TinoClient(base_url="https://api.tino.vn/v1", token=os.environ["TINO_TOKEN"])
soon = (date.today() + timedelta(days=30)).isoformat()
for service in client.services.list()["services"]:
if service["status"] == "Active" and service["expires"] <= soon:
print(service["id"], service["domain"], service["name"], service["expires"])Example: unpaid invoices
Print due invoices (scope billing:read):
import { TinoClient } from "@tinovn/tino-sdk";
const client = new TinoClient({ baseUrl: "https://api.tino.vn/v1", token: process.env.TINO_TOKEN });
const { invoices_due: due } = await client.billing.listInvoicesDue();
for (const invoice of due) console.log(invoice.number, invoice.total, invoice.currency, invoice.duedate);Example: updating an A record when the IP changes
Point home.example.com to a new IP, dynamic DNS style (scopes dns:read, dns:write). The DNS zone is found by domain name; zone_id is the zone's domain_id. When reading, record names are fully qualified (home.example.com); when adding or editing, send the relative name (home):
import os
from tino_userapi import TinoClient
client = TinoClient(base_url="https://api.tino.vn/v1", token=os.environ["TINO_TOKEN"])
new_ip = "203.0.113.25"
zone = next(z for z in client.dns.list_zones_all()["zones"] if z["name"] == "example.com")
records = client.dns.get_zone(zone["service_id"], zone["domain_id"])["records"].values()
record = next(r for r in records if r["type"] == "A" and r["name"] == "home.example.com")
if record["content"] != new_ip:
client.dns.update_record(zone["service_id"], zone["domain_id"], record["id"],
name="home", type="A", content=new_ip, ttl=record["ttl"], priority=record["priority"])The examples read data in the shape of the sample responses on the route pages; try them with a token that has only the scopes they need, see Test environment.