# SDK & Ví dụ

SDK chính thức của Tino cho Python, Node.js và PHP, mã nguồn mở ở [tinovn/tino-sdk](https://github.com/tinovn/tino-sdk) (giấy phép MIT). Mỗi route có một hàm `client.<nhóm>.<thao tác>()`; SDK mã hoá query kiểu `filter[key]=value`, đọc lỗi trong body (kể cả HTTP 200 kèm `error`) và ném lỗi có kiểu.

Cập nhật: 2026-10-27.

## Cài đặt

| Ngôn ngữ | Lệnh cài | Phiên bản đang phát hành | Yêu cầu |
| --- | --- | --- | --- |
| Python | `pip install tino-userapi` | 1.2.0 (PyPI) | Python 3.8+, không phụ thuộc gói khác |
| Node.js | `npm install @tinovn/tino-sdk` | 1.1.0 (npm) | Node.js 18+, ESM, có định nghĩa TypeScript, không phụ thuộc gói khác |
| PHP | `composer require tinovn/tino-sdk` | 1.1.0 (Packagist) | PHP 7.4+, PSR-4, không phụ thuộc gói khác |

## Khởi tạo client

Trỏ SDK vào gateway `https://api.tino.vn/v1` và truyền token: [token API cá nhân](/docs/personal-tokens) (`tinopat_`) hoặc access token của [Đăng nhập bằng Tino](/docs/oauth) (`tinoat_`). Token đọc từ biến môi trường `TINO_TOKEN`, không ghi vào mã nguồn.

```python
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())
```

```javascript
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
<?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());
```

Không truyền refresh token OAuth cho SDK: SDK tự làm mới theo cách đăng nhập riêng của nó, không qua `/oauth/token`. Access token OAuth hết hạn (mặc định 24 giờ) thì ứng dụng [làm mới token](/docs/oauth#lam-moi-va-thu-hoi-token) rồi tạo client với token mới.

## Gọi một route

Mỗi trang route ở [API Reference](/docs/reference) có đoạn gọi SDK của route đó, ở cột Ví dụ yêu cầu. Tham số đường dẫn là đối số theo thứ tự; body là đối số tên (Python), đối tượng (Node.js) hoặc mảng (PHP). Ví dụ thêm bản ghi DNS (quyền `dns:write`):

```python
client.dns.create_record("100003", "500001", name="www", type="A", content="203.0.113.10", ttl=3600, priority=0)
```

```javascript
await client.dns.createRecord("100003", "500001", { name: "www", type: "A", content: "203.0.113.10", ttl: 3600, priority: 0 });
```

```php
$client->dns->createRecord("100003", "500001", ["name" => "www", "type" => "A", "content" => "203.0.113.10", "ttl" => 3600, "priority" => 0]);
```

## Route chưa có hàm riêng

Một số route của gateway chưa có hàm trong SDK (trang route ghi rõ). Gọi bằng `request` với đường dẫn không có tiền tố `/v1` (base URL đã có):

```python
records = client.request("GET", "/domain/200001/dns")
```

```javascript
const records = await client.request("GET", "/domain/200001/dns");
```

```php
$records = $client->request("GET", "/domain/200001/dns");
```

Body truyền qua `body=` (Python), `{ body: … }` (Node.js) hoặc `["body" => …]` (PHP); query qua `query`.

## Lỗi

| Tình huống | Python, Node.js | PHP |
| --- | --- | --- |
| HTTP 401 `invalid_token` (token sai, hết hạn, đã thu hồi) | `TinoAuthError` | `TinoAuthException` |
| HTTP 403 `insufficient_scope`, `ip_not_allowed` | `TinoApiError` | `TinoApiException` |
| Không tìm thấy | `TinoNotFoundError` | `TinoNotFoundException` |
| Dữ liệu gửi lên sai | `TinoValidationError` | `TinoValidationException` |
| Bị giới hạn tần suất | `TinoRateLimitError` | `TinoRateLimitException` |
| Lỗi mạng, hết thời gian chờ | `TinoTransportError` | `TinoTransportException` |

Mọi lỗi kế thừa `TinoError` (`TinoException` ở PHP); lỗi từ API mang mã lỗi gốc (ví dụ `insufficient_scope`).

## Ví dụ: dịch vụ sắp hết hạn

Liệt kê dịch vụ đang hoạt động hết hạn trong 30 ngày tới (quyền `services:read`):

```python
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"])
```

## Ví dụ: hoá đơn chưa thanh toán

In các hoá đơn đến hạn (quyền `billing:read`):

```javascript
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);
```

## Ví dụ: cập nhật bản ghi A khi IP đổi

Trỏ `home.example.com` về IP mới, kiểu DNS động (quyền `dns:read`, `dns:write`). Vùng DNS lấy theo tên miền; `zone_id` là `domain_id` của vùng. Khi đọc, tên bản ghi đầy đủ (`home.example.com`); khi thêm, sửa, gửi tên tương đối (`home`):

```python
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"])
```

Các ví dụ đọc dữ liệu theo dạng mẫu response trên trang route; chạy thử với token chỉ đủ quyền cần dùng, xem [Môi trường thử nghiệm](/docs/sandbox).
