Official SDKs and API contract documentation for the Tino UserAPI — the customer API behind tino.vn that manages services, domains, invoices and support tickets for a Tino account.
Base URL https://api.tino.vn
Auth Authorization: Bearer <JWT>
Surface 257 operations · 18 resource groups
| 🐍 Python | sdk/python — 3.8+, zero dependencies |
| 🟩 Node.js | sdk/nodejs — 18+, ESM, TypeScript definitions, zero dependencies |
| 🐘 PHP | sdk/php — 7.4+, PSR-4, zero dependencies |
| 📖 Docs | docs/ — every endpoint with request, response and SDK call |
| 🤖 Agent skill | skills/tino-userapi — drop-in skill for Paperclip, Claude Code and Codex |
from tino_userapi import TinoClient
client = TinoClient()
client.request_login_otp("you@example.com") # code is sent by Zalo or email
client.verify_login_otp("you@example.com", "123456")
print(client.billing.get_balance())
print(client.domains.list())
client.support.create_ticket(dept_id=2, subject="Hello", body="…")import { TinoClient } from '@tinovn/tino-sdk';
const client = new TinoClient();
await client.requestLoginOtp('you@example.com');
await client.verifyLoginOtp('you@example.com', '123456');
console.log(await client.billing.getBalance());
console.log(await client.domains.list());require 'vendor/autoload.php';
$client = new Tino\UserApi\TinoClient();
$client->requestLoginOtp('you@example.com');
$client->verifyLoginOtp('you@example.com', '123456');
print_r($client->billing->getBalance());
print_r($client->domains->list());All three expose the same surface: client.<group>.<operation>(), one group per section
of the API. See docs/api-contract/README.md for the full
list.
The Tino UserAPI has sharp edges. The SDKs absorb them:
- Errors arrive with HTTP 200.
{"error":["unauthorized"]}comes back as a200 OK. The SDKs inspect the body and raise a typed error (TinoAuthError,TinoNotFoundError,TinoValidationError, …) instead of handing you a "successful" response. - Tokens rotate. A refresh invalidates both the old refresh token and the old
access token. The SDKs swap the pair atomically, retry the failed call once, and call
your
on_token_refreshhook so you can persist the new credentials. - Query encoding is PHP-flavoured.
filter[hide_cancelled]=1,key[]=a&key[]=b, booleans as1/0. Pass native structures; the SDK encodes them. - Pagination is zero-based going in, one-based coming out. Documented, not silently "fixed", so your code stays predictable.
- Some responses are binary. PDFs and certificates are exposed as raw bytes rather than mangled UTF-8.
spec/
tino-userapi-catalog.json canonical catalog — single source of truth (257 operations)
response-samples.json redacted live responses used as documentation samples
docs/
getting-started.md base URL, login flows, token lifecycle
conventions.md errors, pagination, filters, data types, unsafe GETs
api-contract/ per-endpoint reference, one chapter per resource group
sdk/
python/ nodejs/ php/ hand-written core + generated resource layer
tools/
build-catalog.py specification + corrections -> catalog
generate-sdk.py catalog -> resource layer for all three languages
generate-docs.py catalog + samples -> docs/api-contract/
sanitize-samples.py live captures -> publishable samples (PII stripped)
skills/
tino-userapi/ agent skill (SKILL.md + references)
examples/ runnable end-to-end scripts
Every operation is described from three angles: the API's formal specification, the way Tino's own client calls it in production, and what the live service actually answers — with the live service as the tie-breaker.
195 operations were executed against https://api.tino.vn with a real account and their
responses captured, redacted and embedded in the documentation. The sweep covered the write endpoints too — orders, tickets, DNS records, VM power
actions, cancellations — on a sandbox account, with each step restoring what it changed.
The remainder are marked spec only: they need data the sandbox does not have, or they
end the session.
The catalog is committed, so the SDKs and the reference can be regenerated from a fresh clone without any additional input.
python3 tools/build-catalog.py <specification.json> # -> spec/tino-userapi-catalog.json
python3 tools/generate-sdk.py # catalog -> sdk/*/resources
python3 tools/generate-docs.py # catalog + samples -> docs/api-contractThe catalog is committed, so generate-sdk.py and generate-docs.py run on a fresh
clone. build-catalog.py is only needed when a new specification export lands.
Hand-written SDK code (transport, auth, errors, client) is never touched by the generator; only the per-endpoint resource layer is generated, which is what keeps the three languages in step.
Tagging publishes all three packages:
git tag v1.0.0 && git push origin v1.0.0.github/workflows/release.yml verifies that the tag
matches the version in every package, runs all three test suites, checks that the
generated code still matches the catalog, and only then publishes. PyPI uses Trusted
Publishing, so no API token is stored; npm needs an NPM_TOKEN secret; Packagist picks
up tags through the GitHub integration once the repository has been submitted.
Version 1.0.0. The catalog and both generated layers are complete; response samples cover
the 195 operations that could be exercised against a sandbox account. Contributions that add verified
samples for the remaining operations are welcome — see
tools/sanitize-samples.py for the redaction rules that
publishable samples must pass.
MIT — see LICENSE.