"]}`; 401 errors and `insufficient_scope` also carry the header `WWW-Authenticate: Bearer error=""` (with `scope` and `resource_metadata` for MCP clients).
| HTTP | `error` | Cause | Fix |
| --- | --- | --- | --- |
| 401 | `invalid_token` | the token is wrong, expired or revoked | apps refresh the token; for personal tokens, create a new token |
| 403 | `insufficient_scope` | the token lacks the route's scope, or the route is not in the gateway | request more scopes (apps) or create a token with more scopes (personal tokens) |
| 403 | `ip_not_allowed` | a personal API token was used from an IP outside its allowed list | call from a listed IP, or create a token with a new IP list |
| 403 | `ipnotallowed` | the calling IP is blocked by Tino | contact Tino |
Business errors of each route (for example, a service that does not exist) keep the UserAPI response, usually HTTP 200 with `{"error": [""]}`; successful actions usually return `{"info": [""]}`.
## Route list
Each route has its own page: parameters, cURL, JavaScript and PHP examples, sample response; the index by product is at [API Reference](/en/docs/reference), the machine-readable version at [/openapi.json](/en/openapi.json). Markdown for LLMs: add `.md` to the page path (for example `/en/docs/api/get-details.md`); everything is in [/llms-full.txt](/en/llms-full.txt).
---
# Getting started with TinoPage Builder
TinoPage Builder builds websites for businesses with AI: the customer describes the business, the AI picks a template and writes the content, and the customer keeps editing in the TinoPage Builder editor. Websites take contacts and orders and run on custom domains. The service API lets your app read limits, create websites with AI, publish, pause and delete websites, add custom domains, read contacts and orders, and open TinoPage Builder with a one-time sign-in ticket.
Updated: 2026-10-26. Sources: Tino's TinoPage Builder module and the TinoPage Builder API.
## Calling the API
| Item | Value |
| --- | --- |
| Address | `https://api.tino.vn/service//builder/…` |
| Authentication | `Authorization: Bearer `: the TinoPage Builder service owner's sign-in token; through `/v1`: an OAuth app token or personal API token with a `builder:*` scope |
| Body | JSON, `Content-Type: application/json` |
| Each route | the TinoPage Builder section of the [API Reference](/en/docs/reference#tinopage-builder) |
- Only the parameters listed on each route are relayed. Tino checks shapes before calling TinoPage Builder: a website ID that is not a UUID, a malformed domain, or an unknown query or body field answers 400 `INVALID_REQUEST` with `details.field` naming the field.
- Website IDs in paths are UUIDs; domains in paths are lower case.
- Editing website content (pages, blocks, images, items) and handling contacts and orders happen in TinoPage Builder: open it with a [sign-in ticket](#sign-in-tickets).
Since 2026-10-27 the TinoPage Builder routes can also be called through the `/v1` gateway: `https://api.tino.vn/v1/service//builder/…` with an OAuth app token (`tinoat_`, scopes `builder:read`, `builder:write`) or a [personal API token](/en/docs/personal-tokens) (`tinopat_`), with the same bodies and answers. Only the [sign-in ticket](#sign-in-tickets) (`POST …/builder/login`) is not exposed through `/v1`: it needs the sign-in token.
The sign-in token is the token customers use in the tino.vn client area; the [SDK](/en/docs/sdk) gets one by signing in with an OTP code:
```python
from tino_userapi import TinoClient
client = TinoClient()
client.request_login_otp("you@example.com")
client.verify_login_otp("you@example.com", input("OTP code: "))
print(client.request("GET", "/service/100003/builder/overview"))
```
`100003` is the TinoPage Builder service ID: see below for how to find it.
## Find the TinoPage Builder service
Each Tino customer has one TinoPage Builder account, bound to the purchased TinoPage Builder service.
1. Take candidates from [List services](/en/docs/api/get-service) (`product_type`) or [Service details](/en/docs/api/get-service-id) (`ptype`): the value `tinobuilderpanel`. For a service without it, look at `acc_module`: `TinoPage Builder`. Compare case-insensitively, ignoring spaces, `_` and `-`.
2. Call [Account overview](/en/docs/api/builder-get-overview) (`GET /service//builder/overview`):
| Result | Meaning |
| --- | --- |
| 200 | the customer's TinoPage Builder service: call the other routes with this ID |
| 404 `SERVICE_NOT_LINKED` | the TinoPage Builder account is bound to another service of the customer: try the next service |
| 404 `SERVICE_NOT_FOUND`, `invalid method` | not a TinoPage Builder service of the signed-in account |
| 404 `ACCOUNT_NOT_FOUND` | no TinoPage Builder account for the service yet: contact Tino support |
## Service limits
TinoPage Builder is not sold as fixed plans: limits follow the purchased service and live in `config.effective` of [Account overview](/en/docs/api/builder-get-overview). Read them from the API instead of hard-coding them: the numbers can change with the service. `-1` means unlimited.
| Key | Applies to | Meaning |
| --- | --- | --- |
| `sites` | account | websites (not deleted, drafts included) |
| `pages_per_site` | each website | pages |
| `items_per_site` | each website | items |
| `domains_per_site` | each website | custom domains |
| `ai_generations_daily` | each website | AI content creations and edits in 24 hours |
| `ai_chat_daily` | each website | messages to the AI assistant in 24 hours |
| `ai_images_daily` | each website | AI images in 24 hours |
- Usage: `usage` of Account overview (`sites`, `sites_paused`, `sites_published`) and `pages`, `items`, `domains` (`used`, `limit`) of each website.
- No websites left: creating a website answers 409 `LIMIT_EXCEEDED` with `details.key` `sites`; no domains left: `domains_per_site`.
- After limits go down or the service is terminated, the account keeps its oldest websites running up to `sites`; the others are paused by the system (`paused: true`, `paused_reason: over_limit`): the public page says the website is paused and takes no contacts or orders. Raising the limit or deleting websites brings paused websites back, oldest first. Pages, items and domains over a lowered limit are not deleted; only new ones are blocked.
- Suspended service: read only. Writes answer 403 `ACCOUNT_SUSPENDED`; `GET` and sign-in tickets still work (read-only access). Terminated service: writes and sign-in tickets answer 403 `ACCOUNT_TERMINATED`.
## Create a website with AI
[Create a website with AI](/en/docs/api/builder-post-sites) takes `intake`: the business details the AI uses to pick a template and write the content.
| Field | Required | Max (characters) | Notes |
| --- | --- | --- | --- |
| `business_name` | yes | 120 | business name |
| `industry` | yes | 80 | line of business, e.g. bakery, spa, coffee shop |
| `phone` | yes | 20 | Vietnamese number: `0`, `84` or `+84`, then 9 mobile digits (starting 3, 5, 7, 8, 9) or 10 landline digits (starting 2); spaces, dots, dashes and parentheses allowed |
| `address` | — | 200 | address |
| `city` | — | 80 | province or city |
| `services` | — | 300 | main services or products |
| `hours` | — | 100 | opening hours |
| `zalo` | — | 100 | Zalo phone number or link |
| `email` | — | 120 | contact email |
- A field outside the table, a field that is not a string, or one that is too long answers 400 `INVALID_REQUEST` (`details.field`: `intake` or `intake.`). A missing required field or an invalid phone or email answers 422 `VALIDATION_FAILED` with `details` per `intake.` and a reason in Vietnamese.
- Creation runs in the background for 1–5 minutes: the create call answers HTTP 202 with `state: running`. Poll [Website creation progress](/en/docs/api/builder-get-site-creation) every few seconds until `state` is `done` (`site_id` is the new website) or `failed` (`message` is the reason in Vietnamese).
- One creation per account at a time: a second one answers 409 `CREATION_IN_PROGRESS`. AI creation paused: 503 `AI_UNAVAILABLE`.
- The result is kept for 1 hour; after that (or after a server restart) progress answers `idle`, and the created website stays in [List websites](/en/docs/api/builder-get-sites).
- Progress may also carry `step` (`queued`, `design`, `write`, `images`, `build`) and, once done, `mode` (`design`, `merge`): for showing progress only; do not branch on them.
## Publish, pause and delete websites
| State | Meaning |
| --- | --- |
| `status: draft` | not published: a new website, or one paused with [Pause a website](/en/docs/api/builder-post-sites-site-unpublish) |
| `status: published` | published, running on `host` (custom domain) or the preview link `preview_url` |
| `paused: true` | paused by the system (`paused_reason: over_limit`: the account is over its website count), overrides `status` |
- [Publish a website](/en/docs/api/builder-post-sites-site-publish) publishes the latest version, which also reopens a paused website. A `paused` website answers 409 `SITE_PAUSED`: delete another website or raise the website count.
- [Pause a website](/en/docs/api/builder-post-sites-site-unpublish) takes the website off the web with its content kept.
- [Delete a website](/en/docs/api/builder-delete-sites-site) moves it to the trash (Tino can restore it on request through support), removes its custom domains and frees one of the account's websites.
- [Website details](/en/docs/api/builder-get-sites-site) has `todo`: contacts not yet called back (`leads_new`) and orders not yet confirmed (`orders_new`).
## Custom domains
1. Publish the website first: an unpublished website answers 409 `SITE_NOT_PUBLISHED`.
2. [Add a custom domain](/en/docs/api/builder-post-sites-site-domains) with `hostname` (e.g. `example.vn`, `banhang.example.vn`): it answers 201 with the domain at `status: pending` and `dns`, the records the customer must set where the domain's DNS is managed.
3. The customer sets the records: CNAME to `dns_target` (names `@` and `www` for an apex domain, the sub-label for a subdomain) and TXT `_tino-verify` (value `tino-site-verify=…`) proving ownership. Record names are relative to the apex domain. Take `dns_target` and the TXT value from the API; do not hard-code them.
4. TinoPage Builder rechecks `pending` domains on a schedule (`next_check_at`); [Check DNS again](/en/docs/api/builder-post-sites-site-domains-hostname-verify) checks now. With both CNAME and TXT in place `status` turns `active` and an HTTPS certificate is issued automatically (`ssl_status`: `pending`, `active`, `error`); `www` redirects to the apex domain.
- `cname_ok` and `txt_ok` are the latest check's results; `error` says what is missing (in Vietnamese) and is empty once it passes. `checked_at` is `0001-01-01T00:00:00Z` before the first check.
- `owner_verified: true`: domain ownership is already confirmed, no TXT needed (`dns` only has the CNAME records).
- Automatic checks stop after 14 failures (about 10 days); `error` says so, and Check DNS again tries once more.
- No domains left for the website: 409 `LIMIT_EXCEEDED` (`details.key: domains_per_site`); domain used by another website: 409 `DOMAIN_TAKEN`; not a domain: 422 `VALIDATION_FAILED` (`details.hostname`).
## Contacts and orders
[Contacts and orders](/en/docs/api/builder-get-sites-site-leads) is read only: `kind=lead` (default) is a contact from a website form, `kind=order` an order; newest first, paged with `page`, `per_page` (default 20, at most 50) and `meta`.
| `status` | Meaning | Filterable |
| --- | --- | --- |
| `moi` | new | yes |
| `da_lien_he` | contacted (contacts) | yes |
| `xac_nhan` | confirmed | yes |
| `dang_lam` | in preparation (food orders) | no |
| `san_sang` | ready (food orders) | no |
| `dang_giao` | out for delivery | yes |
| `hoan_tat` | completed | yes |
| `huy` | cancelled | yes |
- Contact: `id`, `kind`, `name`, `phone`, `payload` (the form content, keys depend on the website's form), `source_page`, `status`, `created_at`.
- Order: `id`, `code`, `name`, `phone`, `address`, `note`, `items` (`name`, `option`, `qty`, `subtotal`), `shipping`, `total` (`null` when the website shows no price), `payment` (`COD`, `Chuyển khoản` (bank transfer) or other text), `status`, `status_label` (Vietnamese label), `created_at`, `sheet_error` (error sending the order to Google Sheets, empty when none).
- Unknown status: show `status_label` when present, otherwise `status` as is.
## Sign-in tickets
A [one-time sign-in ticket](/en/docs/api/builder-post-login) opens TinoPage Builder on the service's account without the customer signing in again:
- `target: me` (default) opens the dashboard; `target: editor` opens the editor of `site_id`. Without `site_id`: the account's oldest website (the dashboard when the account has no website yet).
- It answers 201 `{url, expires_at}`: `url` is https on the TinoPage Builder server, single use, valid for 120 seconds. Open it right away in the service owner's browser; do not store it or send it through another channel. Chat apps that preview the link do not use up the ticket.
- `login_url` of Account overview is not a ticket: it opens the TinoPage Builder sign-in page with a Tino account, and the customer signs in.
## Safe retries
Every write (create, publish, pause, delete a website; add, check, remove a domain; sign-in ticket) takes `idempotency_key` (8–64 characters `A-Za-z0-9._:-`); with `DELETE`, send it in the query. Make one key per customer click:
- If the connection drops midway, resend the same key with the same content: you get the earlier result and the command does not run twice. TinoPage Builder remembers keys for 24 hours; an earlier 5xx is not remembered, so a resend runs for real.
- The same key with different content, or while the earlier call is still running: 409 `IDEMPOTENCY_CONFLICT`. Reload the data before trying with a new key.
- Without a key, every call is a new command.
## Responses
Success: `{"data": …}`, where `data` is TinoPage Builder's answer; lists add `meta` (`page`, `per_page`, `total`, `last_page`). HTTP 200, 201 when adding a domain or issuing a sign-in ticket, 202 when a website creation is accepted. Times are RFC 3339; website IDs are UUIDs.
## Errors
```json
{ "error": ["LIMIT_EXCEEDED"], "message": "Tài khoản đã dùng hết số website được phép. Nâng cấu hình dịch vụ trên Tino để tạo thêm website.", "details": { "key": "sites" }, "request_id": "…" }
```
Branch on `error[0]`, not on `message` (a Vietnamese sentence that may change). Codes are upper case; `invalid method` (the route does not exist for this service) only has `error`. When reporting an error to Tino, include `request_id`.
| HTTP | Code | Meaning, what to do |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | a website ID, domain, query, body or `idempotency_key` has the wrong shape, `details.field` names the field: a programming error in the app |
| 401 | `unauthorized`, `invalid_token`, `token_expired` | the sign-in token is wrong or expired: refresh it |
| 403 | `ACCOUNT_SUSPENDED` | the service is suspended: writes are blocked, `GET` still works |
| 403 | `ACCOUNT_TERMINATED` | the service is terminated: writes and sign-in tickets are blocked |
| 404 | `SERVICE_NOT_FOUND`, `invalid method` | not a TinoPage Builder service of the signed-in account: go back to the service list |
| 404 | `SERVICE_NOT_LINKED` | the TinoPage Builder account is bound to another service: find the service again ([Find the TinoPage Builder service](#find-the-tinopage-builder-service)) |
| 404 | `ACCOUNT_NOT_FOUND` | no TinoPage Builder account yet: contact Tino support |
| 404 | `RESOURCE_NOT_FOUND` | the website or domain is not in the account (or was deleted): reload the list |
| 409 | `LIMIT_EXCEEDED` | a limit of the purchased service is used up, `details.key` names it: delete something or upgrade the service |
| 409 | `CREATION_IN_PROGRESS` | a website creation is running: wait for it to finish |
| 409 | `SITE_PAUSED` | the website is paused by the system for being over the website count: it cannot be published yet |
| 409 | `SITE_NOT_PUBLISHED` | publish the website before adding a custom domain |
| 409 | `DOMAIN_TAKEN` | the domain is used by another website |
| 409 | `INVALID_ACTION` | not possible in the current state, `message` says why: reload and retry |
| 409 | `IDEMPOTENCY_CONFLICT` | the same `idempotency_key` with different content, or the earlier call is still running |
| 422 | `VALIDATION_FAILED` | invalid data: show the error under the field named in `details` (`intake.`, `hostname`, `kind`, `status`, `target`) |
| 429 | `RATE_LIMITED` | too fast: retry after a few seconds |
| 503 | `AI_UNAVAILABLE` | AI website creation is paused: retry later |
| 500, 502, 503 | `INTERNAL_ERROR`, `NETWORK_ERROR`, `INVALID_RESPONSE`, `NOT_CONFIGURED` | the connection between Tino and TinoPage Builder is interrupted: a Tino-side error, not a token problem. Do not sign out; retry later |
---
# Domain registrant dossier
A paid `.vn` domain whose registrant details or dossier (ID card eKYC, an organisation's declaration) are not complete yet has an order but is **not registered** with VNNIC: status `Pending`. The four routes below let the customer's software see which domains are waiting, what is still missing, and update the registrant; once the dossier is complete Tino registers the domain automatically.
Updated: 2026-10-27.
A domain with an incomplete dossier is **not reserved**: someone else can still register it. Under Circular 48/2025/TT-BKHCN, "first come, first served" only applies to domains whose procedure is complete and paid. Remind customers to finish early.
## Routes and scopes
| Route | Purpose | Scope |
| --- | --- | --- |
| [`GET /v1/domains/pending`](/en/docs/api/get-domains-pending) | Domains not activated yet and what is left to do | `domains:registrant` |
| [`GET /v1/domains/{id}/registrant`](/en/docs/api/get-domains-id-registrant) | Registrant and dossier status of one waiting domain | `domains:registrant` |
| [`PUT /v1/domains/{id}/registrant`](/en/docs/api/put-domains-id-registrant) | Save the registrant (new details or another domain's registrant); Tino tries to register right away | `domains:registrant` |
| [`GET /v1/registrants`](/en/docs/api/get-registrants) | Registrants of the customer's running domains, to pick again | `domains:registrant` |
All four routes need `domains:registrant` (view and complete the registrant dossier of domains awaiting registration): this scope is only available to [personal API tokens](/en/docs/personal-tokens) with an IP restriction; OAuth apps and the MCP server are never granted it. The routes return the customer's own full registrant data (ID number, date of birth, phone, address): never write these fields to logs.
eKYC and declarations are done in Tino's Client Area (tino.vn), not through the API.
## Waiting domains
`GET /v1/domains/pending`, oldest first:
```json
{
"domains": [
{
"id": 200001,
"name": "example.vn",
"status": "Pending",
"subject_type": "ind",
"ready": false,
"actions": [{ "code": "ekyc", "message": "Chủ thể cần eKYC CCCD trong tài khoản này" }],
"reason": "Chưa eKYC CCCD 001******001"
}
]
}
```
| Field | Meaning |
| --- | --- |
| `subject_type` | `ind` individual · `org` organisation · `hkd` household business |
| `ready` | `true`: dossier complete, Tino is registering the domain (a few minutes) |
| `actions` | what the customer still has to do (table below); empty with `ready: false` means the declaration was submitted and awaits Tino's review |
| `reason` | why the latest registration attempt did not go through; may be `null`. ID numbers (runs of 9–13 digits) in the sentence are masked, keeping only the first 3 and last 3 digits |
No waiting domains: `{"domains": []}`. A registered domain leaves this list and becomes `Active` in `GET /v1/domain`.
`message` and `reason` are Vietnamese sentences meant for the customer.
## What is left to do
| `actions[].code` | Task |
| --- | --- |
| `fields` | Missing or invalid fields listed in `fields` (e.g. `["nationalid", "city"]`): fix them, then `PUT` |
| `owner_fields` | Same, for the owner of a household business |
| `ekyc`, `owner_ekyc` | The registrant (or owner) must complete eKYC with the declared ID card, in this same account |
| `ekyc_name`, `owner_ekyc_name` | The declared name differs from the eKYC name: correct it to match the ID card, then `PUT` |
| `ekyc_other_account`, `owner_ekyc_other_account` | The ID card was verified in another Tino account: use another registrant or contact Tino |
| `declaration` | Organisation: submit the signed declaration (or digitally sign the organisation dossier) in the Client Area |
## Reading the registrant
`GET /v1/domains/{id}/registrant`:
```json
{
"domain": { "id": 200001, "name": "example.vn", "status": "Pending" },
"subject_type": "ind",
"registrant": {
"firstname": "Van A", "lastname": "Nguyen", "companyname": "", "taxid": "", "householdsid": "",
"nationalid": "001090000001", "birthday": "1990-01-01", "gender": "M", "email": "user@example.com",
"phonenumber": "0900000001", "country": "VN", "state": "Thành phố Hồ Chí Minh", "city": "Phường Bến Thành", "ward": "",
"address1": "123 Example Street", "address2": "", "postcode": "", "position": ""
},
"owner": null,
"dossier": { "ekyc": "required", "declaration": "not_needed" },
"actions": [{ "code": "ekyc", "message": "Chủ thể cần eKYC CCCD trong tài khoản này" }],
"reason": "Chưa eKYC CCCD 001******001",
"ready": false
}
```
- `state` and `city` are the **names** of the province / city and ward (post-merger list), not codes.
- `owner`: the household owner (name, ID number…) when `subject_type` is `hkd`; VNNIC checks the owner's eKYC. Other types: `null`.
- `dossier.ekyc`: `ok` · `required` · `name_mismatch` · `other_account` · `not_needed` (organisations, non-`.vn` extensions).
- `dossier.declaration` (organisations): `accepted` · `pending` (submitted, awaiting review) · `cancelled` (rejected) · `none` · `not_needed`. `dossier.signature`: `ok` (organisation dossier digitally signed for the tax code) · `none`.
## Reusing an earlier registrant
`GET /v1/registrants` returns the registrants of the customer's **running** domains, newest first, one row per person / organisation:
```json
{
"registrants": [
{ "from_domain_id": 200002, "domain": "example.com.vn", "subject_type": "ind", "registrant": { "…": "as above" }, "owner": null, "ready": true, "actions": [] }
]
}
```
`ready: true`: the registrant completed eKYC (or digital signing) in this account, so a new domain can be registered right away. An organisation that only has a paper declaration on the old domain has `ready: false` and a `declaration` action: declarations belong to each domain and must be submitted again.
## Saving the registrant
`PUT /v1/domains/{id}/registrant`, JSON body, one of two forms:
```json
{ "from_domain_id": 200002 }
```
```json
{
"registrant": {
"firstname": "Van A", "lastname": "Nguyen", "nationalid": "001090000001", "phonenumber": "0900000001",
"state": "Thành phố Hồ Chí Minh", "city": "Phường Bến Thành", "address1": "123 Example Street",
"email": "user@example.com", "birthday": "1990-01-01", "gender": "M", "country": "VN"
},
"owner": null
}
```
- Send **every field** of the registrant: a field left out counts as empty; unknown fields are ignored.
- Required for `.vn`: `firstname`, `lastname`, `nationalid`, `phonenumber`, `state`, `city`, `address1`; organisations add `companyname`, `taxid`; household businesses add `householdsid` and `owner` (`firstname`, `lastname`, `nationalid`). Other extensions: `firstname`, `lastname`, `email`, `phonenumber`, `address1`, `city`, `country`.
- After saving, Tino tries to register right away and returns the new dossier (same shape as `GET`); read `actions` and `ready` again for the next step. Without another call, the system retries about every 5 minutes.
- Only for waiting domains. To change the registrant of an `Active` domain: [`PUT /v1/domain/{id}/contact`](/en/docs/api/put-domain-id-contact).
## Errors
Errors look like `{"error": [""]}` and can be shown to the customer as is.
| HTTP | Sentence | When |
| --- | --- | --- |
| 403 | `insufficient_scope` | the token lacks `domains:registrant` (an OAuth app's token, or a personal token without this scope) |
| 404 | Không tìm thấy tên miền chờ đăng ký | the domain is `Active`, cancelled, or not the customer's |
| 422 | Tên miền không tồn tại hoặc đã kích hoạt | `PUT` on a domain that is no longer waiting |
| 422 | Thiếu city (… fields) | a required field is missing |
| 422 | Trường state không hợp lệ | the value is not a string or is longer than 255 characters |
| 422 | Hộ kinh doanh cần thông tin chủ hộ (owner) | a household business without `owner` |
| 422 | Không dùng được chủ thể của tên miền #N | `from_domain_id` is not one of the customer's running domains |
---
# Sign in with Tino — OAuth 2.0 and OpenID Connect
Your app signs customers in with their Tino account and calls the Tino API on their behalf through OAuth 2.0 Authorization Code + PKCE (S256), with OpenID Connect. The issuer is `https://api.tino.vn`; the sign-in and consent page is `https://auth.tino.vn/oauth/consent`.
Updated: 2026-10-27. Source of truth for endpoints, scopes and claims: the discovery document `https://api.tino.vn/.well-known/openid-configuration`.
## Overview
Every endpoint is listed in the discovery document: read `https://api.tino.vn/.well-known/openid-configuration` (or `/.well-known/oauth-authorization-server`, same content) instead of hard-coding addresses.
| Item | Value |
| --- | --- |
| Issuer (`iss`) | `https://api.tino.vn` |
| Sign-in + consent page (`authorization_endpoint`) | `https://auth.tino.vn/oauth/consent` |
| Code exchange, token refresh (`token_endpoint`) | `POST https://api.tino.vn/oauth/token` |
| Token revocation (`revocation_endpoint`) | `POST https://api.tino.vn/oauth/revoke` |
| UserInfo (`userinfo_endpoint`) | `GET https://api.tino.vn/oauth/userinfo` |
| Public keys (`jwks_uri`) | `GET https://api.tino.vn/oauth/jwks` |
| Calling the API on the customer's behalf (`resource_gateway`) | `https://api.tino.vn/v1/` |
| App registration (`registration_endpoint`) | `POST https://api.tino.vn/oauth/register` — public clients register themselves, see [Sign in from a CLI](/en/docs/cli-login) |
| Device code request (`device_authorization_endpoint`) | `POST https://api.tino.vn/oauth/device_authorization` — CLIs, see [Sign in from a CLI](/en/docs/cli-login) |
| Supported flows | `response_type=code`; `grant_type` = `authorization_code`, `refresh_token`, `urn:ietf:params:oauth:grant-type:device_code` |
| PKCE | required, `S256` only |
| Client authentication at the token endpoint | `client_secret_post` (clients with a secret) or `none` (public clients + PKCE); HTTP Basic is not supported |
| ID token signing | `RS256` |
There is no implicit flow, hybrid flow, client credentials grant or password grant.
## Sign-in flow
1. The app redirects the customer's browser to `https://auth.tino.vn/oauth/consent` with `client_id`, `scope`, `code_challenge`.
2. The customer signs in (if not signed in yet), chooses an account (if the browser has two or more accounts), and approves the requested scopes.
3. Tino redirects the browser back to the app's `redirect_uri` with `?code=…&state=…`.
4. The app calls `POST https://api.tino.vn/oauth/token` with `code` + `code_verifier`.
5. Tino returns `access_token`, `id_token` (with `openid`), `refresh_token` (with `offline_access`).
6. The app calls `https://api.tino.vn/v1/` with `Authorization: Bearer `.
The browser only carries the `code` back to the app; the app itself calls api.tino.vn to exchange the code for tokens, sending `code_verifier` to prove it is the same sign-in.
## Registering an app
Tino creates the app for you and issues a `client_id` (plus a `client_secret` if the app has a server). Public clients (CLIs, apps running on the customer's machine, MCP clients) can also register themselves with `POST https://api.tino.vn/oauth/register` (RFC 7591), see [Sign in from a CLI](/en/docs/cli-login).
| Client type | Use when | Authentication at `/oauth/token` |
| --- | --- | --- |
| Confidential (with `client_secret`) | Web apps that exchange the code on a server | `client_id` + `client_secret` in the form body (`client_secret_post`) + PKCE |
| Public (no secret) | Apps that run on the user's device: SPA, mobile, CLI | `client_id` + PKCE only |
When requesting an app, send Tino: the name and logo (https) shown on the consent page, the list of redirect URIs, the scopes you need (see the Calling the Tino API section), and whether the app can keep a secret on a server.
Redirect URI rules:
- Exact string match against the registered URI (including path and query); URIs with a `#fragment` are not accepted.
- Must be `https://`. Only for local development: `http://localhost` or `http://127.0.0.1`.
- A loopback URI registered without a port matches any port (RFC 8252 section 7.3), which suits CLI apps that listen on a random port.
`client_id` can be the URL of a Client ID Metadata Document (discovery reports `client_id_metadata_document_supported: true`), but only URLs that Tino has registered in advance are accepted.
## Step 1 — Send the customer to the consent page
For each sign-in, the app generates random `code_verifier`, `state` and `nonce` values, keeps them (httpOnly cookie or session), then redirects the browser to `https://auth.tino.vn/oauth/consent` with the following parameters in the query string.
| Parameter | Required | Value |
| --- | --- | --- |
| `response_type` | yes | `code` |
| `client_id` | yes | the client_id issued by Tino |
| `redirect_uri` | yes | exactly the registered string |
| `scope` | yes | space-separated scopes, from the scopes granted to the app; `openid` to get an ID token, `offline_access` to get a refresh token |
| `state` | recommended | random string against CSRF; Tino returns it unchanged |
| `code_challenge` | yes | `BASE64URL(SHA256(code_verifier))`, without `=` padding |
| `code_challenge_method` | yes | `S256` |
| `nonce` | recommended (OpenID) | random string of up to 255 characters; returned in the ID token |
| `prompt` | no | `none`: if the customer is signed in and has already granted all scopes, redirect back immediately without stopping at any screen |
Example:
```text
https://auth.tino.vn/oauth/consent?response_type=code&client_id=YOUR_CLIENT_ID&redirect_uri=https%3A%2F%2Fapp.example.com%2Fauth%2Fcallback&scope=openid%20profile%20email%20offline_access%20services%3Aread&state=af0ifjsldkj&code_challenge=E9Melhoa2OwvFrEMTJguCHaoeK1t8URWbuGJSstw-cM&code_challenge_method=S256&nonce=n-0S6_WzA2Mj
```
Generating PKCE (Node.js 18+):
```javascript
import { createHash, randomBytes } from "node:crypto";
const codeVerifier = randomBytes(32).toString("base64url"); // 43 characters
const codeChallenge = createHash("sha256").update(codeVerifier).digest("base64url");
```
On the consent page, a customer who is not signed in signs in first (OTP, passkey, Google, Zalo, GitHub). If the browser has two or more Tino accounts, the customer chooses an account. If the customer has already granted these scopes to the app, they do not have to approve again; a request for more scopes asks again.
Result: Tino redirects the browser back to `redirect_uri`:
| Situation | Query sent to `redirect_uri` |
| --- | --- |
| The customer clicks Allow | `code=…&state=…` |
| The customer clicks Deny | `error=access_denied&state=…` |
| Unknown `scope` or outside the app's scopes | `error=invalid_scope&state=…` |
| `client_id` missing or disabled, `redirect_uri` not registered | no redirect: a Tino page shows the error |
Before using the `code`, compare the received `state` with the stored `state`; if they differ, discard it.
## Step 2 — Exchange the code for tokens
Exchange the `code` within 60 seconds; each code can be used once: `POST https://api.tino.vn/oauth/token`, body `application/x-www-form-urlencoded`. Confidential clients call from the server and send `client_secret` in the body; do not use the `Authorization: Basic` header.
```bash
curl -X POST https://api.tino.vn/oauth/token \
-H "Content-Type: application/x-www-form-urlencoded" \
-d grant_type=authorization_code \
-d code=AUTH_CODE \
-d redirect_uri=https://app.example.com/auth/callback \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET \
-d code_verifier=CODE_VERIFIER
```
| Parameter | Value |
| --- | --- |
| `grant_type` | `authorization_code` |
| `code` | the code received at `redirect_uri` |
| `redirect_uri` | the same URI sent in step 1 |
| `client_id` | your client_id |
| `client_secret` | confidential clients only |
| `code_verifier` | the original string used to create `code_challenge` |
Response (HTTP 200):
```json
{
"access_token": "tinoat_…",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "openid profile email offline_access services:read",
"id_token": "eyJhbGciOiJSUzI1NiIsImtpZCI6…",
"refresh_token": "tinort_…"
}
```
| Token | Present when | Default lifetime |
| --- | --- | --- |
| `access_token` (`tinoat_…`, opaque string, not a JWT) | always | 86400 seconds (24 hours) |
| `id_token` (JWT RS256) | `scope` includes `openid` | 86400 seconds (24 hours) |
| `refresh_token` (`tinort_…`) | `scope` includes `offline_access` | 2592000 seconds (30 days) |
| `code` | — | 60 seconds, single use |
Lifetimes are configured by Tino and may change: read `expires_in` from each response instead of hard-coding them. Keep tokens on the server (or in the device's secure storage), never in URLs or localStorage.
## Refreshing and revoking tokens
Each refresh returns a new refresh token and invalidates the old one (rotation). If a rotated refresh token is reused, Tino revokes all of the app's access to that account; the customer must sign in and approve again.
```bash
curl -X POST https://api.tino.vn/oauth/token \
-d grant_type=refresh_token \
-d refresh_token=tinort_… \
-d client_id=YOUR_CLIENT_ID \
-d client_secret=YOUR_CLIENT_SECRET
```
- `scope` (optional): narrows the scopes of the new token; exceeding the granted scopes returns `invalid_scope`, and the old refresh token stays usable.
- The response has the same shape as in step 2. Store the new `refresh_token` immediately, before using the `access_token`.
- If several processes refresh the same refresh token, only one wins; the others count as reuse. Keep refreshing in one place (a lock per account).
- `invalid_grant` on refresh: the token expired, was revoked, or the customer revoked the app's access. Send the customer through sign-in again from step 1.
Revoke when the customer signs out of the app (RFC 7009): `POST https://api.tino.vn/oauth/revoke` with `token=`, `client_id`, `client_secret` (confidential clients). The refresh token and its access token both stop working; the endpoint always returns HTTP 200.
Customers can also revoke access themselves on the Authorized apps page (`https://auth.tino.vn/en/me/apps`); the app's tokens stop working immediately, and the next call gets 401 `invalid_token`.
## ID token and UserInfo
The ID token is a JWT signed with `RS256`; verify the signature with the key whose `kid` matches the header, from `https://api.tino.vn/oauth/jwks` (cache it, reload when you see an unknown `kid`). Use an existing OpenID Connect library (openid-client, jose, Authlib…) instead of writing your own.
| Claim | Present when | Check or use |
| --- | --- | --- |
| `iss` | always | must be `https://api.tino.vn` |
| `aud`, `azp` | always | must be your client_id |
| `exp`, `iat` | always | not expired |
| `nonce` | `nonce` was sent in step 1 | must match the stored value |
| `auth_time` | when Tino knows it | when the customer signed in (unix seconds) |
| `sub` | always | Tino customer ID (string): the key for linking the account in your app |
| `email` | scope `email` | the account's email (or the contact's email when the customer signs in with a contact account) |
| `given_name`, `family_name`, `name` | scope `profile` | given name, family name, full name |
Match accounts by `sub`, not by `email`: customers can change their email.
UserInfo returns the same claims (`sub` and the claims for each scope) and needs an access token with `openid`:
```bash
curl https://api.tino.vn/oauth/userinfo -H "Authorization: Bearer tinoat_…"
```
```json
{ "sub": "26", "email": "binh@example.com", "given_name": "Bình", "family_name": "Trần", "name": "Bình Trần" }
```
An access token without `openid` gets 403 `insufficient_scope`.
## Calling the Tino API
The app calls the Tino API on the customer's behalf through the gateway `https://api.tino.vn/v1/` with `Authorization: Bearer `. Each route belongs to one scope; a route outside the token's scopes returns 403 `insufficient_scope`.
```bash
curl https://api.tino.vn/v1/details -H "Authorization: Bearer tinoat_…"
```
`GET /v1/details` runs the UserAPI route `GET /details` with the customer's account; the body and response keep the UserAPI format.
| Scope (`scope`) | What the customer sees on the consent page | Example routes | Routes |
| --- | --- | --- | --- |
| `openid` | Verify your identity | ID token, `/oauth/userinfo` | — |
| `profile` | Your name | claims `name`, `given_name`, `family_name` | — |
| `email` | Email address | claim `email` | — |
| `offline_access` | Keep access while you're offline | issues a `refresh_token` | — |
| `profile:read` | View account details | `GET /v1/details`, `GET /v1/contacts` | 11 |
| `profile:write` | Edit account details | `PUT /v1/notifications/@id/ack` | 1 |
| `services:read` | View services, including service login details | `GET /v1/service`, `GET /v1/service/@id` | 28 |
| `services:write` | Manage your services | `POST /v1/service/@id/renew`, `POST /v1/service/@id/vms/@vmid/reboot` | 14 |
| `domains:read` | View domains | `GET /v1/domain`, `GET /v1/domain/@id` | 17 |
| `domains:write` | Register, renew and manage domains | `POST /v1/domain/@id/renew`, `POST /v1/domain/@id/reg` | 12 |
| `dns:read` | View DNS records | `GET /v1/dns`, `GET /v1/domain/@id/dns` | 7 |
| `dns:write` | Edit DNS records | `POST /v1/domain/@id/dns`, `DELETE /v1/domain/@id/dns/@index` | 10 |
| `billing:read` | View invoices and payments | `GET /v1/invoices`, `GET /v1/balance` | 13 |
| `billing:write` | Make payments and top up | `POST /v1/addfunds` | 7 |
| `orders:write` | Order services: cart, quotes, create orders (paying from balance also needs the Payments permission) | `POST /v1/quote`, `POST /v1/carts`, `POST /v1/order` | 8 |
| `tickets:read` | View support tickets | `GET /v1/tickets`, `GET /v1/tickets/@number` | 9 |
| `tickets:write` | Create and reply to tickets | `POST /v1/tickets`, `POST /v1/tickets/@number` | 4 |
Service scopes (the `/v1` gateway opened them on 2026-10-27): call `https://api.tino.vn/v1/service///…` for the customer's own services.
| Scope | What the customer sees on the consent page | Routes | Guide |
| --- | --- | --- | --- |
| `tinopay:read` | View TinoPay: receiving accounts, transactions, webhooks | 16 | [Getting started with TinoPay](/en/docs/tinopay-start) |
| `tinopay:write` | Manage TinoPay: virtual accounts, webhooks, notifications, Sapo integration | 21 | [Getting started with TinoPay](/en/docs/tinopay-start) |
| `builder:read` | View TinoPage Builder websites | 6 | [Getting started with TinoPage Builder](/en/docs/builder-start) |
| `builder:write` | Manage TinoPage Builder websites: create, publish, custom domains | 7 | [Getting started with TinoPage Builder](/en/docs/builder-start) |
| `waf:read` | View websites protected by TinoWAF and the event log | 8 | [TinoWAF](/en/docs/waf) |
| `waf:write` | Manage TinoWAF: websites, protection level, origin servers, IP rules | 11 | [TinoWAF](/en/docs/waf) |
`tinopay:owner` (create TinoPay API tokens, link receiving bank accounts) works like `domains:owner`: personal API tokens with an IP restriction only.
Request the fewest scopes you need: the customer sees each line on the consent page. `domains:owner` (get the EPP code, change the domain registrant) and `domains:registrant` ([registrant dossier of waiting domains](/en/docs/domain-registrant)) are only for [personal API tokens](/en/docs/personal-tokens); apps are never granted them. The three order commands that can charge the balance (`POST /v1/carts/{uuid}/confirm`, `POST /v1/order`, `POST /v1/order/{product_id}`) need both `orders:write` and `billing:write`.
Each route, its parameters, request and response examples: [Calling the Tino API](/en/docs/api).
## Common errors
`/oauth/token` and `/oauth/revoke` return errors as `{"error": "", "error_description": ""}`. `/oauth/userinfo` and the `/v1` gateway add the header `WWW-Authenticate: Bearer error=""`.
| Endpoint | HTTP | `error` | Cause | Fix |
| --- | --- | --- | --- | --- |
| `/oauth/token` | 401 | `invalid_client` | wrong `client_id` or `client_secret`, app disabled | check the configuration; do not send the secret via HTTP Basic |
| `/oauth/token` | 400 | `invalid_grant` | wrong code, older than 60 seconds, already used, different `redirect_uri` or `code_verifier`; access was revoked | send the customer through sign-in again from step 1 |
| `/oauth/token` (refresh) | 400 | `invalid_grant` | refresh token expired, revoked or reused (which also revokes access) | send the customer through sign-in again |
| `/oauth/token` (refresh) | 400 | `invalid_scope` | the narrowed `scope` exceeds the granted scopes | the old refresh token stays usable |
| `/oauth/token` | 400 | `unsupported_grant_type` | `grant_type` other than `authorization_code` and `refresh_token` | — |
| `/oauth/userinfo`, `/v1/*` | 401 | `invalid_token` | the access token is wrong, expired, or the customer revoked access | refresh the token; if still 401, sign in again |
| `/oauth/userinfo`, `/v1/*` | 403 | `insufficient_scope` | the token lacks the route's scope (UserInfo: missing `openid`) | request more scopes in step 1 |
| `/v1/*` | 403 | `ipnotallowed` | the calling IP is blocked by Tino | contact Tino |
| consent page | — | `access_denied` (to `redirect_uri`) | the customer clicked Deny | tell the customer, let them try again |
| consent page | — | `invalid_scope` (to `redirect_uri`) | unknown scope or outside the app's scopes | fix `scope` |
Business errors of each `/v1/*` route (for example, a service that does not exist) keep the UserAPI response, usually HTTP 200 with `{"error": [""]}`.
## References
- [Tino OpenID discovery](https://api.tino.vn/.well-known/openid-configuration): endpoints, scopes, claims running in production
- [Protected resource metadata](https://api.tino.vn/.well-known/oauth-protected-resource) (RFC 9728): for MCP clients; see [Connect Claude, ChatGPT to Tino (MCP)](/en/docs/mcp)
- [JWKS](https://api.tino.vn/oauth/jwks): public keys for verifying ID tokens
- [RFC 6749](https://www.rfc-editor.org/rfc/rfc6749) OAuth 2.0
- [RFC 7636](https://www.rfc-editor.org/rfc/rfc7636) PKCE
- [RFC 7009](https://www.rfc-editor.org/rfc/rfc7009) token revocation
- [RFC 8252](https://www.rfc-editor.org/rfc/rfc8252) native apps
- [OpenID Connect Core 1.0](https://openid.net/specs/openid-connect-core-1_0.html)
---
# Getting started with TinoPay
TinoPay is Tino's bank-transfer collection service: your customers transfer money to a linked bank account, or to a dedicated virtual account (VA); TinoPay records the transaction, sends a webhook to your system and posts notifications to Telegram, Zalo or Lark. Bank accounts, virtual accounts, transactions, webhooks and notifications live in the TinoPay system (`https://pay.tino.vn`); the service's merchant API on `api.tino.vn` relays to it. TinoPay also has a [public API](/en/docs/tinopay-api) called with an API token and [no-code integrations](/en/docs/tinopay-integrations) (Google Sheets, WordPress / WooCommerce, HostBill, Sapo).
Updated: 2026-10-25. Sources: Tino's TinoPay module and the TinoPay API v1 documentation.
## Setup
1. Order a TinoPay service on tino.vn: each service is one merchant.
2. Link a bank account. For a BIDV business account, send a [link request](/en/docs/api/tinopay-post-bank-links-bidv-business); staff set up the service with BIDV, and the account then appears in the [bank account list](/en/docs/api/tinopay-get-bank-accounts).
3. Create [virtual accounts](/en/docs/api/tinopay-post-bank-accounts-bank-virtual-accounts) when you need to separate money by order, shop or customer: the VA number is the bank account's VA prefix (`first_va_number`) plus a suffix you choose.
4. Set [payment codes](/en/docs/api/tinopay-patch-merchant) (`pay_codes`) if you want TinoPay to extract order codes from transfer descriptions.
5. Create a webhook to receive every transaction: see [Receiving TinoPay webhooks](/en/docs/tinopay-webhook).
6. To post every transaction to a chat: see [Telegram, Zalo and Lark notifications](/en/docs/tinopay-notifications). To read transactions yourself through TinoPay's [public API](/en/docs/tinopay-api): create an [API token](#api-tokens).
## Calling the merchant API
| Item | Value |
| --- | --- |
| Address | `https://api.tino.vn/service//tinopay/…` |
| Authentication | `Authorization: Bearer `: the TinoPay service owner's sign-in token; through `/v1`: an OAuth app token or personal API token with a `tinopay:*` scope |
| Body | JSON, `Content-Type: application/json` |
| Each route | the TinoPay section of the [API Reference](/en/docs/reference#tinopay) |
- `PATCH` requires a JSON body: the API does not read form bodies (`application/x-www-form-urlencoded`) on `PATCH`. With `PATCH`, send only the fields to change.
- Only the parameters listed on each route are forwarded; anything else is dropped.
- IDs in the path after the service ID (bank account, virtual account, webhook, log, transaction, notification app, API token) contain only letters, digits and `-`, up to 64 characters; a malformed ID returns 404 `RESOURCE_NOT_FOUND`.
- Timeouts: Tino waits up to 30 seconds for TinoPay per attempt and retries up to 3 times (1 and 2 seconds apart) on network errors or 5xx for requests that are safe to retry, so when TinoPay is slow one request can take about 95 seconds. Set your timeout to at least 100 seconds (or show a pending state with a cancel option). A proxy in front may cut earlier and return 504 with an HTML body, not JSON: handle it like the 502 connection errors. Finding a Zalo bot's chats always takes about 20 seconds.
Since 2026-10-27 the TinoPay routes can also be called through the `/v1` gateway: `https://api.tino.vn/v1/service//tinopay/…` with an OAuth app token (`tinoat_`, scopes `tinopay:read`, `tinopay:write`) or a [personal API token](/en/docs/personal-tokens) (`tinopat_`), with the same bodies and answers. Creating and deleting TinoPay API tokens and linking receiving bank accounts need `tinopay:owner`: personal tokens with an IP restriction only, OAuth apps never get it. Plugins and third-party software (a WooCommerce plugin, for example) should ask for access through [Sign in with Tino](/en/docs/oauth) rather than ask for the customer's sign-in token.
The sign-in token (the token a customer uses in the tino.vn client area) still calls `https://api.tino.vn/service//tinopay/…` directly; the [SDK](/en/docs/sdk) gets one with a one-time code sign-in:
```python
from tino_userapi import TinoClient
client = TinoClient()
client.request_login_otp("you@example.com")
client.verify_login_otp("you@example.com", input("One-time code: "))
print(client.request("GET", "/service/100001/tinopay/transactions", query={"type": "in"}))
```
`100001` is the TinoPay service ID: see below for finding it.
## Finding the TinoPay service
A Tino account has at most one TinoPay service, which is one merchant.
1. Identify it by product type, not by group or name (TinoPay sits in the "Dịch vụ khác" (other services) group, which other services share):
| Route (same sign-in token) | Field | TinoPay value |
| --- | --- | --- |
| `GET https://api.tino.vn/service` ([List services](/en/docs/api/get-service)) | `product_type` | `"tinopay"` |
| `GET https://api.tino.vn/service/` | `service.ptype` | `"tinopaypanel"` |
2. When opening its management, call [Merchant details](/en/docs/api/tinopay-get-merchant) (`GET /service//tinopay`):
| Result | Meaning |
| --- | --- |
| 200 | the TinoPay service is running: the merchant can be managed |
| 403 `service_not_active` | the TinoPay service is pending, cancelled or locked (Pending, Terminated, Cancelled, Fraud): show only the service status |
| 404 `invalid method` | the service is not TinoPay or does not exist: a service without TinoPay has no TinoPay routes |
| 404 `service_not_found` | a TinoPay service of another account |
| 404 `MERCHANT_NOT_FOUND` | the service is active but the merchant is still being set up; contact Tino support if it takes too long |
## Merchant status
| Service | `data.status` | Read (`GET`) | Write (create, update, delete, resend) |
| --- | --- | --- | --- |
| Active | `active` | yes | yes |
| Suspended | `suspended` | yes | 403 `MERCHANT_SUSPENDED` |
| — | `terminated` | 403 `MERCHANT_TERMINATED` | 403 `MERCHANT_TERMINATED` |
When `data.status` is `suspended` (or `data.suspended_at` is not `null`): tell the customer the service is suspended (paying the invoice reactivates it) and lock every write action. While suspended, bank transactions are still recorded but no webhooks are sent; after reactivation the bank accounts return to their previous state, and missed transactions can be reconciled with the [transaction history](/en/docs/api/tinopay-get-transactions) using `webhook_status=none`.
## Responses
Success: `{"data": …}`; paginated lists also carry `meta`. HTTP 200, 201 when something is created, 202 when a webhook resend is queued.
```json
{ "data": [], "meta": { "page": 1, "per_page": 20, "total": 135, "last_page": 7 } }
```
Paginated lists accept `page` and `per_page` (up to 100).
## API tokens
An API token calls TinoPay's [public API](/en/docs/tinopay-api), e.g. `GET https://pay.tino.vn/api/transactions` (header `Authorization: Bearer `), the same data as the API Access page of the TinoPay web app.
- [Create an API token](/en/docs/api/tinopay-post-api-tokens) (`{name}`) returns the 64-character `token` **only once**, in the 201 response: let the customer copy it, and do not store or log it. TinoPay keeps only a hash, so the [list](/en/docs/api/tinopay-get-api-tokens) has no token.
- Not retried on network errors (each call creates a token): read the list again and [revoke](/en/docs/api/tinopay-delete-api-tokens-token) any extra token. Revoking takes effect immediately.
## Errors
```json
{ "error": ["VA_NUMBER_EXISTS"], "message": "Số VA đã tồn tại.", "details": { "account_number": "9624QA01" }, "request_id": "…" }
```
Branch on `error[0]`, not on `message` (Vietnamese text that may change); use `message` only as a fallback. With `VALIDATION_FAILED`, `details` holds the per-field errors. Errors Tino returns before forwarding (`service_not_found`, `service_not_active`, `invalid method`, `RESOURCE_NOT_FOUND` for a malformed ID or a webhook or notification app of another merchant) carry only `error`. When reporting an error to Tino, include `request_id`.
| HTTP | Code | Meaning, what to do |
| --- | --- | --- |
| 400 | `INVALID_REQUEST` | wrong method or body: a programming error in the app |
| 401 | `unauthorized`, `invalid_token`, `token_expired` | the sign-in token is invalid or expired: refresh it |
| 403 | `service_not_active` | the service is neither Active nor Suspended: show only the service status |
| 403 | `MERCHANT_SUSPENDED` | the merchant is suspended: writes are blocked, `GET` still works |
| 403 | `MERCHANT_TERMINATED` | the merchant is terminated: every route is blocked |
| 404 | `service_not_found`, `invalid method` | not a TinoPay service of the signed-in account: go back to the service list |
| 404 | `MERCHANT_NOT_FOUND` | the merchant is still being set up |
| 404 | `BANK_ACCOUNT_NOT_FOUND` | the bank account does not exist or does not belong to the merchant |
| 404 | `RESOURCE_NOT_FOUND` | the virtual account, webhook, log, transaction, notification app or API token does not exist, was deleted or does not belong to the merchant; malformed ID. Reload the list |
| 409 | `VA_NUMBER_EXISTS` | the virtual account number already exists |
| 409 | `EMAIL_TAKEN` | the email belongs to another Tino customer |
| 409 | `IDEMPOTENCY_CONFLICT` | rare: the previous action is still being processed, retry in a few seconds |
| 422 | `VALIDATION_FAILED` | invalid data; `details` lists the errors per field |
| 429 | `RATE_LIMITED` | over 300 requests per minute per merchant: retry after a minute |
| 500 | `INTERNAL_ERROR` | TinoPay-side error: retry later, report it to Tino with `request_id` |
| 502 | `NETWORK_ERROR`, `NOT_CONFIGURED`, `UNEXPECTED_RESPONSE`, `MISSING_API_KEY`, `INVALID_KEY`, `IP_NOT_ALLOWED` | the connection between Tino and TinoPay is interrupted: a Tino-side error, not a token or session problem. Do not drop the token or sign the customer out; retry later, and report it to Tino with `request_id` if it persists |
| 504 | (HTML body) | a proxy cut a slow request before the answer: handle it like 502, retry later |
Codes not in the table: show `message` (when present) or a generic message, with `request_id`.
## Notes
- `POST` is not deduplicated for you: repeating a `POST` (for example creating a virtual account) is a new request. Read the list again before retrying.
- The merchant's name, email and phone are not synced with the customer's Tino profile: they change only through [Update merchant details](/en/docs/api/tinopay-patch-merchant).
- A webhook's `token` is the customer's secret: when showing it in a UI, mask it and reveal it only when the customer asks.
- Finding chats and sending test messages for notifications, and creating API tokens, are not retried on network errors: let the user try again.
---
# Getting started with Tino Uptime
Tino Uptime checks websites, servers, service ports, DNS records, SSL certificates and domain expiry from probes in Vietnam and abroad. When a target fails, Tino Uptime opens an incident and alerts by email, Telegram, Discord, Slack, webhook or Zalo. The service API lets your application manage monitors and read uptime, check results, incidents and alert channels of a Tino Uptime plan.
Updated: 2026-10-24. Sources: Tino's Tino Uptime module and the Tino Uptime OpenAPI.
## Calling the API
| Item | Value |
| --- | --- |
| Address | `https://api.tino.vn/service//uptime/…` |
| Authentication | `Authorization: Bearer `: the Tino Uptime service owner's sign-in token |
| Body | JSON, `Content-Type: application/json` |
| Each route | the Tino Uptime section of the [API Reference](/en/docs/reference#tino-uptime) |
- The body is always JSON. With [Update a monitor](/en/docs/api/uptime-put-monitors-monitor), send only the fields to change.
- Only the parameters listed on each route are relayed. Tino Uptime refuses unknown body fields (422 `validation_failed`).
- Monitor, incident and channel IDs in the path are UUIDs; a malformed one answers 400 `invalid_request`.
- What this API does not cover (status pages, servers, WordPress, CVE alerts) opens in the Tino Uptime dashboard through the `login_url` of the [plan overview](/en/docs/api/uptime-get-overview).
The `/v1` gateway does not expose the Tino Uptime routes yet: app tokens (`tinoat_`) and [personal API tokens](/en/docs/personal-tokens) (`tinopat_`) cannot call them. The sign-in token is the one customers use in the tino.vn client area; the [SDK](/en/docs/sdk) gets it with an OTP sign-in:
```python
from tino_userapi import TinoClient
client = TinoClient()
client.request_login_otp("you@example.com")
client.verify_login_otp("you@example.com", input("OTP code: "))
print(client.request("GET", "/service/100002/uptime/monitors", query={"state": "down"}))
```
`100002` is the Tino Uptime service ID: how to find it is below.
## Find the Tino Uptime service
Each Tino account has one Tino Uptime account; the current plan belongs to exactly one service.
1. Identify it by the service's module: [Service details](/en/docs/api/get-service-id) (`GET https://api.tino.vn/service/`, same sign-in token) has `service.acc_module` set to `"tino_uptime"`. Candidates come from the [service list](/en/docs/api/get-service).
2. Call the [plan overview](/en/docs/api/uptime-get-overview) (`GET /service//uptime`):
| Result | Meaning |
| --- | --- |
| 200 | the customer's current Tino Uptime plan: call the other routes with this ID |
| 404 `service_not_linked` | the service is no longer the current plan (the customer moved to another service): try the next Tino Uptime service |
| 404 `invalid method` | the service is not Tino Uptime or does not exist |
| 404 `service_not_found` | the service belongs to another account |
| 404 `account_not_found` | no Tino Uptime account for the service yet: contact Tino support |
## Plan limits
Limits are in `subscription.limits` of the [plan overview](/en/docs/api/uptime-get-overview), current numbers in [usage](/en/docs/api/uptime-get-usage). Read the limits from the API, do not hard-code them: each plan's numbers may change.
| Field | Meaning |
| --- | --- |
| `max_monitors` | maximum number of monitors |
| `min_interval_s` | shortest check interval (seconds) |
| `raw_retention_days` | days of per-check results you can read |
| `locations` | probe locations you can use: `vn-cmc`, `vn-vt` (Vietnam), `intl` (international) |
| `channels` | alert channel types you can create |
Going over a limit answers 403 `entitlement_exceeded`; `details.resource` names the limit (`monitors`, `min_interval_s`, `locations`, `channels`). While the service is suspended, the Tino Uptime account is read-only: writes answer 403 `account_suspended`. After a downgrade, monitors over the limit are paused (`disabled_reason: paused_by_plan`, counted in `monitors_paused_by_plan`).
## Monitor types
[Create a monitor](/en/docs/api/uptime-post-monitors) takes `name`, `interval_s` (30–86400 seconds, not below `limits.min_interval_s`), `locations` and `settings`. `settings.type` decides the other fields:
| `type` | Required | Optional |
| --- | --- | --- |
| `http` | `url` | `method` (`GET`, `HEAD`, `POST`), `expected_status` (array of HTTP codes, up to 20), `follow_redirects` (default `true`), `max_latency_ms` (1–60000), `headers` (object), `body` (up to 4096 characters), `basic_auth` (`username`, `password`) |
| `keyword` | `url`, `keyword` (1–100 characters) | `mode` (`present`, `absent`), `case_sensitive`, `method` (`GET`, `POST`) and the other `http` options |
| `ssl` | `host` | `port` (443, 465, 993, 995), `warn_days` (1–90, default 14) |
| `domain` | `domain` | `warn_days` (1–120, default 30) |
| `ping` | `host` | — |
| `tcp` | `host`, `port` (1–65535) | — |
| `dns` | `name`, `record_type` (`A`, `AAAA`, `CNAME`, `MX`, `NS`, `TXT`) | `expected` (array of values, up to 20), `resolver` (an IP, or an IP with port 53; empty asks the authoritative name servers) |
| `heartbeat` | — | `grace_s` (0–86400, default 300) |
- `heartbeat` works the other way round: your system calls `settings.ping_url` after each run. `ping_url` is only in the create answer: store it right away.
- Headers, body and the Basic auth password are never returned; `settings` only tells `header_names`, `body_set`, `basic_auth_set`.
- `international_critical: true`: a failure seen only by the international probe also counts as down (default `false`).
- Internal or unsafe targets are refused: 422 `validation_failed` with `details.reason: blocked_destination`. A target checked too often (counting other customers) answers 422 `target_rate_exceeded`: pick a longer interval.
A monitor's `state`: `pending` (no result yet), `up`, `suspect`, `down`, `partial`, `down_international`, `paused` (turned off, stopped by the system per `disabled_reason`, or within `paused_until`).
## Alert channels
[Create a channel](/en/docs/api/uptime-post-channels) with a `kind` from the plan's `limits.channels`; a new channel awaits verification (except an email to the account owner's own verified address). A channel without `opted_in_at` receives nothing.
| `kind` | `config` | Verification (`verification.method`) |
| --- | --- | --- |
| `email` | `address` | `email_code`: send the code from the email to [Verify an alert channel](/en/docs/api/uptime-post-channels-channel-verify) (`code`); the code is valid 24 hours |
| `telegram` | — | `telegram_link`: open `telegram_url` (or `telegram_group_url` for a group) within 10 minutes |
| `discord`, `slack`, `webhook` | `url` (https) | `test_message`: call Verify to send a test message; the channel is verified when it gets through |
| `zalo` | `consent: true`; `phone` to verify by OTP, no `phone` to link through the OA | `zalo_otp` or `zalo_link` (send `zalo_link_code` to the OA at `zalo_oa_url`, valid 10 minutes) |
- A webhook channel returns `webhook_secret` (`whsec_…`) once only, in the create answer: store it to check the signature of incoming messages.
- Verification emails: at most 5 an hour per account and 3 a day per address; beyond that 429 `rate_limited`. A wrong code answers 422 `token_invalid`.
- [Send a test message](/en/docs/api/uptime-post-channels-channel-test) works only for a verified, enabled channel; otherwise 409 `conflict`.
## Lists and safe retries
Lists (monitors, incidents, channels, per-check results) use cursor pagination: send the previous page's `next_cursor` as `cursor`; `next_cursor` is `null` at the end. `limit` defaults to 50, at most 100 (per-check results: default 100, at most 1000).
```json
{ "data": { "data": [], "next_cursor": null } }
```
[Create a monitor](/en/docs/api/uptime-post-monitors) and [Create an alert channel](/en/docs/api/uptime-post-channels) take `idempotency_key` (8–128 characters `A-Za-z0-9._:-`). If the connection drops midway, resend the same body with the same key: nothing extra is created. The same key with a different body, or while the earlier call is still running, answers 409 `idempotency_conflict`. Without a key every call is a new command: read the list again before retrying.
## Responses
Success: `{"data": …}`, where `data` is Tino Uptime's answer. HTTP 200, 201 on create, 202 once a test message is queued, 204 on delete (no content). Times are RFC 3339; IDs are UUIDs.
## Errors
```json
{ "error": ["entitlement_exceeded"], "message": "Đã vượt hạn mức của gói.", "details": { "resource": "monitors" }, "request_id": "…" }
```
Branch on `error[0]`, not on `message` (a Vietnamese sentence that may change). With `validation_failed`, `details.field` names the wrong field and `details.reason` the reason when there is one. Errors Tino returns before relaying (`service_not_found`, `invalid method`) only have `error`. When reporting a problem to Tino, include `request_id`.
| HTTP | Code | Meaning, what to do |
| --- | --- | --- |
| 400 | `invalid_request` | malformed ID (not a UUID), malformed `idempotency_key`, or a bad request: a bug on the application side |
| 401 | `unauthorized`, `invalid_token`, `token_expired` | the sign-in token is wrong or expired: refresh it |
| 403 | `account_suspended` | the Tino Uptime account is suspended: writes are blocked, `GET` still works |
| 403 | `entitlement_exceeded` | over a plan limit, `details.resource` names it: upgrade or remove monitors, channels |
| 404 | `service_not_found`, `invalid method` | not a Tino Uptime service of the signed-in account: go back to the service list |
| 404 | `service_not_linked` | the service is no longer the current plan: find the service again ([Find the Tino Uptime service](#find-the-tino-uptime-service)) |
| 404 | `account_not_found` | no Tino Uptime account yet: contact Tino support |
| 404 | `not_found` | the monitor, incident or channel does not exist or was deleted: reload the list |
| 409 | `conflict` | not possible in the current state (e.g. a test message to an unverified channel): reload and retry |
| 409 | `idempotency_conflict` | same `idempotency_key` with a different body, or the earlier call is still running |
| 422 | `validation_failed` | invalid data: show the error under the field named by `details.field` |
| 422 | `target_rate_exceeded` | the target is checked too often (counting other customers): pick a longer interval |
| 422 | `token_invalid` | the email channel's verification code is wrong or expired |
| 429 | `rate_limited` | too fast: retry later |
| 502, 503 | `network`, `invalid_response`, `not_configured`, `unavailable` | the connection between Tino and Tino Uptime is interrupted: a Tino-side error, not a token problem. Do not sign out, retry later |
---
# Connect Claude, ChatGPT to Tino (MCP)
Tino Connector lets AI assistants (Claude, ChatGPT, other MCP apps) work with a Tino account: view and manage hosting services, VPS, domains, DNS records, invoices and tickets with plain-language requests, only within the scopes the customer approved. The connection uses the MCP (Model Context Protocol) standard and OAuth sign-in: the assistant never sees the customer's password or OTP code.
Updated: 2026-10-27. MCP server source code: the public repository [tinovn/tino-connector-mcp](https://github.com/tinovn/tino-connector-mcp).
## Overview
| Item | Value |
| --- | --- |
| MCP server | `https://api.tino.vn/mcp` |
| Protocol | MCP Streamable HTTP: each `POST` is one JSON-RPC message and returns JSON (no SSE, no sessions); `GET` returns 405 |
| Protocol versions | `2025-06-18`, `2025-03-26`, `2024-11-05` |
| Authentication | `Authorization: Bearer `: an OAuth token ([Sign in with Tino](/en/docs/oauth)) or a [personal API token](/en/docs/personal-tokens) |
| Client metadata (RFC 9728) | `https://api.tino.vn/.well-known/oauth-protected-resource` |
| Client self-registration (RFC 7591) | `POST https://api.tino.vn/oauth/register` (`registration_endpoint` in `https://api.tino.vn/.well-known/oauth-authorization-server`) |
| Claude Code plugin | marketplace `tinovn/tino-connector-mcp`, plugin `tino@tinovn` |
## Connecting
### Claude (web, Desktop, mobile)
1. Open Claude, go to **Customize → Connectors**, and click **Add custom connector**.
2. Name: `Tino`; URL: `https://api.tino.vn/mcp`.
3. Leave authentication at the default (OAuth sign-in); no client ID or header is needed.
4. Click **Connect**: the Tino sign-in page opens (see [Signing in and granting access](#signing-in-and-granting-access)).
Depending on the Claude plan, an organization admin may need to enable the connector first.
### Claude Code
Install the `tino` plugin:
```bash
claude plugin marketplace add tinovn/tino-connector-mcp
```
```bash
claude plugin install tino@tinovn
```
In Claude Code, type `/mcp`, select `tino`, and click **Authenticate**. Without the plugin, add the MCP server directly, then also run `/mcp` → **Authenticate**:
```bash
claude mcp add --transport http tino https://api.tino.vn/mcp
```
Claude Code does not register a client: it identifies itself with a Client ID Metadata Document (`client_id=https://claude.ai/oauth/claude-code-client-metadata`). By default Claude Code requests every scope the MCP server lists, write scopes included; for narrower access, use a personal API token (see [Other MCP apps](#other-mcp-apps-cursor-vs-code-n8n)).
### ChatGPT
ChatGPT accounts with Developer mode enabled can add a custom MCP connector: URL `https://api.tino.vn/mcp`, OAuth authentication. The exact steps depend on the version of the ChatGPT interface.
### Other MCP apps (Cursor, VS Code, n8n…)
**Main option: OAuth sign-in.** An MCP client that supports OAuth as the MCP specification describes registers itself with Tino, then opens the sign-in page; you only enter the URL `https://api.tino.vn/mcp`, with no client ID, secret or header:
1. Calling `https://api.tino.vn/mcp` without a token returns HTTP 401; the `WWW-Authenticate` header points to the metadata at `https://api.tino.vn/.well-known/oauth-protected-resource`.
2. The client reads the discovery document `https://api.tino.vn/.well-known/oauth-authorization-server` and registers itself at the `registration_endpoint` (`POST https://api.tino.vn/oauth/register`, RFC 7591): a public client (`token_endpoint_auth_method: none`) redirecting to `http://localhost` or `127.0.0.1` (any port) or `https://`. Field rules are in [Sign in from a CLI](/en/docs/cli-login#registering-an-app).
3. The client opens the Tino sign-in page (Authorization Code + PKCE, see [Sign in with Tino](/en/docs/oauth)); the customer approves as described in [Signing in and granting access](#signing-in-and-granting-access).
On the Authorized apps page, a self-registered app appears under the name it registered (`client_name`).
**Alternative: personal API token.** Use it when the app does not support MCP OAuth, runs on a server that cannot open a browser, or needs narrower scopes or an IP restriction:
1. Create a token at `https://auth.tino.vn/en/me/security`, in the **API tokens** section: choose exactly the scopes you need; if the app runs on a fixed server, restrict the token to its IP. See [Personal API tokens](/en/docs/personal-tokens).
2. Configure the MCP server in the app: URL `https://api.tino.vn/mcp`, header `Authorization: Bearer `. A common JSON configuration format:
```json
{
"mcpServers": {
"tino": {
"type": "http",
"url": "https://api.tino.vn/mcp",
"headers": { "Authorization": "Bearer tinopat_…" }
}
}
}
```
The token is a key to the account: do not paste it into the chat, do not commit it to Git; if it leaks, revoke it immediately on the API tokens page.
## Signing in and granting access
When you connect, the browser opens a Tino page: the customer signs in as usual (OTP code by email or phone number, Google, Zalo or passkey), reviews the list of scopes the app requests, then clicks **Approve**. The app receives a time-limited access token (24 hours by default) and a refresh token to renew it automatically; if the refresh token is not used before it expires (30 days by default), the customer must reconnect.
Approving grants every scope in the list. For narrower access, use a personal API token with only the scopes you need.
## Scopes and tools
Each tool is a route of the `/v1` gateway (see [Calling the Tino API](/en/docs/api)); a tool's description ends with its route and scope, for example `GET /details · scope profile:read`. `tools/list` returns only the tools the token can call, so the assistant does not see tools outside the granted scopes.
| Scope (label on the consent page) | What the assistant can do |
| --- | --- |
| View account details | Profile (without ID documents, phone number, address, tax and bank details), notifications |
| Edit account details | Mark notifications as read |
| View services, including service login details | List and details of services and VPS, resources, SSL certificates; **may include service login passwords** |
| Manage your services | Start, shut down, restart VPS, change hostname, renew, order SSL certificates |
| View domains | Domain details, nameservers, transfer lock, auto-renewal |
| Manage your domains | Change nameservers, order, renew, turn locks on or off, forwarding |
| View DNS records | Read DNS zones and records |
| Edit DNS records | Add, edit, delete DNS zones and records |
| View invoices and payments | Invoices, balance, transaction history |
| Make payments and top up | Pay invoices with the **Tino account balance**, create top-up requests |
| View support tickets | View tickets |
| Create and reply to tickets | Create, reply to, close, reopen tickets |
| View TinoPay: receiving accounts, transactions, webhooks | Merchant details, bank accounts, virtual accounts, transactions and totals, webhooks and their delivery log, notification apps, payment code patterns, Sapo integrations and their reconciliation log |
| Manage TinoPay: virtual accounts, webhooks, notifications, Sapo integration | Create, activate, deactivate, delete virtual accounts; add, edit, delete and resend webhooks; Telegram, Zalo, Lark notification apps; Sapo integrations; edit the merchant and payment code patterns |
| View TinoPage Builder websites | Service overview, list and details of websites, status of a running website creation, custom domains, contacts and orders |
| Manage TinoPage Builder websites: create, publish, custom domains | Create websites with AI, publish, pause, delete websites; add, check DNS of, remove custom domains |
| View websites protected by TinoWAF and the event log | Service overview, websites, domain names and the DNS records to set, origin servers, event log and summary, IP block/allow list |
| Manage TinoWAF: websites, protection level, origin servers, IP rules | Add, delete websites; change the protection level, turn emergency anti-attack mode on or off; add, remove domain names, check DNS; edit origin servers; block or allow IPs |
| Keep access while you're offline | Keep the connection (renew the access token automatically) |
Tools with a read scope carry the annotation `readOnlyHint: true`; tools with a write scope carry `destructiveHint: true`. The tools for email forwarding, domain forwarding, registering private nameservers and getting payment content from a payment gateway also carry `openWorldHint: true` (they affect external systems).
## Not available through MCP
- **Cancelling services:** only on the Tino customer control panel; MCP has no cancel tool, even when the token has the service management scope.
- **Getting the domain transfer code (EPP), changing the domain registrant:** never granted to Claude, ChatGPT or any OAuth app. Only a personal API token with the `domains:owner` scope (IP restriction required) has these two tools; choose that scope only when you really need it.
- **Creating TinoPay API tokens, linking receiving bank accounts:** likewise, only a personal API token with the `tinopay:owner` scope (IP restriction required) has these tools.
- **Ordering new hosting or VPS:** not supported yet. Ordering domains and SSL certificates is supported.
## Example requests
- "List my Tino services and their renewal dates."
- "Which nameservers does example.vn use?"
- "Add an A record `www` pointing to 203.0.113.10 for example.vn."
- "Do I have any unpaid invoices?"
- "Restart the VPS with hostname web01." (write action: the assistant should ask for confirmation first)
## Safety and data
- Every change (orders, renewals, payments, DNS or nameserver changes, VPS restarts) is marked as a write action; the MCP server also tells the assistant to state what it will do and wait for the customer's approval. The confirmation step is done by Claude or ChatGPT according to the app's settings: keep the ask-before-running mode on and read the details carefully before approving.
- The assistant only reads and writes through Tino's API within the granted scopes; results are sent to Claude or ChatGPT to answer the customer.
- The MCP server filters identity fields out of every person record (objects with `email` plus `firstname`, `lastname` or `name`: accounts, contacts, domain registrants) before sending them to the assistant: ID document numbers, date of birth, gender, phone number, address, tax code, household business code, bank, Zalo link, IP and sign-in times. Calling the `/v1` gateway directly returns the response unchanged (see [Calling the Tino API](/en/docs/api#personal-data)).
- [Privacy policy](https://tino.vn/chinh-sach-bao-mat) · [Terms of use](https://tino.vn/dieu-khoan-su-dung-dich-vu).
## Revoking access
The customer goes to `https://auth.tino.vn/en/me/apps` (Authorized apps) and clicks Revoke on "Claude", "Claude Code", "ChatGPT" or the name of a self-registered MCP app: every token of that app stops working immediately. Personal API tokens are revoked at `https://auth.tino.vn/en/me/security`, in the API tokens section.
## Troubleshooting
| Symptom | Fix |
| --- | --- |
| Claude Code says authentication is needed (Needs authentication) | Type `/mcp`, select `tino`, click **Authenticate** |
| The connection works for a while, then asks you to sign in again | Reconnect: the refresh token expired after a long time without use, or the customer revoked access |
| The Tino sign-in page says the app is invalid | Send a screenshot to support@tino.vn |
| Another MCP app cannot sign in | The app must support MCP OAuth (client self-registration); if it does not, or it runs on a server that cannot open a browser, use a personal API token (see Other MCP apps) |
| Self-registration returns `429 too_many_requests` | Each IP can register at most 10 times per hour: the client should store its registered `client_id` and reuse it |
| With a personal token, the assistant does not see a tool it needs | The token lacks that scope: create a new token with the required scopes |
| The assistant reports `insufficient_scope` | The tool does not apply to that resource with the granted scopes, for example a VPS command on a hosting plan |
| HTTP 403 `ip_not_allowed` | The personal token has an IP restriction and is being used from another IP |
| No command to cancel services | By design: cancel on the Tino control panel |
## For MCP client developers
Try it with a personal API token (`$TINO_TOKEN`). Each message is one JSON-RPC 2.0 `POST`; with no token or a wrong token you get HTTP 401 with the header `WWW-Authenticate: Bearer error="invalid_token", scope="…", resource_metadata="https://api.tino.vn/.well-known/oauth-protected-resource"` so the client can start OAuth.
```bash
curl -X POST https://api.tino.vn/mcp \
-H "Authorization: Bearer $TINO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 1, "method": "tools/list"}'
```
Calling a tool: the route's path parameters are required arguments; other arguments go into the query (`GET`, `DELETE`) or the body (`POST`, `PUT`).
```bash
curl -X POST https://api.tino.vn/mcp \
-H "Authorization: Bearer $TINO_TOKEN" \
-H "Content-Type: application/json" \
-d '{"jsonrpc": "2.0", "id": 2, "method": "tools/call", "params": {"name": "service_details", "arguments": {"id": "100001"}}}'
```
- Supported methods: `initialize`, `ping`, `tools/list`, `tools/call`; messages without an `id` (notifications) get HTTP 202.
- The result of `tools/call` is the route's response: JSON as `text`, with `isError: true` when the route reports an error (HTTP 4xx, 5xx or an `error` field); files (for example invoice PDFs) are returned as an embedded base64 `resource`.
- Tool names are lowercase with underscores (`get_details`, `service_details`, `dns_add_record`…) and may change when the API adds routes; get the list with `tools/list` instead of hard-coding it.
---
# Personal API tokens
Personal API tokens let the customer's own software (a WHMCS module, a domain management script, your own tool…) call the Tino API without signing in. Customers create tokens themselves in Tino ID and choose the scopes, expiry and allowed IPs; the software sends the token in the header `Authorization: Bearer tinopat_…` to the gateway `https://api.tino.vn/v1/`.
Updated: 2026-10-27.
## When to use
| You need | Use |
| --- | --- |
| Your own software calls the API with your Tino account | Personal API token (`tinopat_`) |
| Your app serves many Tino customers, each of whom signs in and approves | [Sign in with Tino](/en/docs/oauth) (`tinoat_`) |
| Claude, ChatGPT work with your Tino account | [MCP connection](/en/docs/mcp): OAuth sign-in, no token needed |
| Other MCP apps (Cursor, VS Code, n8n…) | [MCP connection](/en/docs/mcp#other-mcp-apps-cursor-vs-code-n8n): OAuth sign-in (the app registers itself); a personal API token when the app does not support MCP OAuth or runs on a server that cannot open a browser |
Only personal API tokens can carry the `domains:owner` scope (get the EPP code, change the domain registrant); OAuth apps are never granted this scope.
## Creating a token
1. Sign in to Tino ID, open `https://auth.tino.vn/en/me/security`, go to the **API tokens** section, and click **Create token**.
2. Name the token (1–64 characters), for example "WHMCS module".
3. Choose scopes: at least one scope from the table below. When you choose the write scope of a group, the interface also selects the read scope of the same group.
4. Choose the expiry: 30 days, 90 days, 1 year (default) or no expiry.
5. IP restriction (optional, unless you choose `domains:owner`): IPv4 or IPv6 addresses or CIDR ranges, separated by spaces or commas, up to 20. Empty: the token can be used from any IP.
6. Copy the token right away: it is shown only once, when it is created. Tino stores only a SHA-256 hash; the token list keeps only the last 4 characters so you can recognize it.
Each sign-in account can have up to 20 tokens. A token has the form `tinopat_` + 43 characters (base64url).
| Scope | Allows |
| --- | --- |
| `profile:read` | View account details |
| `profile:write` | Edit account details |
| `services:read` | View services, including service login details |
| `services:write` | Service management |
| `domains:read` | View domains |
| `domains:write` | Register, renew and manage domains |
| `domains:owner` | Get domain transfer codes (EPP) and change domain registrants; IP restriction required |
| `domains:registrant` | View and complete the registrant dossier of domains awaiting registration (includes ID number, date of birth); IP restriction required |
| `dns:read` | View DNS records |
| `dns:write` | Edit DNS records |
| `billing:read` | View invoices and payments |
| `billing:write` | Make payments and top up |
| `orders:write` | Order services: cart, quotes, create orders (paying from balance also needs the Payments permission) |
| `tickets:read` | View support tickets |
| `tickets:write` | Create and reply to tickets |
| `tinopay:read` | View TinoPay: receiving accounts, transactions, webhooks |
| `tinopay:write` | Manage TinoPay: virtual accounts, webhooks, notifications, Sapo integration |
| `tinopay:owner` | Create TinoPay API tokens and link receiving bank accounts; IP restriction required |
| `builder:read` | View TinoPage Builder websites |
| `builder:write` | Manage TinoPage Builder websites: create, publish, custom domains |
| `waf:read` | View websites protected by TinoWAF and the event log |
| `waf:write` | Manage TinoWAF: websites, protection level, origin servers, IP rules |
Each scope opens a group of routes; see the routes of each scope at [Calling the Tino API](/en/docs/api).
## `domains:owner` and IP restriction
`domains:owner` opens [get the EPP code](/en/docs/api/get-domain-id-epp) (`GET /v1/domain/@id/epp`), [change domain contacts and registrant](/en/docs/api/put-domain-id-contact) (`PUT /v1/domain/@id/contact`) The four [registrant dossier routes of waiting domains](/en/docs/domain-registrant) have their own scope, `domains:registrant`, also personal tokens with an IP restriction only. Anyone with the two `domains:owner` routes can transfer the domain elsewhere, so a token with `domains:owner` must have an IP list: Tino refuses to create the token when the list is empty. A leaked token still cannot transfer domains from another machine.
When called from an IP outside the list, every route of the token returns 403 `ip_not_allowed`.
## Calling the API
```bash
curl https://api.tino.vn/v1/domain -H "Authorization: Bearer tinopat_…"
```
The token calls the API as the account that created it, only within the chosen scopes. See the body, parameters and response of each route in the [API Reference](/en/docs/reference).
Keep the token like a password: store it in an environment variable or the server's secret store, never in source code, URLs or the browser.
## Monitoring and revoking
The token list at `https://auth.tino.vn/en/me/security` shows the name, scopes, allowed IPs, creation time, last use (updated at most once per minute) and expiry. Expired tokens show the **Expired** label.
Click **Revoke** to delete a token: software using it gets 401 `invalid_token` on its next call. To change the scopes, expiry or IPs, create a new token, then revoke the old one.
## Errors
| HTTP | `error` | Cause | Fix |
| --- | --- | --- | --- |
| 401 | `invalid_token` | the token is wrong, expired or revoked | create a new token |
| 403 | `ip_not_allowed` | called from an IP outside the token's allowed list | call from a listed IP, or create a token with a new IP list |
| 403 | `insufficient_scope` | the token lacks the route's scope | create a token with more scopes |
| 403 | `ipnotallowed` | the calling IP is blocked by Tino | contact Tino |
When you create a token, Tino shows an error if the name is empty or longer than 64 characters, no scope is chosen, the expiry is invalid, an IP or IP range is invalid (or there are more than 20), `domains:owner` is chosen without IPs, or the account already has 20 tokens.
---
# Test environment
Tino does not have a separate test environment (sandbox) for the API yet: every route runs on the customer's real account, services and money. This page describes how to test safely under that condition.
Updated: 2026-10-21.
## Testing with a read-only token
When exploring the API, create a [personal API token](/en/docs/personal-tokens) with only the `…:read` scopes you need, a 30-day expiry, and an IP restriction to the machine you test from. Write routes then return 403 `insufficient_scope` instead of making real changes.
```bash
curl https://api.tino.vn/v1/service -H "Authorization: Bearer $TINO_TOKEN"
```
Revoke the token as soon as you finish testing, at `https://auth.tino.vn/en/me/security`.
## Write routes have real effects
| Action | Real effect |
| --- | --- |
| Ordering or renewing domains, services, SSL certificates | creates an order and an invoice |
| Paying with the balance, paying invoices with the balance | deducts money from the account |
| Topping up | creates a top-up invoice |
| Changing nameservers, editing or deleting DNS records | the domain's website and email change once DNS propagates |
| Starting, stopping, restarting, resetting a VPS | services on the VPS are interrupted |
| Creating or replying to tickets | Tino staff receive and handle them |
Test DNS actions on a domain or zone that no real website uses, or use a `TXT` record with its own name and delete it afterwards.
## Check before you act
- `GET /v1/service/@id/methods`: the actions the service supports, before you call them.
- `POST /v1/service/@id/upgrade` without `send=true`: only calculates the upgrade price; the request is not sent.
- `POST /v1/domain/lookup`: checks whether a domain is available and its price, before you order.
## Running tests without touching a real account
Run a mock server from the [OpenAPI](/en/openapi.json) document (for example with Prism or another OpenAPI tool): routes with complete sample responses have them attached as `example`. The app's tests call the mock; only the final check calls the real API with a read-only token.
## AI assistants through MCP
When you connect Claude or ChatGPT to Tino through [MCP](/en/docs/mcp), keep the ask-before-running mode on for write actions and read the details carefully before approving.
---
# Receiving TinoPay webhooks
For every incoming (or outgoing) transaction on a linked bank account, TinoPay sends a `POST` request to your webhook URL; the JSON body is the transaction, and headers carry the webhook's token so you can verify it. This is the recommended integration for flows that need instant payment confirmation (payment gateways, automatic top-ups, order confirmation), instead of polling the API.
Updated: 2026-10-25. Sources: the [TinoPay webhook documentation](https://pay.tino.vn/docs/webhooks) and the TinoPay API v1 documentation.
## Flow
1. The customer transfers money to a bank account connected to TinoPay.
2. TinoPay records the transaction (incoming or outgoing).
3. TinoPay sends a `POST` with the transaction data (JSON) to the URL of every matching webhook.
4. Your server answers HTTP 2xx: TinoPay marks the delivery successful. Otherwise TinoPay sends it again, see [Responses and retries](#responses-and-retries).
## Creating a webhook
- In the TinoPay web app: menu Tích hợp (Integrations) → Webhooks → Thêm webhook (Add webhook); the Tạo ngẫu nhiên (Generate) button creates a 50-character token.
- With the merchant API: the [Create a webhook](/en/docs/api/tinopay-post-webhooks) route (see [Getting started with TinoPay](/en/docs/tinopay-start) for calling the API).
| Field | Required | Meaning |
| --- | --- | --- |
| `name` | yes | webhook name, up to 250 characters |
| `bank_account_id` | yes | the bank account to watch |
| `url` | yes | the receiving URL, up to 250 characters; use HTTPS |
| `event` | yes | the triggering event, table below |
| `token` | no | verification string; through the API, leave empty to get a generated 50-character token |
| `virtual_accounts` | no | array of that bank account's virtual account numbers; when set, the webhook fires only for transactions to these virtual accounts; empty: every virtual account |
| `skip_if_no_code` | no | `true`: skip transactions without an extracted payment code |
| `status` | no | `active` (default), `inactive`; only active webhooks are sent |
| `event` | Fires on |
| --- | --- |
| `in` | incoming transactions (credit): money received, used to confirm payments |
| `out` | outgoing transactions (debit) |
| `all` | every transaction, incoming and outgoing |
Most payment integrations only need `in` (the web app's default). You can create several webhooks for different bank accounts and systems.
## The request TinoPay sends
A `POST` to the webhook's `url` with a JSON body, carrying the webhook's token in two headers with the same value:
```http
POST /your-webhook-endpoint HTTP/1.1
Content-Type: application/json
Authorization: Apikey
ALLPAY-TOKEN:
```
| Header | Value |
| --- | --- |
| `Content-Type` | `application/json` |
| `Authorization` | `Apikey ` |
| `ALLPAY-TOKEN` | ``: the header name TinoPay sends, keep it as is |
Checking either header is enough; prefer `Authorization`.
```json
{
"transaction_id": "20261020083338000006",
"gateway": "BIDV",
"transactionDate": "2026-10-20 08:33:38",
"accountNumber": "96240000001",
"code": "DH12345",
"content": "DH12345 chuyen tien mua hang",
"transferType": "in",
"transferAmount": 150000,
"referenceCode": "0551dTCO-82LiomOss",
"virtualAccount": "9624QA01"
}
```
| Field | Type | Meaning |
| --- | --- | --- |
| `transaction_id` | string \| null | transaction code at TinoPay |
| `gateway` | string | source bank code, e.g. `BIDV` |
| `transactionDate` | string (`Y-m-d H:i:s`) | when the transaction happened |
| `accountNumber` | string | the bank account number that received or made the transaction |
| `code` | string \| null | payment code extracted from the transfer description with your code structure; `null` when none is found |
| `content` | string | the transfer description, verbatim from the bank |
| `transferType` | string | `in` incoming, `out` outgoing |
| `transferAmount` | int | amount (VND) |
| `referenceCode` | string \| null | the bank's reference code (core reference) |
| `virtualAccount` | string \| null | the virtual account that received the money; `null` when the transfer did not go through one |
`code` lets you match a transaction with an order: use the order code as the transfer description (e.g. a [QR code](/en/docs/tinopay-api#vietqr-codes) with `memo=DH12345`) and TinoPay extracts it into `code`. The code structure is set in the TinoPay web app (Cài đặt → Cấu trúc mã thanh toán, Settings → Payment code structure) or through `pay_codes` of [Update merchant details](/en/docs/api/tinopay-patch-merchant).
## Verifying and processing
The webhook is public on the Internet, so check that every request really comes from TinoPay. TinoPay uses a shared token, not an HMAC signature; the token travels in plain text in a header, so the webhook URL must always be HTTPS.
1. Compare the token in the `Authorization` header (without the `Apikey ` prefix) with the webhook's token using a constant-time comparison; if they differ, answer 401 and ignore the request.
2. Deduplicate on `transaction_id` (or `referenceCode`): one transaction can arrive more than once (retries, manual resends). If it was already processed, skip it but still answer 200.
3. Check `transferType` so you only handle the direction you need.
4. Check `transferAmount` against the order found by `code`, `virtualAccount` or `content` before marking it paid.
5. Answer 2xx as soon as you receive it and queue heavy work.
Node.js (18+), no external libraries:
```javascript
import { createServer } from "node:http";
import { timingSafeEqual } from "node:crypto";
const token = Buffer.from(process.env.TINOPAY_WEBHOOK_TOKEN);
const seen = new Set(); // in production: store transaction_id in your database
const valid = (header) => {
const got = Buffer.from(String(header ?? "").replace(/^Apikey /, ""));
return got.length === token.length && timingSafeEqual(got, token);
};
createServer((req, res) => {
if (req.method !== "POST" || !valid(req.headers.authorization)) return res.writeHead(401).end();
let raw = "";
req.on("data", (chunk) => (raw += chunk)).on("end", () => {
const tx = JSON.parse(raw);
if (!seen.has(tx.transaction_id)) {
seen.add(tx.transaction_id);
// record the payment: tx.code, tx.transferAmount, tx.virtualAccount
}
res.writeHead(200).end("ok");
});
}).listen(3000);
```
PHP:
```php
. Some servers (Apache + PHP-FPM) do not pass the Authorization header to PHP:
// then read the ALLPAY-TOKEN header, which carries the same value.
$auth = $_SERVER["HTTP_AUTHORIZATION"] ?? "";
$got = substr($auth, 0, 7) === "Apikey " ? substr($auth, 7) : ($_SERVER["HTTP_ALLPAY_TOKEN"] ?? "");
if (!hash_equals($token, $got)) {
http_response_code(401);
exit;
}
$tx = json_decode(file_get_contents("php://input"), true);
// deduplicate on $tx["transaction_id"], then record $tx["code"], $tx["transferAmount"], $tx["virtualAccount"]
http_response_code(200);
echo "ok";
```
## Responses and retries
| Your answer | TinoPay reads it as |
| --- | --- |
| HTTP 2xx (200–299) | success, not sent again |
| any other status (4xx, 5xx), no answer or timeout | failure, will be sent again |
TinoPay only looks at the HTTP status, not the body: `200 OK` is enough; `{"success": true}` is a good habit but not required. Slow synchronous processing can time out and count as a failure.
When the first delivery gets no 2xx, TinoPay schedules a resend after 5 minutes, and the resend job tries at most 3 times. One transaction can therefore produce several calls to your webhook.
## Logs and resends
Every delivery is logged: the payload sent, your server's answer, the HTTP status (`status_code`), `success`, the URL and method, the related transaction.
- In the TinoPay web app: menu Tích hợp → Lịch sử webhook (Integrations → Webhook history, `/webhookslog`); click a row for details, and Gọi lại (Call again) to resend. On a transaction's detail page, Gửi lại hook (Resend hook) resends to every active webhook, notification channel and Google Sheets integration.
- [Webhook logs](/en/docs/api/tinopay-get-webhook-logs): filter with `status=failed` to see failures.
- [Resend a webhook log](/en/docs/api/tinopay-post-webhook-logs-log-resend): resend exactly that request to the same webhook.
- [Resend webhooks for a transaction](/en/docs/api/tinopay-post-transactions-transaction-resend-webhook): send the transaction to every active webhook of the bank account.
- [Transaction history](/en/docs/api/tinopay-get-transactions) with `webhook_status=failed` or `none`: transactions not delivered yet.
- Suspended merchant: transactions are still recorded but no webhooks are sent; after reactivation, reconcile with the transaction history.
- Deleting a webhook also deletes all of its logs.
## Checklist
- The webhook URL runs on HTTPS.
- Verify the token on every request.
- Answer HTTP 2xx on receipt; queue heavy work.
- Deduplicate on `transaction_id` or `referenceCode`.
- Check `transferType` to handle only the direction you need.
- Reconcile regularly with the [transaction history API](/en/docs/tinopay-api#transaction-history) as a fallback.
---
# Sign in from a CLI or an app without a backend
Command-line tools (CLIs), scripts and apps without their own server sign in to Tino the way `gh auth login` works: the CLI prints a short code, the customer opens `https://auth.tino.vn/oauth/device` in any browser (computer or phone), enters the code and clicks Allow, and the CLI receives a token. No redirect URI, no `client_secret`.
Updated: 2026-10-27. Every endpoint is in the discovery document `https://api.tino.vn/.well-known/oauth-authorization-server` (`registration_endpoint`, `device_authorization_endpoint`, `token_endpoint`).
| Item | Value |
| --- | --- |
| Register an app (`registration_endpoint`) | `POST https://api.tino.vn/oauth/register` |
| Request a device code (`device_authorization_endpoint`) | `POST https://api.tino.vn/oauth/device_authorization` |
| Page where the customer enters the code (`verification_uri`) | `https://auth.tino.vn/oauth/device` |
| Poll for and refresh tokens (`token_endpoint`) | `POST https://api.tino.vn/oauth/token` |
| Revoke tokens (`revocation_endpoint`) | `POST https://api.tino.vn/oauth/revoke` |
| Call the API on the customer's behalf | `https://api.tino.vn/v1/` |
| Device code `grant_type` | `urn:ietf:params:oauth:grant-type:device_code` |
| Code the customer enters (`user_code`) | 8 letters as `XXXX-XXXX`, expires after 600 seconds |
Apps that have a browser and can receive a redirect (web, mobile, MCP clients) use the Authorization Code + PKCE flow in [Sign in with Tino](/en/docs/oauth); this page covers everything else.
## Device code sign-in flow
1. The CLI calls `POST /oauth/device_authorization` → gets a `device_code` (keep it secret) and a `user_code` (show it to the customer).
2. The CLI prints: "Open https://auth.tino.vn/oauth/device and enter the code LPMR-SKST" — or opens `verification_uri_complete` (code included).
3. The customer signs in to Tino (if needed), enters the code, sees the app name and the requested permissions, and clicks Allow or Deny.
4. Meanwhile the CLI polls `POST /oauth/token` every `interval` seconds with the `device_code` until it gets a token, is denied, or the code expires.
5. The CLI calls `https://api.tino.vn/v1/` with `Authorization: Bearer `.
## Registering an app
There are two ways to get a `client_id`.
**Self-registration (RFC 7591).** Call it once at install time or on first run, and store the `client_id`:
```bash
curl -X POST https://api.tino.vn/oauth/register \
-H "Content-Type: application/json" \
-d '{"client_name":"My CLI","grant_types":["urn:ietf:params:oauth:grant-type:device_code","refresh_token"],"scope":"offline_access profile:read services:read"}'
```
Response (HTTP 201):
```json
{
"client_id": "…",
"client_id_issued_at": 1793106000,
"client_name": "My CLI",
"grant_types": ["urn:ietf:params:oauth:grant-type:device_code", "refresh_token"],
"token_endpoint_auth_method": "none",
"scope": "offline_access profile:read services:read"
}
```
| Field | Rule |
| --- | --- |
| `client_name` | name shown to the customer when entering the code; at most 100 characters |
| `grant_types` | among `authorization_code`, `refresh_token`, `urn:ietf:params:oauth:grant-type:device_code` |
| `redirect_uris` | required with `authorization_code`: `https://`, `http://localhost` / `127.0.0.1` / `[::1]` (any port) or the app's own scheme (`myapp://callback`); no `#` |
| `scope` | space-separated permissions (permission table in [Sign in with Tino](/en/docs/oauth)); empty = every permission an app may request |
| `logo_uri` | optional, `https://` |
| `token_endpoint_auth_method` | `none` only: self-registered apps are public clients without a `client_secret` |
Registering grants nothing: the customer still enters the code and clicks Allow. Each IP address can register at most 10 times per hour (`429 too_many_requests`). Metadata errors: `400 invalid_client_metadata` or `400 invalid_redirect_uri`, with an English sentence in `error_description`.
**Tino creates the app.** Contact Tino to create an app with "CLI (device code)" enabled; use this when you need a `client_secret` or a fixed name and logo.
## Step 1 — Request a device code
`POST https://api.tino.vn/oauth/device_authorization`, body `application/x-www-form-urlencoded`:
| Parameter | Value |
| --- | --- |
| `client_id` | your client_id |
| `scope` | space-separated permissions; empty = the app's permissions. Add `offline_access` to get a refresh token |
| `client_secret` | confidential clients created by Tino only |
```bash
curl -X POST https://api.tino.vn/oauth/device_authorization \
-d client_id=YOUR_CLIENT_ID \
-d "scope=offline_access services:read"
```
Response (HTTP 200):
```json
{
"device_code": "…",
"user_code": "LPMR-SKST",
"verification_uri": "https://auth.tino.vn/oauth/device",
"verification_uri_complete": "https://auth.tino.vn/oauth/device?user_code=LPMR-SKST",
"expires_in": 600,
"interval": 5
}
```
| Error | Meaning |
| --- | --- |
| `401 invalid_client` | `client_id` unknown or disabled, or wrong `client_secret` |
| `400 unauthorized_client` | the app does not have device code sign-in enabled |
| `400 invalid_scope` | unknown permission, or outside the app's permissions |
The `device_code` is the CLI's secret: never print or log it. Only the `user_code` and the page address are for the customer.
## Step 2 — The customer enters the code
Print `verification_uri` and `user_code` so the customer can type them on any device; on a machine with a browser, open `verification_uri_complete` directly. On Tino's page the customer signs in (if needed), sees the app name, the requested permissions and how long the code stays valid, then clicks Allow or Deny. The page reminds the customer to only enter a code they just saw on their own machine.
## Step 3 — Poll for the token
Every `interval` seconds call `POST https://api.tino.vn/oauth/token`, body `application/x-www-form-urlencoded`:
| Parameter | Value |
| --- | --- |
| `grant_type` | `urn:ietf:params:oauth:grant-type:device_code` |
| `device_code` | the `device_code` from step 1 |
| `client_id` | your client_id |
| `client_secret` | confidential clients only |
While waiting, Tino returns HTTP 400 with `error`:
| `error` | What the CLI does |
| --- | --- |
| `authorization_pending` | the customer has not answered yet: wait `interval` seconds and poll again |
| `slow_down` | polling too fast: add 5 seconds to the wait and keep the new interval |
| `access_denied` | the customer clicked Deny: stop |
| `expired_token` | more than 10 minutes passed: request a new code (step 1) |
| `invalid_grant` | wrong or already used `device_code`: stop |
Once the customer clicks Allow, the next poll gets HTTP 200 (each `device_code` can be exchanged only once):
```json
{
"access_token": "tinoat_…",
"token_type": "Bearer",
"expires_in": 86400,
"scope": "offline_access services:read",
"refresh_token": "tinort_…"
}
```
`refresh_token` is only present when `scope` includes `offline_access`.
## Step 4 — Call the API
```bash
curl https://api.tino.vn/v1/service -H "Authorization: Bearer $ACCESS_TOKEN"
```
The permissions in `scope` decide which routes you can call: permission and route table in [Sign in with Tino](/en/docs/oauth), each route in the [API Reference](/en/docs/reference).
## Refreshing and revoking
- Refresh when the `access_token` expires (`expires_in` 86400 seconds, 24 hours): `POST /oauth/token` with `grant_type=refresh_token`, `client_id`, `refresh_token`. Each refresh returns a new refresh token and invalidates the old one; store the new one right away. Reusing an old refresh token makes Tino revoke all of the app's access, and the customer has to enter a code again.
- Sign out: `POST /oauth/revoke` with `token=`, `client_id`. Customers can also revoke access themselves on the Authorized apps page (`https://auth.tino.vn/me/apps`).
Parameter details: [Sign in with Tino](/en/docs/oauth).
## Storing tokens safely
- Store tokens in a per-user file, for example `~/.config//token.json` with mode `600` (owner read/write only), or in the operating system keychain.
- Never print tokens or the `device_code` to the screen or logs; never pass tokens as command-line arguments (they show up in the process list).
- Only request the permissions you really need; add `offline_access` when the CLI must keep working without asking the customer for a code again.
## Bash example
Needs `curl` and `jq`.
```bash
#!/usr/bin/env bash
set -euo pipefail
CLIENT_ID="${TINO_CLIENT_ID:?set TINO_CLIENT_ID}"
API=https://api.tino.vn
start=$(curl -s -X POST "$API/oauth/device_authorization" -d client_id="$CLIENT_ID" -d "scope=offline_access services:read")
device_code=$(jq -r .device_code <<<"$start")
interval=$(jq -r .interval <<<"$start")
echo "Open $(jq -r .verification_uri <<<"$start") and enter the code $(jq -r .user_code <<<"$start")"
while true; do
sleep "$interval"
reply=$(curl -s -X POST "$API/oauth/token" \
-d grant_type=urn:ietf:params:oauth:grant-type:device_code \
-d device_code="$device_code" -d client_id="$CLIENT_ID")
case "$(jq -r '.error // empty' <<<"$reply")" in
"") break ;;
authorization_pending) ;;
slow_down) interval=$((interval + 5)) ;;
*) echo "Sign-in failed: $(jq -r .error <<<"$reply")" >&2; exit 1 ;;
esac
done
mkdir -p ~/.config/my-cli
( umask 077; printf '%s' "$reply" > ~/.config/my-cli/token.json )
echo "Signed in."
```
## Node.js example
Node.js 18+ (built-in `fetch`), no libraries.
```javascript
import { mkdir, writeFile } from "node:fs/promises";
import { homedir } from "node:os";
import { join } from "node:path";
const API = "https://api.tino.vn";
const clientId = process.env.TINO_CLIENT_ID;
const form = (body) => ({ method: "POST", body: new URLSearchParams(body) });
const start = await (await fetch(`${API}/oauth/device_authorization`, form({ client_id: clientId, scope: "offline_access services:read" }))).json();
console.log(`Open ${start.verification_uri} and enter the code ${start.user_code}`);
let interval = start.interval;
let token;
while (!token) {
await new Promise((done) => setTimeout(done, interval * 1000));
const reply = await (await fetch(`${API}/oauth/token`, form({
grant_type: "urn:ietf:params:oauth:grant-type:device_code", device_code: start.device_code, client_id: clientId,
}))).json();
if (!reply.error) token = reply;
else if (reply.error === "slow_down") interval += 5;
else if (reply.error !== "authorization_pending") throw new Error(`Sign-in failed: ${reply.error}`);
}
const dir = join(homedir(), ".config", "my-cli");
await mkdir(dir, { recursive: true, mode: 0o700 });
await writeFile(join(dir, "token.json"), JSON.stringify(token), { mode: 0o600 });
console.log("Signed in.");
```
## Python example
Python 3, standard library only.
```python
import json, os, time, urllib.error, urllib.parse, urllib.request
API = "https://api.tino.vn"
CLIENT_ID = os.environ["TINO_CLIENT_ID"]
def post(path, body):
request = urllib.request.Request(API + path, data=urllib.parse.urlencode(body).encode())
try:
with urllib.request.urlopen(request) as response:
return json.load(response)
except urllib.error.HTTPError as error: # 400 authorization_pending, slow_down…
return json.load(error)
start = post("/oauth/device_authorization", {"client_id": CLIENT_ID, "scope": "offline_access services:read"})
print(f"Open {start['verification_uri']} and enter the code {start['user_code']}")
interval = start["interval"]
while True:
time.sleep(interval)
reply = post("/oauth/token", {"grant_type": "urn:ietf:params:oauth:grant-type:device_code",
"device_code": start["device_code"], "client_id": CLIENT_ID})
error = reply.get("error")
if not error:
break
if error == "slow_down":
interval += 5
elif error != "authorization_pending":
raise SystemExit(f"Sign-in failed: {error}")
path = os.path.expanduser("~/.config/my-cli/token.json")
os.makedirs(os.path.dirname(path), mode=0o700, exist_ok=True)
with open(os.open(path, os.O_WRONLY | os.O_CREAT | os.O_TRUNC, 0o600), "w") as file:
json.dump(reply, file)
print("Signed in.")
```
## References
- RFC 8628 — OAuth 2.0 Device Authorization Grant
- RFC 7591 — OAuth 2.0 Dynamic Client Registration
- [Sign in with Tino (OAuth 2.0, OpenID Connect)](/en/docs/oauth)
- [Personal API tokens](/en/docs/personal-tokens): customers create a token for their own scripts, no app registration needed
---
# SDKs & examples
Tino's official SDKs for Python, Node.js and PHP are open source at [tinovn/tino-sdk](https://github.com/tinovn/tino-sdk) (MIT license). Each route has a function `client..()`; 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](/en/docs/personal-tokens) (`tinopat_`) or an access token from [Sign in with Tino](/en/docs/oauth) (`tinoat_`). Read the token from the `TINO_TOKEN` environment variable; do not write it into source code.
```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
"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](/en/docs/oauth#refreshing-and-revoking-tokens) and then creates a client with the new token.
## Calling a route
Each route page in the [API Reference](/en/docs/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`):
```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]);
```
## 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):
```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");
```
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`):
```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"])
```
## Example: unpaid invoices
Print due invoices (scope `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);
```
## 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`):
```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"])
```
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](/en/docs/sandbox).
---
# Telegram, Zalo and Lark notifications
Besides [webhooks](/en/docs/tinopay-webhook), TinoPay posts a notification for every transaction to Telegram, Zalo or Lark. Each notification app is tied to one bank account and filters by virtual account like a webhook. The routes are in the Notifications group of the [API Reference](/en/docs/reference#tinopay); calling, authentication and error codes are in [Getting started with TinoPay](/en/docs/tinopay-start).
Updated: 2026-10-25. Sources: Tino's TinoPay module and the TinoPay API v1 documentation.
## App types
| `type` | Bot | `endpoint` (where it posts) |
| --- | --- | --- |
| `telegram` | the shared bot **@allpay_telebot** by default; send `bot_token` to use your own bot | shared bot: a group ID starting with `-` (add @allpay_telebot to the group); own bot: a group ID (`-…`) or a private chat (positive number) |
| `zalo` | your own bot is required (`bot_token` from Zalo Bot Creator) | the Zalo `chat_id` of a private chat or a group (groups start with `zgr-`) |
| `lark` | a Lark bot webhook URL, no `bot_token` | `https://open.larksuite.com/open-apis/bot/v2/hook/…` (or `open.feishu.cn`) |
`bot_token` is never returned: lists carry only `bot_token_masked`. In a UI, make the token field a password field and do not refill it when editing.
## Adding an app (own bot)
1. [Check the bot token](/en/docs/api/tinopay-post-notification-apps-bot-info): `{type, bot_token}` → `{valid, bot_name, error}`. Show the bot name; when `valid` is `false`, show `error`.
2. [Find a chat](/en/docs/api/tinopay-post-notification-apps-find-chats): `{type, bot_token}` → `{chats: [{id, title, type}], error}`; the user picks one, and its `id` becomes the `endpoint`.
3. [Send a test message](/en/docs/api/tinopay-post-notification-apps-test): `{type, endpoint, bot_token}` sends a real message → `{connected, chat_title}`. Allow saving only when `connected` is `true`; Telegram returns `chat_title` to prefill the name.
4. [Create the app](/en/docs/api/tinopay-post-notification-apps): `type`, `name` (up to 250 characters), `endpoint`, `bot_token`, `bank_account_id`, `virtual_accounts` (empty means every virtual account), `status` (`active` by default, `inactive`) → 201. `type` cannot change after creation.
Step 2 by type:
- Telegram returns right away the groups and people that messaged the bot in the last 24 hours. A non-null `error` (the bot is attached to another system's webhook): let the user enter the ID by hand.
- Zalo keeps no old messages: the request waits up to about 20 seconds and only catches messages sent to the bot meanwhile (groups: mention the bot). Tell the user to message the bot now, and set the HTTP timeout to at least 30 seconds. When nothing arrives, `chats` is empty: let them retry or enter the ID by hand.
Shared Telegram bot: skip steps 1 and 2; enter the group ID, and step 3 sends no `bot_token`. Lark: only steps 3 and 4, with the webhook URL as `endpoint`.
Steps 2 and 3 are not retried on network errors (a retry would multiply the wait or send the test message twice): let the user try again.
## Editing an app
- When editing, steps 1–3 send `app_id` instead of `bot_token` to use the stored token; `app_id` must be an app of this merchant, otherwise 404 `RESOURCE_NOT_FOUND`.
- [Update the app](/en/docs/api/tinopay-patch-notification-apps-app): send only the fields to change, never `type`.
| `bot_token` when editing | Result |
| --- | --- |
| not sent | keep the stored token |
| a new string | replace the token |
| `null` | Telegram goes back to the shared bot (`endpoint` must be a group ID `-…`); Zalo cannot drop its token |
A `bank_account_id` that does not belong to the merchant: 404 `BANK_ACCOUNT_NOT_FOUND`.
## Watching delivery errors
The [app list](/en/docs/api/tinopay-get-notification-apps) carries `last_error` and `last_error_at`: the error of the latest delivery (token revoked, bot removed from the group…). Show a "connection error" warning with `last_error`; the next successful delivery resets it to `null`. A suspended merchant sends no notifications, as with webhooks.
---
# Changelog
Changes to the API, MCP server, SDKs and developer documentation, newest first.
## 2026-10-28
- **Scopes:** `domains:registrant` (new, personal tokens only) for the four [domain registrant dossier](/en/docs/domain-registrant) routes, split from `domains:owner`; `domains:write` is now labelled "Register, renew and manage domains"; `orders:write` (new) for [quotes](/en/docs/api/post-quote), [carts](/en/docs/api/post-carts) and [orders](/en/docs/api/post-order) — orders that charge the balance also need `billing:write`.
## 2026-10-27
- **Identity & SSO:** OAuth access tokens and ID tokens last 86400 seconds (24 hours) by default, not 3600: corrected in [Sign in with Tino](/en/docs/oauth), [Sign in from a CLI](/en/docs/cli-login), [MCP](/en/docs/mcp), [SDK](/en/docs/sdk). Still read `expires_in` from each response instead of hard-coding it.
- **TinoPay, TinoPage Builder, TinoWAF:** the `/v1` gateway opens the three services' routes to OAuth app tokens and personal API tokens, with new scopes `tinopay:read`, `tinopay:write`, `tinopay:owner` (personal tokens only), `builder:read`, `builder:write`, `waf:read`, `waf:write`: table in [Sign in with Tino](/en/docs/oauth), [Getting started with TinoPay](/en/docs/tinopay-start), [Getting started with TinoPage Builder](/en/docs/builder-start).
- **MCP server:** [Connect Claude, ChatGPT to Tino (MCP)](/en/docs/mcp): other MCP apps sign in with OAuth by registering a client themselves (`POST https://api.tino.vn/oauth/register`, RFC 7591); a personal API token is the alternative. Claude Code identifies itself with a Client ID Metadata Document and by default requests every scope the MCP server lists, write scopes included.
- **Identity & SSO:** new page [Sign in from a CLI or an app without a backend](/en/docs/cli-login): app self-registration (`POST https://api.tino.vn/oauth/register`, RFC 7591), device code sign-in (RFC 8628: `POST /oauth/device_authorization`, the customer enters the code at `https://auth.tino.vn/oauth/device`, the CLI polls `POST /oauth/token`), examples in bash, Node.js, Python. [Sign in with Tino](/en/docs/oauth) lists the two new endpoints and the `urn:ietf:params:oauth:grant-type:device_code` grant.
- **Domains:** new page [Domain registrant dossier](/en/docs/domain-registrant) and 4 routes: [domains awaiting their dossier](/en/docs/api/get-domains-pending), [read](/en/docs/api/get-domains-id-registrant) and [save the registrant](/en/docs/api/put-domains-id-registrant) of a waiting domain, [registrants used before](/en/docs/api/get-registrants); all four need `domains:owner` (personal API tokens only), and ID numbers in `reason` are masked. Whois lookup can also be called through `/v1` with a token: [`GET /v1/whois/{domain}`](/en/docs/api/get-v1-whois-domain).
- **TinoWAF, TinoPay, MCP:** API section [TinoWAF](/en/docs/waf): 19 routes `/v1/service/{id}/waf/…` (websites, protection level, emergency anti-attack mode, domain names, origin servers, event log, IP block/allow list), scopes `waf:read`, `waf:write`. TinoPay adds 9 routes: [payment code patterns](/en/docs/api/tinopay-get-pay-codes), [Sapo integrations](/en/docs/api/tinopay-get-sapo-integrations), [Sapo reconciliation log](/en/docs/api/tinopay-get-sapo-order-logs). The [MCP](/en/docs/mcp) page adds the tools of the 6 TinoPay, TinoPage Builder, TinoWAF scopes.
## 2026-10-26
- **Domains:** new page [Domain whois lookup](/en/docs/api/get-whois-domain) (`GET https://api.tino.vn/whois/`): a public route with no token; how to read `available`, `error` and `raw` when rate-limited.
- **TinoPage Builder:** new section: [Getting started with TinoPage Builder](/en/docs/builder-start) and a page for each of the 14 routes of the service API `https://api.tino.vn/service//builder/…` (account overview and limits, websites, AI website creation, publishing, pausing, deleting, custom domains, contacts and orders, one-time sign-in tickets), called with the service owner's sign-in token.
## 2026-10-24
- **Tino Uptime:** new section: [Getting started with Tino Uptime](/en/docs/uptime-start) and a page for each of the 20 routes of the service API `https://api.tino.vn/service//uptime/…` (plan, usage, monitors, uptime, check results, incidents, alert channels), called with the service owner's sign-in token.
## 2026-10-22
- **TinoPay:** merchant routes dropped the `/v2/services` prefix and are now `https://api.tino.vn/service//tinopay/…`; the old path returns 404 `invalid method`. Added 11 routes: [Telegram, Zalo and Lark notifications](/en/docs/tinopay-notifications) and [API tokens](/en/docs/tinopay-start#api-tokens). Identify the TinoPay service by `product_type` (`tinopay`) instead of the service name; set timeouts to at least 100 seconds ([Getting started with TinoPay](/en/docs/tinopay-start)).
## 2026-10-21
- **Documentation:** developers.tino.vn has a page for every route of the `/v1` gateway (parameters, cURL, JavaScript, PHP, SDK examples, sample response), an [API Reference](/en/docs/reference) by product, [OpenAPI 3.1](/en/openapi.json), `llms.txt` and a Markdown version of every page.
- **Documentation:** added the pages [Connect Claude, ChatGPT to Tino (MCP)](/en/docs/mcp), [Personal API tokens](/en/docs/personal-tokens), [SDKs & examples](/en/docs/sdk), [Test environment](/en/docs/sandbox).
- **Docs:** TinoPay section: [Getting started with TinoPay](/en/docs/tinopay-start), [Receiving TinoPay webhooks](/en/docs/tinopay-webhook) and a page for every route of the TinoPay merchant API.
## 2026-10-20
- **Sign in with Tino:** OAuth 2.0 Authorization Code + PKCE (S256) and OpenID Connect; issuer `https://api.tino.vn`, sign-in and consent page `https://auth.tino.vn/oauth/consent`. See [Sign in with Tino](/en/docs/oauth).
- **Gateway `/v1`:** call the API on the customer's behalf at `https://api.tino.vn/v1/`; each route needs a scope, and a route outside the token's scopes returns 403 `insufficient_scope`. See [Calling the Tino API](/en/docs/api).
- **Personal API tokens** (`tinopat_`): customers create them at `https://auth.tino.vn/en/me/security` and choose scopes, expiry and IP restriction; only personal tokens can have the `domains:owner` scope, and it requires an IP restriction.
- **MCP server** `https://api.tino.vn/mcp`: Claude, Claude Code and ChatGPT sign in with OAuth; other MCP apps use a personal token. There is no tool for cancelling services.
## SDK 1.2.0 (2026-10-13)
- The `backorder` group: .vn domain auctions. Version 1.2.0 is on PyPI; npm and Packagist are at 1.1.0.
## SDK 1.1.0 (2026-08-04)
- Phone number verification for the account; the code is sent through Zalo, or by SMS if the number has no Zalo account.
## SDK 1.0.0 (2026-08-02)
- Python, Node.js and PHP SDKs and contract documents for each operation: [tinovn/tino-sdk](https://github.com/tinovn/tino-sdk).
---
# TinoPay public API
TinoPay's public API runs at `https://pay.tino.vn`: query transaction history, list banks and generate VietQR code images. It is separate from the [merchant API](/en/docs/tinopay-start#calling-the-merchant-api) (`https://api.tino.vn/service//tinopay/…`, Tino sign-in token), which manages the merchant, bank accounts and webhooks.
Updated: 2026-10-25. Source: the [TinoPay documentation](https://pay.tino.vn/docs).
## Two ways to integrate
| Approach | Description | Use it for |
| --- | --- | --- |
| API | you call TinoPay to query data (transaction history, bank list) and generate QR codes | checks, reconciliation, showing a payment QR |
| Webhook | TinoPay sends transaction data to your URL as soon as it happens | instant payment confirmation, no polling |
Use [webhooks](/en/docs/tinopay-webhook) to receive transactions instantly and the [transaction history](#transaction-history) for reconciliation or as a fallback.
## Basics
| Item | Value |
| --- | --- |
| Base URL | `https://pay.tino.vn` |
| Prefix | API endpoints live under `/api`, e.g. `https://pay.tino.vn/api/transactions`; the QR image is at `https://pay.tino.vn/img-qr` |
| Format | JSON, UTF-8 |
| Currency | VND, integers, no decimal part |
| Authentication | `Authorization: Bearer `, except the bank list and the QR image |
```bash
curl -G "https://pay.tino.vn/api/transactions" \
-H "Authorization: Bearer $TINOPAY_API_TOKEN" \
-H "Accept: application/json"
```
## Authentication
Every request that needs authentication sends an API token in the `Authorization` header with the Bearer scheme:
```http
Authorization: Bearer
```
### Creating an API token
- In the TinoPay web app: menu Tích hợp (Integrations) → API Access (`/api-access`), click Tạo API Token (Create API Token), give it a memorable name (e.g. `server-production`) and copy the token right away.
- With the merchant API: [Create an API token](/en/docs/api/tinopay-post-api-tokens); see [API tokens](/en/docs/tinopay-start#api-tokens) in Getting started.
The raw token is shown **only once**, at creation. If you lose it, revoke the old token and create a new one. You can create several tokens and revoke any of them on the API Access page.
### Access scope
- Each token belongs to one owner account and only reads that owner's data (transactions, bank accounts).
- A parameter pointing at a resource you do not own (e.g. someone else's `bank_account_id`) returns no data.
### Calling examples
JavaScript:
```javascript
const res = await fetch("https://pay.tino.vn/api/transactions", {
headers: { Authorization: `Bearer ${process.env.TINOPAY_API_TOKEN}`, Accept: "application/json" },
});
const data = await res.json();
```
PHP (Laravel):
```php
$response = Http::withToken(env('TINOPAY_API_TOKEN'))
->acceptJson()
->get('https://pay.tino.vn/api/transactions');
$data = $response->json();
```
### Authentication errors
| Case | HTTP | Body |
| --- | --- | --- |
| missing or malformed `Authorization` header | 401 | `{"message": "Thiếu API Token. Vui lòng gửi header Authorization: Bearer {token}."}` |
| token does not exist or was revoked | 401 | `{"message": "API Token không hợp lệ."}` |
The `message` texts are in Vietnamese, as the API returns them.
### Security
- TinoPay stores tokens as SHA-256 hashes, never the raw token: the raw token can be seen only once, at creation.
- Every valid request updates the token's last-used time (`last_used_at`), so you can see which tokens are in use.
- Never put a token in client code (browser, mobile app) or commit it to Git: use it on servers only.
- Revoke a leaked token at once on the API Access page; a revoked token returns 401 from the next request.
## Transaction history
```http
GET https://pay.tino.vn/api/transactions
```
- Authentication: required.
- Returns transactions of every bank account owned by the token's owner.
- Defaults to today's transactions; pass `transaction_date_from`, `transaction_date_to` for another range.
- Sorted by `transaction_date` descending (newest first), paginated.
Query parameters, all optional:
| Parameter | Type | Default | Description |
| --- | --- | --- | --- |
| `q` | string | — | search (LIKE) in `reference_number`, `transaction_content`, `transaction_id` |
| `bank_account_id` | string (UUID) | — | filter on one bank account; the UUID must belong to the token's owner |
| `type` | string | — | `in` incoming, `out` outgoing |
| `transaction_date_from` | date (`Y-m-d`) | today | start date (inclusive) |
| `transaction_date_to` | date (`Y-m-d`) | today | end date (inclusive); at most 90 days after `from` |
| `per_page` | int (1–100) | `20` | records per page |
| `page` | int | `1` | current page |
Date range:
- Neither `from` nor `to`: today.
- Only one of them: exactly that whole day.
- `from` must be less than or equal to `to`, at most 90 days apart.
```bash
curl -G "https://pay.tino.vn/api/transactions" \
-H "Authorization: Bearer $TINOPAY_API_TOKEN" \
-H "Accept: application/json" \
--data-urlencode "q=DH12345" \
--data-urlencode "type=in" \
--data-urlencode "transaction_date_from=2026-10-01" \
--data-urlencode "transaction_date_to=2026-10-20" \
--data-urlencode "per_page=20"
```
```javascript
const params = new URLSearchParams({ q: "DH12345", type: "in", transaction_date_from: "2026-10-01", transaction_date_to: "2026-10-20", per_page: "20" });
const res = await fetch(`https://pay.tino.vn/api/transactions?${params}`, {
headers: { Authorization: `Bearer ${process.env.TINOPAY_API_TOKEN}`, Accept: "application/json" },
});
const json = await res.json();
```
Response `200`:
```json
{
"data": [
{
"id": 1032,
"reference_number": "0551dTCO-82LiomOss",
"transaction_content": "DH12345 chuyen tien mua hang",
"transaction_id": "20261020083338000006",
"bank_account_id": "9c1e6f2a-5b7d-4e1a-9f3c-2d8b6a4e0c11",
"virtual_number": "9624QA01",
"amount": 150000,
"type": 1,
"type_label": "in",
"status": 0,
"transaction_date": "2026-10-20 08:33:38",
"created_at": "2026-10-20 08:33:40"
}
],
"links": {
"first": "https://pay.tino.vn/api/transactions?page=1",
"last": "https://pay.tino.vn/api/transactions?page=3",
"prev": null,
"next": "https://pay.tino.vn/api/transactions?page=2"
},
"meta": { "current_page": 1, "from": 1, "last_page": 3, "per_page": 20, "to": 20, "total": 57 }
}
```
| Field in `data[]` | Type | Description |
| --- | --- | --- |
| `id` | int | transaction ID |
| `reference_number` | string \| null | the bank's reference code (core reference) |
| `transaction_content` | string | transaction description |
| `transaction_id` | string | transaction code |
| `bank_account_id` | string (UUID) | the bank account of the transaction |
| `virtual_number` | string \| null | virtual account number, if any |
| `amount` | int | amount (VND) |
| `type` | int | `1` incoming, `2` outgoing |
| `type_label` | string | `in` incoming, `out` outgoing |
| `status` | int | transaction status |
| `transaction_date` | string (`Y-m-d H:i:s`) | when the transaction actually happened |
| `created_at` | string (`Y-m-d H:i:s`) | when TinoPay recorded it |
`links` and `meta` are the Laravel API Resource pagination structure: page through with `meta.last_page` or `links.next`.
| Error | HTTP | Body |
| --- | --- | --- |
| `from` after `to` | 422 | `{"message": "transaction_date_from phải nhỏ hơn hoặc bằng transaction_date_to."}` |
| range over 90 days | 422 | `{"message": "Khoảng thời gian tối đa là 90 ngày."}` |
| wrong parameter type | 422 | Laravel validation error: `{"message": …, "errors": {…}}` |
## Bank list
```http
GET https://pay.tino.vn/api/banks
```
Banks TinoPay supports, with the bank code (`code`) and BIN: use it for a bank picker or to get the `code` for [VietQR codes](#vietqr-codes). No authentication. Returns a JSON array, not wrapped in `data`.
```bash
curl "https://pay.tino.vn/api/banks" -H "Accept: application/json"
```
```json
[
{ "id": 1, "code": "BIDV", "bin": "970418", "name": "Ngân hàng TMCP Đầu tư và Phát triển Việt Nam", "created_at": "2025-01-01 00:00:00", "updated_at": "2025-01-01 00:00:00" },
{ "id": 2, "code": "MB", "bin": "970422", "name": "Ngân hàng TMCP Quân đội", "created_at": "2025-01-01 00:00:00", "updated_at": "2025-01-01 00:00:00" }
]
```
| Field | Type | Description |
| --- | --- | --- |
| `id` | int | bank ID in TinoPay |
| `code` | string | short code (e.g. `BIDV`, `MB`), used as `bank_code` for QR codes |
| `bin` | string | bank identification number (BIN) of the Napas / VietQR standard |
| `name` | string | full bank name |
The list mirrors the bank data configured in TinoPay: use the `code` values from it as-is when calling other APIs.
## VietQR codes
```http
GET https://pay.tino.vn/img-qr
```
Generates a dynamic VietQR code for customers to scan and pay. The endpoint returns a PNG image directly, so you can put it in an `
` tag without drawing the QR yourself.
- No authentication. No `/api` prefix.
- Returns `image/png` with the header `Access-Control-Allow-Origin: *`, so it works from browsers.
- Codes follow the VietQR / Napas 247 standard (fast transfer service QRIBFTTA).
| Parameter | Required | Description |
| --- | --- | --- |
| `bank_code` | yes | beneficiary bank code (e.g. `BIDV`, `MB`) from the [bank list](#bank-list) |
| `account_no` | yes | beneficiary account number |
| `account_name` | no | account holder name, shown on the framed image |
| `amount` | no | amount (VND); applied only between 1,000 and 9,999,999,999,999 |
| `memo` | no | transfer description, up to 25 characters (longer values are ignored) |
| `template` | no | `default` framed image (default), `only-qr` QR code only |
| `is_mask` | no | `1`: partly mask the account number on the framed image |
```html
```
Every parameter:
```text
https://pay.tino.vn/img-qr?bank_code=BIDV&account_no=96240000001&account_name=CONG%20TY%20ABC&amount=150000&memo=DH12345&template=default&is_mask=1
```
QR code only, no frame:
```text
https://pay.tino.vn/img-qr?bank_code=BIDV&account_no=96240000001&amount=150000&memo=DH12345&template=only-qr
```
A missing or invalid required parameter returns text, not an image:
| Case | Returns |
| --- | --- |
| `bank_code` invalid or unknown | `Vui lòng nhập đúng mã ngân hàng` |
| `account_no` missing | `Vui lòng nhập số tài khoản` |
In a payment flow, set `memo` to a unique order code (e.g. `DH12345`). When the customer pays, the [webhook](/en/docs/tinopay-webhook) receives the transaction with this description (and `code` when it matches your [payment codes](/en/docs/api/tinopay-patch-merchant)), so you can match the order and confirm payment automatically.
## Support
Stuck on an integration: open a support ticket from the TinoPay dashboard.
---
# No-code integrations
TinoPay ships four integrations configured in the TinoPay web app (menu Tích hợp, Integrations), with no endpoint to build: Google Sheets writes transactions to a spreadsheet; the WordPress / WooCommerce plugin, the HostBill module and Sapo show a VietQR code and confirm orders and invoices automatically when the customer pays. For a deeper integration with your own system, see [webhooks](/en/docs/tinopay-webhook) and the [public API](/en/docs/tinopay-api).
Updated: 2026-10-25. Source: the [TinoPay documentation](https://pay.tino.vn/docs/google-sheets), no-code integrations section.
Labels below are the web app's Vietnamese labels, with an English gloss in parentheses.
The three store integrations match transactions by the payment code in the transfer description: the prefix set in the TinoPay web app (Cài đặt → Cấu trúc mã thanh toán, Settings → Payment code structure) must match the prefix configured in the plugin, module or store.
## Google Sheets
TinoPay writes every matching transaction to one of your Google spreadsheets as soon as the bank reports it; handy for accountants and shop owners who track cash flow in a spreadsheet. TinoPay batches transactions for about 10 seconds and writes several rows at once to stay within the Google Sheets API quota, so new rows usually appear after a few seconds rather than instantly.
You need a Google account (Gmail or Google Workspace) and at least one bank account connected to TinoPay.
### Setting up Google Sheets
Menu Tích hợp → Google Sheets → Thêm tích hợp (Add integration), four steps:
1. Google account: the first time, click Đăng nhập bằng Google (Sign in with Google) and pick the account holding the spreadsheet; if already linked, pick it from the list or click Liên kết tài khoản Google khác (Link another Google account). Google's consent screen has a Google Drive access checkbox that is unchecked by default: you must tick it, or TinoPay cannot create or write any spreadsheet.
2. Spreadsheet: Tạo mới bảng tính (Create a new spreadsheet: TinoPay creates it with the right columns; name it and pick a folder in My Drive) or Chọn bảng tính có sẵn (Use an existing spreadsheet: TinoPay adds the header row if the sheet is empty). Pick the sheet that receives data and the option Tự động xóa giao dịch cũ sau (Delete old transactions after): Không xóa (never: keep everything, the spreadsheet keeps growing), 60 minutes, 4 hours, 8 hours or 1 day (rows older than that are deleted; cleanup runs every 10 minutes).
3. Scope: Tất cả ngân hàng (All banks: every account, including ones added later) or Tùy chỉnh ngân hàng (Custom: only the accounts you pick, optionally filtered by virtual account; picking no virtual account means every transaction of that account). Transaction type: all, incoming, outgoing. Ẩn số dư giao dịch (Hide balance): leaves the Lũy kế (running balance) column empty, for spreadsheets you share with others.
4. Confirm: give it a memorable name, check the summary and click Lưu tích hợp (Save integration). From then on every matching transaction is written to the spreadsheet.
Rows deleted from the spreadsheet stay in TinoPay: look them up in menu Giao dịch → Lịch sử giao dịch (Transactions → Transaction history) or through the [transaction history API](/en/docs/tinopay-api#transaction-history).
### Spreadsheet columns
TinoPay writes exactly 10 columns in this order (headers in Vietnamese):
| Column | Header | Content |
| --- | --- | --- |
| A | Ngân hàng | full bank name |
| B | Ngày giao dịch | transaction date, `YYYY-MM-DD HH:MM:SS` |
| C | Số tài khoản | bank account number that received or paid |
| D | Tài khoản phụ | virtual account number, empty when the transaction did not use one |
| E | Code TT | payment code extracted from the transfer description with your payment code structure; empty when nothing matches |
| F | Nội dung thanh toán | transfer description |
| G | Loại | `Tiền vào` (incoming) or `Tiền ra` (outgoing) |
| H | Số tiền | amount |
| I | Mã tham chiếu | the bank's reference code |
| J | Lũy kế | balance after the transaction, empty when Hide balance is on |
### Permissions and notes
- TinoPay asks for a single Google Drive permission: `drive.file`, only files TinoPay creates or that you pick in the file picker; TinoPay cannot read other files in your Drive. Revoke it any time at [myaccount.google.com/permissions](https://myaccount.google.com/permissions) or by deleting the integration.
- Do not rename, insert or delete the 10 default columns: TinoPay writes by column order, and a changed structure puts data in the wrong place. Add your own columns to the right of column J. Do not delete the header row (row 1).
- If you revoke the Google permission or delete the spreadsheet, the integration switches to Tạm dừng (Paused) with an error message; link the Google account again to continue.
- You can create several integrations pointing at different spreadsheets, e.g. one per branch.
- The Đẩy gần nhất (Last push) column on the list page shows the last successful write; on errors, a warning icon next to the integration name shows details on hover.
| Symptom | Common cause |
| --- | --- |
| the spreadsheet has no rows | no transaction matches the step 3 filters yet, or the integration is paused |
| the integration switches to paused by itself | the Google permission was revoked, or the spreadsheet was deleted or changed owner |
| a spreadsheet cannot be created | the Google Drive checkbox was not ticked on the consent screen |
| data lands in the wrong columns | the 10 default columns were changed |
## WordPress / WooCommerce
The TinoPay Gateway plugin adds a VietQR bank-transfer payment method to WooCommerce and confirms orders automatically when the customer pays. It uses [webhooks](/en/docs/tinopay-webhook) behind the scenes but configures them itself, so you build no endpoint.
1. The customer places an order; the thank-you page shows a VietQR code with the transfer description `DH`.
2. The customer scans and pays; TinoPay records the transaction and extracts `DH`.
3. The webhook reaches the website; the plugin matches the order and changes its status.
- Full amount: the order moves to the status you chose (Processing by default) and stock is reduced.
- Short or excess amount: the order keeps its status, and the plugin adds a note with the difference for you to handle.
- The customer's payment page shows "Bạn đã thanh toán thành công" (payment successful) once the order is confirmed.
Requirements: WordPress 5.6+, PHP 7.2+, WooCommerce installed; the website on HTTPS (required to receive webhooks and connect securely); a TinoPay account (sign in with your Tino account) and at least one connected bank account. Automatic confirmation currently supports BIDV and needs a virtual account.
### Installing the plugin
1. [Download the TinoPay Gateway plugin (.zip)](https://pay.tino.vn/downloads/tinopay-gateway.zip), or in the TinoPay web app: Tích hợp → WordPress → Tải plugin (.zip) (Download plugin).
2. In WordPress: Plugins → Add New → Upload Plugin, choose the zip file, click Install, then Activate.
3. WooCommerce → Settings → Payments → TinoPay, click Kết nối TinoPay (Connect TinoPay): the browser goes to TinoPay; sign in with your Tino account, check the website name and click Cho phép kết nối (Allow connection).
4. Pick the bank account (BIDV: also pick a virtual account, whose number the QR code uses) and click Hoàn tất thiết lập (Finish setup). The plugin creates the webhook on TinoPay (visible under Tích hợp → Webhooks) and syncs the payment code structure: prefix `DH` by default, so `DH1234` matches order #1234.
5. Place a small test order, choose TinoPay, scan the QR code and pay: a few seconds later the order changes status with the note "TinoPay: Đã nhận thanh toán…" (payment received).
A connected website may only view bank account and virtual account details, create webhooks pointing at itself, and view and update the payment code structure. It cannot see passwords or move money.
| Plugin option | Meaning |
| --- | --- |
| Payment code prefix | must match Cài đặt → Cấu trúc mã thanh toán on TinoPay |
| Order status on completion | status given to the order when the full amount arrives (Processing by default) |
| Success message | text shown to the customer after payment |
| Download mode | digital products: show the download link right after payment |
| Bank name display | short name, full name or both |
Managing connections: TinoPay web app → Tích hợp → WordPress, section Website đã kết nối (Connected websites) lists websites and when each was last used. Ngắt kết nối (Disconnect) revokes access at once and the website must connect again; webhooks it created stay, managed on the Webhooks page.
### Configuring the plugin manually
Without Connect TinoPay (OAuth), configure it by hand:
1. In the plugin: fill in the bank, account number (or virtual account number), account holder, payment code prefix and an API Key of your choice (letters and digits only, at least 10 characters).
2. On TinoPay → Tích hợp → Webhooks → Thêm webhook: URL `https:///wp-json/tinopay-gateway/v2/add-payment`, Token equal to the API Key from step 1, event Tiền vào (incoming), and turn on Bỏ qua nếu không có mã thanh toán (skip if no payment code).
3. On TinoPay → Cài đặt → Cấu trúc mã thanh toán: add the same prefix as the plugin, with a suffix length that fits your order numbers (e.g. 1–10, type alphanumeric or digits only).
| Symptom | Common cause |
| --- | --- |
| the customer paid but the order status did not change | the transfer description lacks or misspells `DH`; the plugin prefix differs from the code structure on TinoPay; the webhook is paused |
| webhook history shows `Invalid API Key` | the webhook token on TinoPay differs from the plugin's API Key (manual setup) |
| webhook history shows `Order not found` | the extracted code matches no order: check the suffix length in the payment code structure |
| the order has a short / excess payment note | the customer paid the wrong amount: the order keeps its status for you to handle |
| Connect TinoPay fails | the website is not on HTTPS, or the connection session passed 15 minutes: click again |
| no QR code on the thank-you page | no bank account or virtual account selected in the plugin settings |
Each delivery (HTTP status, data sent, the website's answer) is in the TinoPay web app → Tích hợp → Lịch sử webhook (Webhook history).
## HostBill
The TinoPay payment module for HostBill shows a VietQR code on the invoice and records the payment automatically when the customer pays: the invoice turns Paid within seconds, with no manual reconciliation. The module uses [webhooks](/en/docs/tinopay-webhook): every incoming transaction whose description contains an invoice code is sent to HostBill.
1. The customer opens an invoice in HostBill and chooses TinoPay; the invoice shows a VietQR code with the description `` (e.g. `TNG2026`).
2. The customer pays; TinoPay extracts the payment code `TNG2026`.
3. The webhook reaches HostBill; the module records the transaction and the invoice turns Paid.
- Full amount: the transaction is added to the invoice and the invoice turns Paid.
- Short or excess amount: ignored by default; turn on Allow lower / higher amount to record it anyway.
- The customer's invoice page reloads by itself once the invoice is paid.
Requirements: permission to install HostBill payment modules (the `includes/modules/Payment/` folder); a TinoPay account and at least one connected bank account; for BIDV, receive money on a virtual account.
### Installing the module
1. [Download the HostBill TinoPay module (.zip)](https://pay.tino.vn/downloads/hostbill-tinopay.zip) and extract it into HostBill's payment module folder, giving the structure in the block below.
2. HostBill Admin → Settings → Modules → Payment Modules → activate TinoPay - Quét QR Code chuyển khoản. Note the module ID, used in the webhook URL in step 4.
3. Configure the module with the table below.
4. On TinoPay: Cài đặt → Cấu trúc mã thanh toán, add a prefix equal to Payment Code Prefix (e.g. `TNG`), suffix type digits only, with a length that fits invoice numbers (e.g. 1–10). Then Tích hợp → Webhooks → Thêm webhook with the last table of this section; you can turn on skip if no payment code so only transactions with an invoice code reach HostBill.
5. Test: create a small invoice and choose TinoPay; a QR code on the invoice means the module works. Make a test transfer: a few seconds later the invoice turns Paid. Each delivery is in Tích hợp → Lịch sử webhook on TinoPay.
```text
includes/modules/Payment/tinopay/class.tinopay.php
includes/modules/Payment/tinopay/user/class.tinopay_controller.php
```
| Module field | Description |
| --- | --- |
| TinoPay URL | `https://pay.tino.vn` |
| Bank Info | one receiving account per line, in the format of the block below; for BIDV use the virtual account number; empty minimum / maximum means no limit; with several lines the customer can choose the bank |
| Payment Code Prefix | transfer description prefix, e.g. `TNG` → description `TNG2026` for invoice 2026 |
| Template QR | TinoPay frame with the bank logo, or the QR code only |
| Webhook Token | secret string, must equal the webhook's verification token on TinoPay |
| Allow lower / higher amount | record payments that are short of or above the invoice amount |
| Periodic invoice status check interval | how often the invoice page checks whether it is paid, 1–10 seconds, default 3 |
```text
bank_code|account_holder_name|account_number|minimum_amount|maximum_amount
BIDV|CONG TY ABC|9624QA01||
```
| Webhook on TinoPay | Value |
| --- | --- |
| Receiving URL | `https:///index.php?cmd=callback&module=` |
| Bank account | the account listed in Bank Info (BIDV: pick the virtual account) |
| Verification token | equal to the module's Webhook Token |
| Event | Tiền vào (incoming) |
### The webhook sent to HostBill
HostBill receives TinoPay's standard [webhook request](/en/docs/tinopay-webhook#the-request-tinopay-sends). The module takes the invoice code from `code` (or searches `content` when it is empty), strips the prefix and HostBill's InvoicePrefix, checks the amount, then adds the transaction to the invoice with `transaction_id` as the transaction code, so resending the same transaction does not record it twice.
| Result | HTTP status the module returns |
| --- | --- |
| recorded | 201 |
| skipped: already recorded, outgoing, no invoice code, invoice not found, short or excess amount (when not allowed) | 200 |
| wrong token | 401 |
| invalid data | 400 |
TinoPay resends the webhook when it gets a non-2xx status: see [Responses and retries](/en/docs/tinopay-webhook#responses-and-retries).
| Symptom | Common cause |
| --- | --- |
| no QR code on the invoice | malformed Bank Info, or the invoice amount is outside every line's minimum / maximum |
| webhook history shows `401` | the Webhook Token in HostBill differs from the webhook's verification token on TinoPay |
| webhook history shows `200` but the invoice is not Paid | read `message` in the answer: no invoice code (the prefix on TinoPay differs from Payment Code Prefix), short or excess amount, or a wrong invoice code |
| webhooks do not reach HostBill | the webhook has the wrong account or virtual account, the event is not incoming, or the URL has the wrong module ID |
## Sapo
Confirms bank-transfer payments automatically for orders on a Sapo online store: the thank-you page shows a VietQR code with the transfer description; a few seconds after the customer pays, the Sapo order is recorded as paid.
1. The customer places an order and chooses bank transfer; the thank-you page shows a VietQR code with the description `DH`.
2. The customer pays; TinoPay extracts the order number from the transfer description.
3. TinoPay calls the Sapo Admin API to record the payment; the order becomes paid.
- Full amount (or more): TinoPay records a payment of exactly the order total.
- Short amount: the order stays as is, and TinoPay lists it under recent reconciliation for you to handle.
- Order already paid or cancelled: skipped, never recorded twice.
- The thank-you page switches to a "received … for order …" message once the order is recorded.
Requirements: a Sapo online store (admin address like `https://ten-cua-hang.mysapo.net/admin`); Sapo Omni / POS are not supported yet; a TinoPay account and at least one connected bank account; for BIDV, pick a receiving virtual account.
### Setting up Sapo
1. Sapo Admin → Ứng dụng → Ứng dụng riêng → Tạo ứng dụng riêng (Apps → Private apps → Create private app), named e.g. `TinoPay`. Permissions: set the orders, transactions and shipping row to read and write, everything else to not allowed. Save, then copy the API Key and API Secret.
2. TinoPay web app → Tích hợp → Sapo → Thêm cửa hàng (Add store), filled in with the table below. TinoPay tests the connection to Sapo before saving; click Kiểm tra kết nối (Test connection) to try first.
3. Sapo Admin → Cấu hình → Phương thức thanh toán → Phương thức thanh toán thủ công → Thêm phương thức (Settings → Payment methods → Manual payment methods → Add). Type: bank transfer. The name must contain "Chuyển khoản" (bank transfer), e.g. `Chuyển khoản ngân hàng (Tự động xác nhận)`: the thank-you page shows the QR code only for orders using a bank-transfer method, never for COD. Add payment instructions, e.g. scan the QR code on the order confirmation page and keep the transfer description; the order is confirmed automatically within seconds. Save.
4. On TinoPay, on the store card, click Copy next to Mã nhúng trang thanh toán (Checkout embed code). In Sapo Admin → Cấu hình → Trang thanh toán (Settings → Checkout), section order processing, paste it into the additional message field (or the checkout footer field) and save.
5. Test: place an order with the bank-transfer method from step 3; a QR code on the order confirmation page means the setup is right. Pay a small amount: the Sapo order becomes paid and a reconciliation row appears on the Tích hợp → Sapo page.
| Store field | Description |
| --- | --- |
| Integration name | any name, e.g. `Sapo Web` |
| Receiving bank | bank account shown on the QR code; for BIDV also pick a virtual account, and only transactions to it are reconciled |
| Payment code prefix | one of the prefixes set in Cài đặt → Cấu trúc mã thanh toán (e.g. `DH`); the transfer description is prefix + Sapo order number, e.g. order #1001 → `DH1001` |
| Store URL | like `https://ten-cua-hang.mysapo.net` (the `.mysapo.net` address, not a custom domain) |
| API Key / API Secret | from step 1; the secret is encrypted when saved, and left empty on edit it keeps the old secret |
| Parameter settings | parameters of the transaction TinoPay creates on the Sapo order, next section |
| Status | active, paused |
The embed code looks like the block below (paste the copied code as is, script address included). It only holds the store's public key, never the API Key or API Secret. The thank-you page shows the QR code only for orders created in the last 7 days.
```html
```
### Sapo transaction parameters
When the customer pays the full amount, TinoPay creates a payment transaction on the order through the [Sapo Transaction API](https://support.sapo.vn/transaction#create) (`POST /admin/orders/{id}/transactions.json`) for the order total. Three parameters shape that transaction:
| Parameter | Values | Meaning |
| --- | --- | --- |
| `kind` | `capture` (default), `sale`, `authorization` | transaction kind; `capture` collects money for a placed order, which suits manual bank-transfer orders |
| `source_name` | not customised, `web`, `pos`, `iphone`, `android` | transaction source; left empty, Sapo sets it |
| `status` | not customised, `success`, `pending`, `failure`, `error` | transaction status; left empty, Sapo sets it |
The defaults (`capture`, not customised) suit most stores; change them only when a Sapo order does not become paid after the payment is recorded.
### Sapo reconciliation
The Tích hợp → Sapo page shows the 20 latest transactions with an order code:
| Status | Meaning |
| --- | --- |
| Đã ghi nhận (Recorded) | the payment was recorded on the Sapo order |
| Chuyển thiếu (Short) | less than the order total, not recorded; handle it manually in Sapo |
| Đã thanh toán trước (Already paid) | the order was already paid, skipped |
| Không thấy đơn (Order not found) | no order has the number in the transfer description |
| Đơn đã huỷ (Cancelled) | the order is cancelled, not recorded |
| Lỗi (Error) | Sapo refused or could not be reached (wrong API Key / API Secret, app deleted…) |
Click Gửi lại hook (Resend hook) on the transaction's detail page to reconcile again after fixing the configuration; paid orders are never recorded twice.
| Symptom | Common cause |
| --- | --- |
| no QR code on the thank-you page | the embed code is not pasted, the integration is paused, or the Sapo payment method name lacks "Chuyển khoản" |
| saving the integration reports a wrong API Key / API Secret | the private app lacks read and write access to orders, or characters were lost when copying |
| the customer paid but the order is not confirmed | the transfer description lacks the order code (`DH1001`), the amount is short, or the money went to a different account or virtual account than the one selected; check recent reconciliation |
---
# API Reference
166 routes of the gateway `https://api.tino.vn/v1` and the TinoPay, Tino Uptime and TinoPage Builder service APIs, by product. Each route has a page: parameters, cURL, JavaScript, PHP and SDK examples, response sample. Authentication, sending parameters and common errors: [Calling the Tino API](/en/docs/api). Machine-readable: [/en/openapi.json](/en/openapi.json).
## Identity & SSO
### Account
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/clientarea` | [Account overview](/en/docs/api/get-clientarea) | `profile:read` |
| `GET` | `/v1/clientarea/nav/service` | [Service categories](/en/docs/api/get-clientarea-nav-service) | `profile:read` |
| `POST` | `/v1/clientarea/search/@query` | [Search services and products](/en/docs/api/post-clientarea-search-query) | `profile:read` |
| `GET` | `/v1/clientarea/statuses` | [List statuses](/en/docs/api/get-clientarea-statuses) | `profile:read` |
| `GET` | `/v1/contact` | [List contacts](/en/docs/api/get-contact) | `profile:read` |
| `GET` | `/v1/contact/@id` | [Contact details](/en/docs/api/get-contact-id) | `profile:read` |
| `GET` | `/v1/contact/privileges` | [Permissions available for contacts](/en/docs/api/get-contact-privileges) | `profile:read` |
| `GET` | `/v1/contacts` | [List contacts (contacts)](/en/docs/api/get-contacts) | `profile:read` |
| `GET` | `/v1/details` | [Account details](/en/docs/api/get-details) | `profile:read` |
| `GET` | `/v1/notifications` | [List notifications](/en/docs/api/get-notifications) | `profile:read` |
| `PUT` | `/v1/notifications/@id/ack` | [Mark notification as read](/en/docs/api/put-notifications-id-ack) | `profile:write` |
| `GET` | `/v1/notifications/new` | [Unread notifications](/en/docs/api/get-notifications-new) | `profile:read` |
## Domains & DNS
### Domains
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/domain` | [List domains](/en/docs/api/get-domain) | `domains:read` |
| `GET` | `/v1/domain/@id` | [Domain details](/en/docs/api/get-domain-id) | `domains:read` |
| `GET` | `/v1/domain/@id/autorenew` | [Get domain auto-renewal](/en/docs/api/get-domain-id-autorenew) | `domains:read` |
| `PUT` | `/v1/domain/@id/autorenew` | [Turn domain auto-renewal on or off](/en/docs/api/put-domain-id-autorenew) | `domains:write` |
| `GET` | `/v1/domain/@id/contact` | [Get domain contacts](/en/docs/api/get-domain-id-contact) | `domains:read` |
| `PUT` | `/v1/domain/@id/contact` | [Change domain contacts (registrant)](/en/docs/api/put-domain-id-contact) | `domains:owner` |
| `GET` | `/v1/domain/@id/dnssec` | [List DNSSEC keys](/en/docs/api/get-domain-id-dnssec) | `domains:read` |
| `PUT` | `/v1/domain/@id/dnssec` | [Add DNSSEC key](/en/docs/api/put-domain-id-dnssec) | `domains:write` |
| `DELETE` | `/v1/domain/@id/dnssec/@key` | [Delete DNSSEC key](/en/docs/api/delete-domain-id-dnssec-key) | `domains:write` |
| `GET` | `/v1/domain/@id/dnssec/flags` | [Supported DNSSEC parameters](/en/docs/api/get-domain-id-dnssec-flags) | `domains:read` |
| `GET` | `/v1/domain/@id/emforwarding` | [Get email forwarding](/en/docs/api/get-domain-id-emforwarding) | `domains:read` |
| `PUT` | `/v1/domain/@id/emforwarding` | [Update email forwarding](/en/docs/api/put-domain-id-emforwarding) | `domains:write` |
| `GET` | `/v1/domain/@id/epp` | [Get domain transfer code (EPP)](/en/docs/api/get-domain-id-epp) | `domains:owner` |
| `PUT` | `/v1/domain/@id/forwarding` | [Update domain forwarding](/en/docs/api/put-domain-id-forwarding) | `domains:write` |
| `PUT` | `/v1/domain/@id/idprotection` | [Turn registrant privacy on or off](/en/docs/api/put-domain-id-idprotection) | `domains:write` |
| `GET` | `/v1/domain/@id/invoices` | [Domain invoices](/en/docs/api/get-domain-id-invoices) | `domains:read` |
| `GET` | `/v1/domain/@id/ns` | [Get nameservers](/en/docs/api/get-domain-id-ns) | `domains:read` |
| `PUT` | `/v1/domain/@id/ns` | [Change nameservers](/en/docs/api/put-domain-id-ns) | `domains:write` |
| `GET` | `/v1/domain/@id/reg` | [Registered private nameservers](/en/docs/api/get-domain-id-reg) | `domains:read` |
| `POST` | `/v1/domain/@id/reg` | [Manage private nameservers](/en/docs/api/post-domain-id-reg) | `domains:write` |
| `GET` | `/v1/domain/@id/reglock` | [Get domain lock](/en/docs/api/get-domain-id-reglock) | `domains:read` |
| `PUT` | `/v1/domain/@id/reglock` | [Turn domain lock on or off](/en/docs/api/put-domain-id-reglock) | `domains:write` |
| `POST` | `/v1/domain/@id/renew` | [Renew domain](/en/docs/api/post-domain-id-renew) | `domains:write` |
| `GET` | `/v1/domain/@id/sync` | [Sync domain](/en/docs/api/get-domain-id-sync) | `domains:write` |
| `POST` | `/v1/domain/lookup` | [Check domain availability](/en/docs/api/post-domain-lookup) | `domains:read` |
| `GET` | `/v1/domain/name/@name` | [Domain details by name](/en/docs/api/get-domain-name-name) | `domains:read` |
| `GET` | `/v1/domain/order` | [Available domain extensions](/en/docs/api/get-domain-order) | `domains:read` |
| `POST` | `/v1/domain/order` | [Order domain](/en/docs/api/post-domain-order) | `domains:write` |
| `GET` | `/v1/domain/order/@id/form` | [Extra fields per domain extension](/en/docs/api/get-domain-order-id-form) | `domains:read` |
| `GET` | `/v1/domains` | [List domains (paginated)](/en/docs/api/get-domains) | `domains:read` |
| `GET` | `/v1/domains/@id/registrant` | [Read a waiting domain's registrant](/en/docs/api/get-domains-id-registrant) | `domains:registrant` |
| `PUT` | `/v1/domains/@id/registrant` | [Save a waiting domain's registrant](/en/docs/api/put-domains-id-registrant) | `domains:registrant` |
| `GET` | `/v1/domains/pending` | [Domains awaiting their dossier](/en/docs/api/get-domains-pending) | `domains:registrant` |
| `GET` | `/v1/registrants` | [Registrants used before](/en/docs/api/get-registrants) | `domains:registrant` |
| `GET` | `/v1/whois/@domain` | [Domain whois lookup (with a token)](/en/docs/api/get-v1-whois-domain) | `domains:read` |
| `GET` | `/whois/@domain` | [Domain whois lookup](/en/docs/api/get-whois-domain) | — |
### DNS
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/dns` | [List DNS zones](/en/docs/api/get-dns) | `dns:read` |
| `GET` | `/v1/dns/templates` | [DNS record templates](/en/docs/api/get-dns-templates) | `dns:read` |
| `GET` | `/v1/domain/@id/dns` | [Domain DNS records](/en/docs/api/get-domain-id-dns) | `dns:read` |
| `POST` | `/v1/domain/@id/dns` | [Add DNS record to domain](/en/docs/api/post-domain-id-dns) | `dns:write` |
| `PUT` | `/v1/domain/@id/dns/@index` | [Edit domain DNS record](/en/docs/api/put-domain-id-dns-index) | `dns:write` |
| `DELETE` | `/v1/domain/@id/dns/@index` | [Delete domain DNS record](/en/docs/api/delete-domain-id-dns-index) | `dns:write` |
| `GET` | `/v1/domain/@id/dns/types` | [Supported DNS record types](/en/docs/api/get-domain-id-dns-types) | `dns:read` |
| `GET` | `/v1/service/@service_id/dns` | [DNS zones of a service](/en/docs/api/get-service-service-id-dns) | `dns:read` |
| `POST` | `/v1/service/@service_id/dns` | [Add DNS zone](/en/docs/api/post-service-service-id-dns) | `dns:write` |
| `GET` | `/v1/service/@service_id/dns/@zone_id` | [DNS zone details](/en/docs/api/get-service-service-id-dns-zone-id) | `dns:read` |
| `PUT` | `/v1/service/@service_id/dns/@zone_id` | [Edit DNS zone](/en/docs/api/put-service-service-id-dns-zone-id) | `dns:write` |
| `DELETE` | `/v1/service/@service_id/dns/@zone_id` | [Delete DNS zone](/en/docs/api/delete-service-service-id-dns-zone-id) | `dns:write` |
| `GET` | `/v1/service/@service_id/dns/@zone_id/dnssec` | [Get DNS zone DNSSEC](/en/docs/api/get-service-service-id-dns-zone-id-dnssec) | `dns:read` |
| `PUT` | `/v1/service/@service_id/dns/@zone_id/dnssec` | [Turn DNS zone DNSSEC on or off](/en/docs/api/put-service-service-id-dns-zone-id-dnssec) | `dns:write` |
| `POST` | `/v1/service/@service_id/dns/@zone_id/records` | [Add DNS record](/en/docs/api/post-service-service-id-dns-zone-id-records) | `dns:write` |
| `PUT` | `/v1/service/@service_id/dns/@zone_id/records/@record_id` | [Edit DNS record](/en/docs/api/put-service-service-id-dns-zone-id-records-record-id) | `dns:write` |
| `DELETE` | `/v1/service/@service_id/dns/@zone_id/records/@record_id` | [Delete DNS record](/en/docs/api/delete-service-service-id-dns-zone-id-records-record-id) | `dns:write` |
## Cloud & Hosting
### Services
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/service` | [List services](/en/docs/api/get-service) | `services:read` |
| `GET` | `/v1/service/@id` | [Service details](/en/docs/api/get-service-id) | `services:read` |
| `POST` | `/v1/service/@id/cancel` | [Request service cancellation](/en/docs/api/post-service-id-cancel) | `services:write` |
| `GET` | `/v1/service/@id/cycle` | [Billing cycles](/en/docs/api/get-service-id-cycle) | `services:read` |
| `POST` | `/v1/service/@id/cycle` | [Change billing cycle](/en/docs/api/post-service-id-cycle) | `services:write` |
| `GET` | `/v1/service/@id/emails` | [Hosting email accounts](/en/docs/api/get-service-id-emails) | `services:read` |
| `GET` | `/v1/service/@id/invoices` | [Service invoices](/en/docs/api/get-service-id-invoices) | `services:read` |
| `GET` | `/v1/service/@id/label` | [Get service label](/en/docs/api/get-service-id-label) | `services:read` |
| `POST` | `/v1/service/@id/label` | [Set service label](/en/docs/api/post-service-id-label) | `services:write` |
| `POST` | `/v1/service/@id/manualrenew` | [Create service renewal invoice](/en/docs/api/post-service-id-manualrenew) | `services:write` |
| `GET` | `/v1/service/@id/methods` | [Supported service actions](/en/docs/api/get-service-id-methods) | `services:read` |
| `POST` | `/v1/service/@id/renew` | [Renew service](/en/docs/api/post-service-id-renew) | `services:write` |
| `GET` | `/v1/service/@id/upgrade` | [Upgrade options](/en/docs/api/get-service-id-upgrade) | `services:read` |
| `POST` | `/v1/service/@id/upgrade` | [Upgrade quote or request](/en/docs/api/post-service-id-upgrade) | `services:write` |
| `GET` | `/v1/service/@id/widgets` | [Service widgets](/en/docs/api/get-service-id-widgets) | `services:read` |
| `GET` | `/v1/service/cpanel2/@id/statistics` | [cPanel statistics](/en/docs/api/get-service-cpanel2-id-statistics) | `services:read` |
| `POST` | `/v1/service/cpanel2/@id/unblock` | [Unblock IP on cPanel](/en/docs/api/post-service-cpanel2-id-unblock) | `services:write` |
| `GET` | `/v1/services/@cid` | [Services by category](/en/docs/api/get-services-cid) | `services:read` |
| `GET` | `/v1/services/active` | [Active services](/en/docs/api/get-services-active) | `services:read` |
### VPS servers
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/service/@id/networks` | [Networks and IP ranges](/en/docs/api/get-service-id-networks) | `services:read` |
| `GET` | `/v1/service/@id/resources` | [VPS resources](/en/docs/api/get-service-id-resources) | `services:read` |
| `GET` | `/v1/service/@id/templates` | [OS templates](/en/docs/api/get-service-id-templates) | `services:read` |
| `GET` | `/v1/service/@id/vms` | [List virtual machines](/en/docs/api/get-service-id-vms) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid` | [Virtual machine details](/en/docs/api/get-service-id-vms-vmid) | `services:read` |
| `POST` | `/v1/service/@id/vms/@vmid/hostname` | [Change VM hostname](/en/docs/api/post-service-id-vms-vmid-hostname) | `services:write` |
| `GET` | `/v1/service/@id/vms/@vmid/interfaces` | [VM network interfaces](/en/docs/api/get-service-id-vms-vmid-interfaces) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid/interfaces/@iface` | [Network interface details](/en/docs/api/get-service-id-vms-vmid-interfaces-iface) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid/ippool` | [VM IP pools](/en/docs/api/get-service-id-vms-vmid-ippool) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid/ips` | [VM IP addresses](/en/docs/api/get-service-id-vms-vmid-ips) | `services:read` |
| `POST` | `/v1/service/@id/vms/@vmid/reboot` | [Restart VM](/en/docs/api/post-service-id-vms-vmid-reboot) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/reset` | [Reset VM](/en/docs/api/post-service-id-vms-vmid-reset) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/shutdown` | [Shut down VM](/en/docs/api/post-service-id-vms-vmid-shutdown) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/start` | [Start VM](/en/docs/api/post-service-id-vms-vmid-start) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/stop` | [Stop VM](/en/docs/api/post-service-id-vms-vmid-stop) | `services:write` |
| `GET` | `/v1/service/@id/vms/@vmid/traffic` | [VM network traffic](/en/docs/api/get-service-id-vms-vmid-traffic) | `services:read` |
### SSL certificates
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/certificate` | [List SSL certificates](/en/docs/api/get-certificate) | `services:read` |
| `GET` | `/v1/certificate/@id` | [SSL certificate details](/en/docs/api/get-certificate-id) | `services:read` |
| `GET` | `/v1/certificate/@id/crt` | [Download certificate](/en/docs/api/get-certificate-id-crt) | `services:read` |
| `GET` | `/v1/certificate/order` | [Certificates available to order](/en/docs/api/get-certificate-order) | `services:read` |
| `POST` | `/v1/certificate/order` | [Order SSL certificate](/en/docs/api/post-certificate-order) | `services:write` |
| `GET` | `/v1/certificate/order/@product_id/software` | [Server software for certificates](/en/docs/api/get-certificate-order-product-id-software) | `services:read` |
| `GET` | `/v1/certificates` | [List SSL services (certificates)](/en/docs/api/get-certificates) | `services:read` |
## Billing
### Invoices & payments
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/addfunds` | [Top-up information](/en/docs/api/get-addfunds) | `billing:read` |
| `POST` | `/v1/addfunds` | [Top up](/en/docs/api/post-addfunds) | `billing:write` |
| `GET` | `/v1/balance` | [Account balance](/en/docs/api/get-balance) | `billing:read` |
| `GET` | `/v1/billing/@invoice_id/pay/@gateway_id` | [Invoice payment link](/en/docs/api/get-billing-invoice-id-pay-gateway-id) | `billing:write` |
| `POST` | `/v1/billing/addfunds` | [Top up (billing)](/en/docs/api/post-billing-addfunds) | `billing:write` |
| `GET` | `/v1/billing/creditlogs` | [Credit balance history](/en/docs/api/get-billing-creditlogs) | `billing:read` |
| `POST` | `/v1/billing/creditvoucher` | [Redeem top-up code](/en/docs/api/post-billing-creditvoucher) | `billing:write` |
| `GET` | `/v1/estimate/@id/pdf` | [Download estimate PDF](/en/docs/api/get-estimate-id-pdf) | `billing:read` |
| `GET` | `/v1/estimates` | [List estimates](/en/docs/api/get-estimates) | `billing:read` |
| `GET` | `/v1/estimates/@id` | [Estimate details](/en/docs/api/get-estimates-id) | `billing:read` |
| `GET` | `/v1/invoice` | [List invoices](/en/docs/api/get-invoice) | `billing:read` |
| `GET` | `/v1/invoice/@id` | [Invoice details](/en/docs/api/get-invoice-id) | `billing:read` |
| `DELETE` | `/v1/invoice/@id` | [Delete invoice](/en/docs/api/delete-invoice-id) | `billing:write` |
| `POST` | `/v1/invoice/@id/credit` | [Pay invoice with balance](/en/docs/api/post-invoice-id-credit) | `billing:write` |
| `GET` | `/v1/invoice/@id/pdf` | [Download invoice PDF](/en/docs/api/get-invoice-id-pdf) | `billing:read` |
| `GET` | `/v1/invoices` | [List invoices (paginated)](/en/docs/api/get-invoices) | `billing:read` |
| `GET` | `/v1/invoices/due` | [Due invoices](/en/docs/api/get-invoices-due) | `billing:read` |
| `POST` | `/v1/invoices/payall` | [Pay invoices together](/en/docs/api/post-invoices-payall) | `billing:write` |
| `GET` | `/v1/payment` | [Payment methods](/en/docs/api/get-payment) | `billing:read` |
| `GET` | `/v1/payment/fees` | [Payment method fees](/en/docs/api/get-payment-fees) | `billing:read` |
### Orders
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `POST` | `/v1/carts` | [Create a cart](/en/docs/api/post-carts) | `orders:write` |
| `GET` | `/v1/carts/@uuid` | [Get a cart](/en/docs/api/get-carts-uuid) | `orders:write` |
| `PUT` | `/v1/carts/@uuid` | [Update a cart](/en/docs/api/put-carts-uuid) | `orders:write` |
| `POST` | `/v1/carts/@uuid/confirm` | [Place the order from a cart](/en/docs/api/post-carts-uuid-confirm) | `orders:write` |
| `GET` | `/v1/carts/latest` | [Latest cart](/en/docs/api/get-carts-latest) | `orders:write` |
| `POST` | `/v1/order` | [Order several services](/en/docs/api/post-order) | `orders:write` |
| `POST` | `/v1/order/@product_id` | [Order one service](/en/docs/api/post-order-product-id) | `orders:write` |
| `POST` | `/v1/quote` | [Get an order quote](/en/docs/api/post-quote) | `orders:write` |
## TinoPay
### Merchant
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay` | [Merchant details](/en/docs/api/tinopay-get-merchant) | — |
| `PATCH` | `/service/@id/tinopay` | [Update merchant details](/en/docs/api/tinopay-patch-merchant) | — |
| `GET` | `/service/@id/tinopay/pay-codes` | [Payment code patterns](/en/docs/api/tinopay-get-pay-codes) | — |
| `PUT` | `/service/@id/tinopay/pay-codes` | [Save payment code patterns](/en/docs/api/tinopay-put-pay-codes) | — |
### Bank accounts
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/bank-accounts` | [List bank accounts](/en/docs/api/tinopay-get-bank-accounts) | — |
| `GET` | `/service/@id/tinopay/bank-accounts/@bank` | [Bank account details](/en/docs/api/tinopay-get-bank-accounts-bank) | — |
| `POST` | `/service/@id/tinopay/bank-links/bidv-business` | [Request a BIDV business link](/en/docs/api/tinopay-post-bank-links-bidv-business) | — |
### Virtual accounts
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts` | [List virtual accounts (VA)](/en/docs/api/tinopay-get-bank-accounts-bank-virtual-accounts) | — |
| `POST` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts` | [Create a virtual account](/en/docs/api/tinopay-post-bank-accounts-bank-virtual-accounts) | — |
| `POST` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts/@va/activate` | [Activate a virtual account](/en/docs/api/tinopay-post-bank-accounts-bank-virtual-accounts-va-activate) | — |
| `POST` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts/@va/deactivate` | [Deactivate a virtual account](/en/docs/api/tinopay-post-bank-accounts-bank-virtual-accounts-va-deactivate) | — |
| `DELETE` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts/@va` | [Delete a virtual account](/en/docs/api/tinopay-delete-bank-accounts-bank-virtual-accounts-va) | — |
### Webhooks
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/webhooks` | [List webhooks](/en/docs/api/tinopay-get-webhooks) | — |
| `POST` | `/service/@id/tinopay/webhooks` | [Create a webhook](/en/docs/api/tinopay-post-webhooks) | — |
| `GET` | `/service/@id/tinopay/webhooks/@webhook` | [Webhook details](/en/docs/api/tinopay-get-webhooks-webhook) | — |
| `PATCH` | `/service/@id/tinopay/webhooks/@webhook` | [Update a webhook](/en/docs/api/tinopay-patch-webhooks-webhook) | — |
| `DELETE` | `/service/@id/tinopay/webhooks/@webhook` | [Delete a webhook](/en/docs/api/tinopay-delete-webhooks-webhook) | — |
| `GET` | `/service/@id/tinopay/webhook-logs` | [Webhook logs](/en/docs/api/tinopay-get-webhook-logs) | — |
| `POST` | `/service/@id/tinopay/webhook-logs/@log/resend` | [Resend a webhook log](/en/docs/api/tinopay-post-webhook-logs-log-resend) | — |
### Transactions
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/transactions` | [Transaction history](/en/docs/api/tinopay-get-transactions) | — |
| `GET` | `/service/@id/tinopay/transactions/summary` | [Transaction summary](/en/docs/api/tinopay-get-transactions-summary) | — |
| `GET` | `/service/@id/tinopay/transactions/@transaction` | [Transaction details](/en/docs/api/tinopay-get-transactions-transaction) | — |
| `POST` | `/service/@id/tinopay/transactions/@transaction/resend-webhook` | [Resend webhooks for a transaction](/en/docs/api/tinopay-post-transactions-transaction-resend-webhook) | — |
### Notifications
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `POST` | `/service/@id/tinopay/notification-apps/bot-info` | [Check a bot token](/en/docs/api/tinopay-post-notification-apps-bot-info) | — |
| `POST` | `/service/@id/tinopay/notification-apps/find-chats` | [Find a bot's chats](/en/docs/api/tinopay-post-notification-apps-find-chats) | — |
| `POST` | `/service/@id/tinopay/notification-apps/test` | [Send a test message](/en/docs/api/tinopay-post-notification-apps-test) | — |
| `GET` | `/service/@id/tinopay/notification-apps` | [List notification apps](/en/docs/api/tinopay-get-notification-apps) | — |
| `POST` | `/service/@id/tinopay/notification-apps` | [Create a notification app](/en/docs/api/tinopay-post-notification-apps) | — |
| `GET` | `/service/@id/tinopay/notification-apps/@app` | [Notification app details](/en/docs/api/tinopay-get-notification-apps-app) | — |
| `PATCH` | `/service/@id/tinopay/notification-apps/@app` | [Update a notification app](/en/docs/api/tinopay-patch-notification-apps-app) | — |
| `DELETE` | `/service/@id/tinopay/notification-apps/@app` | [Delete a notification app](/en/docs/api/tinopay-delete-notification-apps-app) | — |
### Sapo integration
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `POST` | `/service/@id/tinopay/sapo-integrations/test` | [Test the Sapo connection](/en/docs/api/tinopay-post-sapo-integrations-test) | — |
| `GET` | `/service/@id/tinopay/sapo-integrations` | [List Sapo integrations](/en/docs/api/tinopay-get-sapo-integrations) | — |
| `POST` | `/service/@id/tinopay/sapo-integrations` | [Create a Sapo integration](/en/docs/api/tinopay-post-sapo-integrations) | — |
| `GET` | `/service/@id/tinopay/sapo-integrations/@integration` | [Get a Sapo integration](/en/docs/api/tinopay-get-sapo-integrations-integration) | — |
| `PATCH` | `/service/@id/tinopay/sapo-integrations/@integration` | [Update a Sapo integration](/en/docs/api/tinopay-patch-sapo-integrations-integration) | — |
| `DELETE` | `/service/@id/tinopay/sapo-integrations/@integration` | [Delete a Sapo integration](/en/docs/api/tinopay-delete-sapo-integrations-integration) | — |
| `GET` | `/service/@id/tinopay/sapo-order-logs` | [Sapo reconciliation log](/en/docs/api/tinopay-get-sapo-order-logs) | — |
### API tokens
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/api-tokens` | [List API tokens](/en/docs/api/tinopay-get-api-tokens) | — |
| `POST` | `/service/@id/tinopay/api-tokens` | [Create an API token](/en/docs/api/tinopay-post-api-tokens) | — |
| `DELETE` | `/service/@id/tinopay/api-tokens/@token` | [Revoke an API token](/en/docs/api/tinopay-delete-api-tokens-token) | — |
## Tino Uptime
### Uptime plan
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime` | [Tino Uptime plan overview](/en/docs/api/uptime-get-overview) | — |
| `GET` | `/service/@id/uptime/usage` | [Usage against plan limits](/en/docs/api/uptime-get-usage) | — |
### Monitors
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime/monitors` | [List monitors](/en/docs/api/uptime-get-monitors) | — |
| `POST` | `/service/@id/uptime/monitors` | [Create a monitor](/en/docs/api/uptime-post-monitors) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor` | [Monitor details](/en/docs/api/uptime-get-monitors-monitor) | — |
| `PUT` | `/service/@id/uptime/monitors/@monitor` | [Update a monitor](/en/docs/api/uptime-put-monitors-monitor) | — |
| `DELETE` | `/service/@id/uptime/monitors/@monitor` | [Delete a monitor](/en/docs/api/uptime-delete-monitors-monitor) | — |
| `POST` | `/service/@id/uptime/monitors/@monitor/pause` | [Pause a monitor](/en/docs/api/uptime-post-monitors-monitor-pause) | — |
| `POST` | `/service/@id/uptime/monitors/@monitor/resume` | [Resume a monitor](/en/docs/api/uptime-post-monitors-monitor-resume) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor/uptime` | [Monitor uptime](/en/docs/api/uptime-get-monitors-monitor-uptime) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor/results` | [Monitor check results](/en/docs/api/uptime-get-monitors-monitor-results) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor/incidents` | [Monitor incidents](/en/docs/api/uptime-get-monitors-monitor-incidents) | — |
### Incidents
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime/incidents` | [List incidents](/en/docs/api/uptime-get-incidents) | — |
| `GET` | `/service/@id/uptime/incidents/@incident` | [Incident details](/en/docs/api/uptime-get-incidents-incident) | — |
| `POST` | `/service/@id/uptime/incidents/@incident/ack` | [Acknowledge an incident](/en/docs/api/uptime-post-incidents-incident-ack) | — |
### Alert channels
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime/channels` | [List alert channels](/en/docs/api/uptime-get-channels) | — |
| `POST` | `/service/@id/uptime/channels` | [Create an alert channel](/en/docs/api/uptime-post-channels) | — |
| `POST` | `/service/@id/uptime/channels/@channel/verify` | [Verify an alert channel](/en/docs/api/uptime-post-channels-channel-verify) | — |
| `POST` | `/service/@id/uptime/channels/@channel/test` | [Send a test message](/en/docs/api/uptime-post-channels-channel-test) | — |
| `DELETE` | `/service/@id/uptime/channels/@channel` | [Delete an alert channel](/en/docs/api/uptime-delete-channels-channel) | — |
## TinoPage Builder
### Builder account
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/overview` | [TinoPage Builder account overview](/en/docs/api/builder-get-overview) | — |
| `POST` | `/service/@id/builder/login` | [One-time sign-in ticket](/en/docs/api/builder-post-login) | — |
### Websites
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/sites` | [List websites](/en/docs/api/builder-get-sites) | — |
| `GET` | `/service/@id/builder/sites/@site` | [Website details](/en/docs/api/builder-get-sites-site) | — |
| `POST` | `/service/@id/builder/sites` | [Create a website with AI](/en/docs/api/builder-post-sites) | — |
| `GET` | `/service/@id/builder/site-creation` | [Website creation progress](/en/docs/api/builder-get-site-creation) | — |
| `POST` | `/service/@id/builder/sites/@site/publish` | [Publish a website](/en/docs/api/builder-post-sites-site-publish) | — |
| `POST` | `/service/@id/builder/sites/@site/unpublish` | [Pause a website](/en/docs/api/builder-post-sites-site-unpublish) | — |
| `DELETE` | `/service/@id/builder/sites/@site` | [Delete a website](/en/docs/api/builder-delete-sites-site) | — |
### Custom domains
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/sites/@site/domains` | [Custom domains of a website](/en/docs/api/builder-get-sites-site-domains) | — |
| `POST` | `/service/@id/builder/sites/@site/domains` | [Add a custom domain](/en/docs/api/builder-post-sites-site-domains) | — |
| `POST` | `/service/@id/builder/sites/@site/domains/@hostname/verify` | [Check DNS again](/en/docs/api/builder-post-sites-site-domains-hostname-verify) | — |
| `DELETE` | `/service/@id/builder/sites/@site/domains/@hostname` | [Remove a custom domain](/en/docs/api/builder-delete-sites-site-domains-hostname) | — |
### Contacts & orders
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/sites/@site/leads` | [Contacts and orders](/en/docs/api/builder-get-sites-site-leads) | — |
## TinoWAF
### TinoWAF
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/service/@id/waf/events` | [Event summary](/en/docs/api/get-service-id-waf-events) | `waf:read` |
| `GET` | `/v1/service/@id/waf/ip-rules` | [List IP rules](/en/docs/api/get-service-id-waf-ip-rules) | `waf:read` |
| `POST` | `/v1/service/@id/waf/ip-rules` | [Block or allow an IP](/en/docs/api/post-service-id-waf-ip-rules) | `waf:write` |
| `DELETE` | `/v1/service/@id/waf/ip-rules/@rule` | [Delete an IP rule](/en/docs/api/delete-service-id-waf-ip-rules-rule) | `waf:write` |
| `POST` | `/v1/service/@id/waf/ip-rules/apply` | [Re-apply IP rules](/en/docs/api/post-service-id-waf-ip-rules-apply) | `waf:write` |
| `GET` | `/v1/service/@id/waf/overview` | [TinoWAF overview](/en/docs/api/get-service-id-waf-overview) | `waf:read` |
| `GET` | `/v1/service/@id/waf/zones` | [List websites](/en/docs/api/get-service-id-waf-zones) | `waf:read` |
| `POST` | `/v1/service/@id/waf/zones` | [Add a website](/en/docs/api/post-service-id-waf-zones) | `waf:write` |
| `GET` | `/v1/service/@id/waf/zones/@zone` | [Get a website](/en/docs/api/get-service-id-waf-zones-zone) | `waf:read` |
| `DELETE` | `/v1/service/@id/waf/zones/@zone` | [Delete a website](/en/docs/api/delete-service-id-waf-zones-zone) | `waf:write` |
| `POST` | `/v1/service/@id/waf/zones/@zone/check` | [Check DNS now](/en/docs/api/post-service-id-waf-zones-zone-check) | `waf:write` |
| `GET` | `/v1/service/@id/waf/zones/@zone/events` | [List a website's events](/en/docs/api/get-service-id-waf-zones-zone-events) | `waf:read` |
| `GET` | `/v1/service/@id/waf/zones/@zone/hostnames` | [List a website's domain names](/en/docs/api/get-service-id-waf-zones-zone-hostnames) | `waf:read` |
| `POST` | `/v1/service/@id/waf/zones/@zone/hostnames` | [Add a domain name to a website](/en/docs/api/post-service-id-waf-zones-zone-hostnames) | `waf:write` |
| `DELETE` | `/v1/service/@id/waf/zones/@zone/hostnames/@hostname` | [Remove a domain name from a website](/en/docs/api/delete-service-id-waf-zones-zone-hostnames-hostname) | `waf:write` |
| `GET` | `/v1/service/@id/waf/zones/@zone/origin` | [Get a website's origin servers](/en/docs/api/get-service-id-waf-zones-zone-origin) | `waf:read` |
| `PUT` | `/v1/service/@id/waf/zones/@zone/origin` | [Update origin servers](/en/docs/api/put-service-id-waf-zones-zone-origin) | `waf:write` |
| `PUT` | `/v1/service/@id/waf/zones/@zone/under-attack` | [Turn emergency anti-attack mode on or off](/en/docs/api/put-service-id-waf-zones-zone-under-attack) | `waf:write` |
| `PUT` | `/v1/service/@id/waf/zones/@zone/waf-mode` | [Change the protection level](/en/docs/api/put-service-id-waf-zones-zone-waf-mode) | `waf:write` |
## Tickets & support
### Tickets & support
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/knowledgebase` | [Knowledge base categories](/en/docs/api/get-knowledgebase) | `tickets:read` |
| `GET` | `/v1/knowledgebase/@category_id` | [Articles in a category](/en/docs/api/get-knowledgebase-category-id) | `tickets:read` |
| `GET` | `/v1/knowledgebase/article/@article_id` | [Knowledge base article](/en/docs/api/get-knowledgebase-article-article-id) | `tickets:read` |
| `GET` | `/v1/news` | [List news](/en/docs/api/get-news) | `tickets:read` |
| `GET` | `/v1/news/@news_id` | [News details](/en/docs/api/get-news-news-id) | `tickets:read` |
| `GET` | `/v1/ticket/attachment/@file` | [Download ticket attachment](/en/docs/api/get-ticket-attachment-file) | `tickets:read` |
| `GET` | `/v1/ticket/departments` | [Support departments](/en/docs/api/get-ticket-departments) | `tickets:read` |
| `GET` | `/v1/tickets` | [List tickets](/en/docs/api/get-tickets) | `tickets:read` |
| `POST` | `/v1/tickets` | [Create ticket](/en/docs/api/post-tickets) | `tickets:write` |
| `GET` | `/v1/tickets/@number` | [Ticket details](/en/docs/api/get-tickets-number) | `tickets:read` |
| `POST` | `/v1/tickets/@number` | [Reply to ticket](/en/docs/api/post-tickets-number) | `tickets:write` |
| `PUT` | `/v1/tickets/@number/close` | [Close ticket](/en/docs/api/put-tickets-number-close) | `tickets:write` |
| `PUT` | `/v1/tickets/@number/open` | [Reopen ticket](/en/docs/api/put-tickets-number-open) | `tickets:write` |
---
# Identity & SSO
Sign in with Tino (OAuth 2.0, OpenID Connect), personal API tokens, account details.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Guides
- [Sign in with Tino (OAuth 2.0, OpenID Connect)](/en/docs/oauth)
- [Personal API tokens](/en/docs/personal-tokens)
- [Sign in from a CLI or an app without a backend](/en/docs/cli-login)
## Account
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/clientarea` | [Account overview](/en/docs/api/get-clientarea) | `profile:read` |
| `GET` | `/v1/clientarea/nav/service` | [Service categories](/en/docs/api/get-clientarea-nav-service) | `profile:read` |
| `POST` | `/v1/clientarea/search/@query` | [Search services and products](/en/docs/api/post-clientarea-search-query) | `profile:read` |
| `GET` | `/v1/clientarea/statuses` | [List statuses](/en/docs/api/get-clientarea-statuses) | `profile:read` |
| `GET` | `/v1/contact` | [List contacts](/en/docs/api/get-contact) | `profile:read` |
| `GET` | `/v1/contact/@id` | [Contact details](/en/docs/api/get-contact-id) | `profile:read` |
| `GET` | `/v1/contact/privileges` | [Permissions available for contacts](/en/docs/api/get-contact-privileges) | `profile:read` |
| `GET` | `/v1/contacts` | [List contacts (contacts)](/en/docs/api/get-contacts) | `profile:read` |
| `GET` | `/v1/details` | [Account details](/en/docs/api/get-details) | `profile:read` |
| `GET` | `/v1/notifications` | [List notifications](/en/docs/api/get-notifications) | `profile:read` |
| `PUT` | `/v1/notifications/@id/ack` | [Mark notification as read](/en/docs/api/put-notifications-id-ack) | `profile:write` |
| `GET` | `/v1/notifications/new` | [Unread notifications](/en/docs/api/get-notifications-new) | `profile:read` |
---
# Domains & DNS
Look up, order and renew domains; nameservers, DNSSEC; DNS zones and records.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Guides
- [Domain registrant dossier](/en/docs/domain-registrant)
## Domains
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/domain` | [List domains](/en/docs/api/get-domain) | `domains:read` |
| `GET` | `/v1/domain/@id` | [Domain details](/en/docs/api/get-domain-id) | `domains:read` |
| `GET` | `/v1/domain/@id/autorenew` | [Get domain auto-renewal](/en/docs/api/get-domain-id-autorenew) | `domains:read` |
| `PUT` | `/v1/domain/@id/autorenew` | [Turn domain auto-renewal on or off](/en/docs/api/put-domain-id-autorenew) | `domains:write` |
| `GET` | `/v1/domain/@id/contact` | [Get domain contacts](/en/docs/api/get-domain-id-contact) | `domains:read` |
| `PUT` | `/v1/domain/@id/contact` | [Change domain contacts (registrant)](/en/docs/api/put-domain-id-contact) | `domains:owner` |
| `GET` | `/v1/domain/@id/dnssec` | [List DNSSEC keys](/en/docs/api/get-domain-id-dnssec) | `domains:read` |
| `PUT` | `/v1/domain/@id/dnssec` | [Add DNSSEC key](/en/docs/api/put-domain-id-dnssec) | `domains:write` |
| `DELETE` | `/v1/domain/@id/dnssec/@key` | [Delete DNSSEC key](/en/docs/api/delete-domain-id-dnssec-key) | `domains:write` |
| `GET` | `/v1/domain/@id/dnssec/flags` | [Supported DNSSEC parameters](/en/docs/api/get-domain-id-dnssec-flags) | `domains:read` |
| `GET` | `/v1/domain/@id/emforwarding` | [Get email forwarding](/en/docs/api/get-domain-id-emforwarding) | `domains:read` |
| `PUT` | `/v1/domain/@id/emforwarding` | [Update email forwarding](/en/docs/api/put-domain-id-emforwarding) | `domains:write` |
| `GET` | `/v1/domain/@id/epp` | [Get domain transfer code (EPP)](/en/docs/api/get-domain-id-epp) | `domains:owner` |
| `PUT` | `/v1/domain/@id/forwarding` | [Update domain forwarding](/en/docs/api/put-domain-id-forwarding) | `domains:write` |
| `PUT` | `/v1/domain/@id/idprotection` | [Turn registrant privacy on or off](/en/docs/api/put-domain-id-idprotection) | `domains:write` |
| `GET` | `/v1/domain/@id/invoices` | [Domain invoices](/en/docs/api/get-domain-id-invoices) | `domains:read` |
| `GET` | `/v1/domain/@id/ns` | [Get nameservers](/en/docs/api/get-domain-id-ns) | `domains:read` |
| `PUT` | `/v1/domain/@id/ns` | [Change nameservers](/en/docs/api/put-domain-id-ns) | `domains:write` |
| `GET` | `/v1/domain/@id/reg` | [Registered private nameservers](/en/docs/api/get-domain-id-reg) | `domains:read` |
| `POST` | `/v1/domain/@id/reg` | [Manage private nameservers](/en/docs/api/post-domain-id-reg) | `domains:write` |
| `GET` | `/v1/domain/@id/reglock` | [Get domain lock](/en/docs/api/get-domain-id-reglock) | `domains:read` |
| `PUT` | `/v1/domain/@id/reglock` | [Turn domain lock on or off](/en/docs/api/put-domain-id-reglock) | `domains:write` |
| `POST` | `/v1/domain/@id/renew` | [Renew domain](/en/docs/api/post-domain-id-renew) | `domains:write` |
| `GET` | `/v1/domain/@id/sync` | [Sync domain](/en/docs/api/get-domain-id-sync) | `domains:write` |
| `POST` | `/v1/domain/lookup` | [Check domain availability](/en/docs/api/post-domain-lookup) | `domains:read` |
| `GET` | `/v1/domain/name/@name` | [Domain details by name](/en/docs/api/get-domain-name-name) | `domains:read` |
| `GET` | `/v1/domain/order` | [Available domain extensions](/en/docs/api/get-domain-order) | `domains:read` |
| `POST` | `/v1/domain/order` | [Order domain](/en/docs/api/post-domain-order) | `domains:write` |
| `GET` | `/v1/domain/order/@id/form` | [Extra fields per domain extension](/en/docs/api/get-domain-order-id-form) | `domains:read` |
| `GET` | `/v1/domains` | [List domains (paginated)](/en/docs/api/get-domains) | `domains:read` |
| `GET` | `/v1/domains/@id/registrant` | [Read a waiting domain's registrant](/en/docs/api/get-domains-id-registrant) | `domains:registrant` |
| `PUT` | `/v1/domains/@id/registrant` | [Save a waiting domain's registrant](/en/docs/api/put-domains-id-registrant) | `domains:registrant` |
| `GET` | `/v1/domains/pending` | [Domains awaiting their dossier](/en/docs/api/get-domains-pending) | `domains:registrant` |
| `GET` | `/v1/registrants` | [Registrants used before](/en/docs/api/get-registrants) | `domains:registrant` |
| `GET` | `/v1/whois/@domain` | [Domain whois lookup (with a token)](/en/docs/api/get-v1-whois-domain) | `domains:read` |
| `GET` | `/whois/@domain` | [Domain whois lookup](/en/docs/api/get-whois-domain) | — |
## DNS
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/dns` | [List DNS zones](/en/docs/api/get-dns) | `dns:read` |
| `GET` | `/v1/dns/templates` | [DNS record templates](/en/docs/api/get-dns-templates) | `dns:read` |
| `GET` | `/v1/domain/@id/dns` | [Domain DNS records](/en/docs/api/get-domain-id-dns) | `dns:read` |
| `POST` | `/v1/domain/@id/dns` | [Add DNS record to domain](/en/docs/api/post-domain-id-dns) | `dns:write` |
| `PUT` | `/v1/domain/@id/dns/@index` | [Edit domain DNS record](/en/docs/api/put-domain-id-dns-index) | `dns:write` |
| `DELETE` | `/v1/domain/@id/dns/@index` | [Delete domain DNS record](/en/docs/api/delete-domain-id-dns-index) | `dns:write` |
| `GET` | `/v1/domain/@id/dns/types` | [Supported DNS record types](/en/docs/api/get-domain-id-dns-types) | `dns:read` |
| `GET` | `/v1/service/@service_id/dns` | [DNS zones of a service](/en/docs/api/get-service-service-id-dns) | `dns:read` |
| `POST` | `/v1/service/@service_id/dns` | [Add DNS zone](/en/docs/api/post-service-service-id-dns) | `dns:write` |
| `GET` | `/v1/service/@service_id/dns/@zone_id` | [DNS zone details](/en/docs/api/get-service-service-id-dns-zone-id) | `dns:read` |
| `PUT` | `/v1/service/@service_id/dns/@zone_id` | [Edit DNS zone](/en/docs/api/put-service-service-id-dns-zone-id) | `dns:write` |
| `DELETE` | `/v1/service/@service_id/dns/@zone_id` | [Delete DNS zone](/en/docs/api/delete-service-service-id-dns-zone-id) | `dns:write` |
| `GET` | `/v1/service/@service_id/dns/@zone_id/dnssec` | [Get DNS zone DNSSEC](/en/docs/api/get-service-service-id-dns-zone-id-dnssec) | `dns:read` |
| `PUT` | `/v1/service/@service_id/dns/@zone_id/dnssec` | [Turn DNS zone DNSSEC on or off](/en/docs/api/put-service-service-id-dns-zone-id-dnssec) | `dns:write` |
| `POST` | `/v1/service/@service_id/dns/@zone_id/records` | [Add DNS record](/en/docs/api/post-service-service-id-dns-zone-id-records) | `dns:write` |
| `PUT` | `/v1/service/@service_id/dns/@zone_id/records/@record_id` | [Edit DNS record](/en/docs/api/put-service-service-id-dns-zone-id-records-record-id) | `dns:write` |
| `DELETE` | `/v1/service/@service_id/dns/@zone_id/records/@record_id` | [Delete DNS record](/en/docs/api/delete-service-service-id-dns-zone-id-records-record-id) | `dns:write` |
---
# Cloud & Hosting
Hosting services, VPS servers (power, networks, IPs), SSL certificates.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Services
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/service` | [List services](/en/docs/api/get-service) | `services:read` |
| `GET` | `/v1/service/@id` | [Service details](/en/docs/api/get-service-id) | `services:read` |
| `POST` | `/v1/service/@id/cancel` | [Request service cancellation](/en/docs/api/post-service-id-cancel) | `services:write` |
| `GET` | `/v1/service/@id/cycle` | [Billing cycles](/en/docs/api/get-service-id-cycle) | `services:read` |
| `POST` | `/v1/service/@id/cycle` | [Change billing cycle](/en/docs/api/post-service-id-cycle) | `services:write` |
| `GET` | `/v1/service/@id/emails` | [Hosting email accounts](/en/docs/api/get-service-id-emails) | `services:read` |
| `GET` | `/v1/service/@id/invoices` | [Service invoices](/en/docs/api/get-service-id-invoices) | `services:read` |
| `GET` | `/v1/service/@id/label` | [Get service label](/en/docs/api/get-service-id-label) | `services:read` |
| `POST` | `/v1/service/@id/label` | [Set service label](/en/docs/api/post-service-id-label) | `services:write` |
| `POST` | `/v1/service/@id/manualrenew` | [Create service renewal invoice](/en/docs/api/post-service-id-manualrenew) | `services:write` |
| `GET` | `/v1/service/@id/methods` | [Supported service actions](/en/docs/api/get-service-id-methods) | `services:read` |
| `POST` | `/v1/service/@id/renew` | [Renew service](/en/docs/api/post-service-id-renew) | `services:write` |
| `GET` | `/v1/service/@id/upgrade` | [Upgrade options](/en/docs/api/get-service-id-upgrade) | `services:read` |
| `POST` | `/v1/service/@id/upgrade` | [Upgrade quote or request](/en/docs/api/post-service-id-upgrade) | `services:write` |
| `GET` | `/v1/service/@id/widgets` | [Service widgets](/en/docs/api/get-service-id-widgets) | `services:read` |
| `GET` | `/v1/service/cpanel2/@id/statistics` | [cPanel statistics](/en/docs/api/get-service-cpanel2-id-statistics) | `services:read` |
| `POST` | `/v1/service/cpanel2/@id/unblock` | [Unblock IP on cPanel](/en/docs/api/post-service-cpanel2-id-unblock) | `services:write` |
| `GET` | `/v1/services/@cid` | [Services by category](/en/docs/api/get-services-cid) | `services:read` |
| `GET` | `/v1/services/active` | [Active services](/en/docs/api/get-services-active) | `services:read` |
## VPS servers
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/service/@id/networks` | [Networks and IP ranges](/en/docs/api/get-service-id-networks) | `services:read` |
| `GET` | `/v1/service/@id/resources` | [VPS resources](/en/docs/api/get-service-id-resources) | `services:read` |
| `GET` | `/v1/service/@id/templates` | [OS templates](/en/docs/api/get-service-id-templates) | `services:read` |
| `GET` | `/v1/service/@id/vms` | [List virtual machines](/en/docs/api/get-service-id-vms) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid` | [Virtual machine details](/en/docs/api/get-service-id-vms-vmid) | `services:read` |
| `POST` | `/v1/service/@id/vms/@vmid/hostname` | [Change VM hostname](/en/docs/api/post-service-id-vms-vmid-hostname) | `services:write` |
| `GET` | `/v1/service/@id/vms/@vmid/interfaces` | [VM network interfaces](/en/docs/api/get-service-id-vms-vmid-interfaces) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid/interfaces/@iface` | [Network interface details](/en/docs/api/get-service-id-vms-vmid-interfaces-iface) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid/ippool` | [VM IP pools](/en/docs/api/get-service-id-vms-vmid-ippool) | `services:read` |
| `GET` | `/v1/service/@id/vms/@vmid/ips` | [VM IP addresses](/en/docs/api/get-service-id-vms-vmid-ips) | `services:read` |
| `POST` | `/v1/service/@id/vms/@vmid/reboot` | [Restart VM](/en/docs/api/post-service-id-vms-vmid-reboot) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/reset` | [Reset VM](/en/docs/api/post-service-id-vms-vmid-reset) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/shutdown` | [Shut down VM](/en/docs/api/post-service-id-vms-vmid-shutdown) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/start` | [Start VM](/en/docs/api/post-service-id-vms-vmid-start) | `services:write` |
| `POST` | `/v1/service/@id/vms/@vmid/stop` | [Stop VM](/en/docs/api/post-service-id-vms-vmid-stop) | `services:write` |
| `GET` | `/v1/service/@id/vms/@vmid/traffic` | [VM network traffic](/en/docs/api/get-service-id-vms-vmid-traffic) | `services:read` |
## SSL certificates
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/certificate` | [List SSL certificates](/en/docs/api/get-certificate) | `services:read` |
| `GET` | `/v1/certificate/@id` | [SSL certificate details](/en/docs/api/get-certificate-id) | `services:read` |
| `GET` | `/v1/certificate/@id/crt` | [Download certificate](/en/docs/api/get-certificate-id-crt) | `services:read` |
| `GET` | `/v1/certificate/order` | [Certificates available to order](/en/docs/api/get-certificate-order) | `services:read` |
| `POST` | `/v1/certificate/order` | [Order SSL certificate](/en/docs/api/post-certificate-order) | `services:write` |
| `GET` | `/v1/certificate/order/@product_id/software` | [Server software for certificates](/en/docs/api/get-certificate-order-product-id-software) | `services:read` |
| `GET` | `/v1/certificates` | [List SSL services (certificates)](/en/docs/api/get-certificates) | `services:read` |
---
# Billing
Invoices, estimates, balance, top-ups and payments.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Invoices & payments
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/addfunds` | [Top-up information](/en/docs/api/get-addfunds) | `billing:read` |
| `POST` | `/v1/addfunds` | [Top up](/en/docs/api/post-addfunds) | `billing:write` |
| `GET` | `/v1/balance` | [Account balance](/en/docs/api/get-balance) | `billing:read` |
| `GET` | `/v1/billing/@invoice_id/pay/@gateway_id` | [Invoice payment link](/en/docs/api/get-billing-invoice-id-pay-gateway-id) | `billing:write` |
| `POST` | `/v1/billing/addfunds` | [Top up (billing)](/en/docs/api/post-billing-addfunds) | `billing:write` |
| `GET` | `/v1/billing/creditlogs` | [Credit balance history](/en/docs/api/get-billing-creditlogs) | `billing:read` |
| `POST` | `/v1/billing/creditvoucher` | [Redeem top-up code](/en/docs/api/post-billing-creditvoucher) | `billing:write` |
| `GET` | `/v1/estimate/@id/pdf` | [Download estimate PDF](/en/docs/api/get-estimate-id-pdf) | `billing:read` |
| `GET` | `/v1/estimates` | [List estimates](/en/docs/api/get-estimates) | `billing:read` |
| `GET` | `/v1/estimates/@id` | [Estimate details](/en/docs/api/get-estimates-id) | `billing:read` |
| `GET` | `/v1/invoice` | [List invoices](/en/docs/api/get-invoice) | `billing:read` |
| `GET` | `/v1/invoice/@id` | [Invoice details](/en/docs/api/get-invoice-id) | `billing:read` |
| `DELETE` | `/v1/invoice/@id` | [Delete invoice](/en/docs/api/delete-invoice-id) | `billing:write` |
| `POST` | `/v1/invoice/@id/credit` | [Pay invoice with balance](/en/docs/api/post-invoice-id-credit) | `billing:write` |
| `GET` | `/v1/invoice/@id/pdf` | [Download invoice PDF](/en/docs/api/get-invoice-id-pdf) | `billing:read` |
| `GET` | `/v1/invoices` | [List invoices (paginated)](/en/docs/api/get-invoices) | `billing:read` |
| `GET` | `/v1/invoices/due` | [Due invoices](/en/docs/api/get-invoices-due) | `billing:read` |
| `POST` | `/v1/invoices/payall` | [Pay invoices together](/en/docs/api/post-invoices-payall) | `billing:write` |
| `GET` | `/v1/payment` | [Payment methods](/en/docs/api/get-payment) | `billing:read` |
| `GET` | `/v1/payment/fees` | [Payment method fees](/en/docs/api/get-payment-fees) | `billing:read` |
## Orders
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `POST` | `/v1/carts` | [Create a cart](/en/docs/api/post-carts) | `orders:write` |
| `GET` | `/v1/carts/@uuid` | [Get a cart](/en/docs/api/get-carts-uuid) | `orders:write` |
| `PUT` | `/v1/carts/@uuid` | [Update a cart](/en/docs/api/put-carts-uuid) | `orders:write` |
| `POST` | `/v1/carts/@uuid/confirm` | [Place the order from a cart](/en/docs/api/post-carts-uuid-confirm) | `orders:write` |
| `GET` | `/v1/carts/latest` | [Latest cart](/en/docs/api/get-carts-latest) | `orders:write` |
| `POST` | `/v1/order` | [Order several services](/en/docs/api/post-order) | `orders:write` |
| `POST` | `/v1/order/@product_id` | [Order one service](/en/docs/api/post-order-product-id) | `orders:write` |
| `POST` | `/v1/quote` | [Get an order quote](/en/docs/api/post-quote) | `orders:write` |
---
# TinoPay
Receive bank transfers through linked bank accounts and virtual accounts (VA); webhooks and Telegram, Zalo, Lark notifications for each transaction; API tokens.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Guides
- [Getting started with TinoPay](/en/docs/tinopay-start)
- [Receiving TinoPay webhooks](/en/docs/tinopay-webhook)
- [Telegram, Zalo and Lark notifications](/en/docs/tinopay-notifications)
- [TinoPay public API](/en/docs/tinopay-api)
- [No-code integrations](/en/docs/tinopay-integrations)
## Merchant
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay` | [Merchant details](/en/docs/api/tinopay-get-merchant) | — |
| `PATCH` | `/service/@id/tinopay` | [Update merchant details](/en/docs/api/tinopay-patch-merchant) | — |
| `GET` | `/service/@id/tinopay/pay-codes` | [Payment code patterns](/en/docs/api/tinopay-get-pay-codes) | — |
| `PUT` | `/service/@id/tinopay/pay-codes` | [Save payment code patterns](/en/docs/api/tinopay-put-pay-codes) | — |
## Bank accounts
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/bank-accounts` | [List bank accounts](/en/docs/api/tinopay-get-bank-accounts) | — |
| `GET` | `/service/@id/tinopay/bank-accounts/@bank` | [Bank account details](/en/docs/api/tinopay-get-bank-accounts-bank) | — |
| `POST` | `/service/@id/tinopay/bank-links/bidv-business` | [Request a BIDV business link](/en/docs/api/tinopay-post-bank-links-bidv-business) | — |
## Virtual accounts
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts` | [List virtual accounts (VA)](/en/docs/api/tinopay-get-bank-accounts-bank-virtual-accounts) | — |
| `POST` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts` | [Create a virtual account](/en/docs/api/tinopay-post-bank-accounts-bank-virtual-accounts) | — |
| `POST` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts/@va/activate` | [Activate a virtual account](/en/docs/api/tinopay-post-bank-accounts-bank-virtual-accounts-va-activate) | — |
| `POST` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts/@va/deactivate` | [Deactivate a virtual account](/en/docs/api/tinopay-post-bank-accounts-bank-virtual-accounts-va-deactivate) | — |
| `DELETE` | `/service/@id/tinopay/bank-accounts/@bank/virtual-accounts/@va` | [Delete a virtual account](/en/docs/api/tinopay-delete-bank-accounts-bank-virtual-accounts-va) | — |
## Webhooks
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/webhooks` | [List webhooks](/en/docs/api/tinopay-get-webhooks) | — |
| `POST` | `/service/@id/tinopay/webhooks` | [Create a webhook](/en/docs/api/tinopay-post-webhooks) | — |
| `GET` | `/service/@id/tinopay/webhooks/@webhook` | [Webhook details](/en/docs/api/tinopay-get-webhooks-webhook) | — |
| `PATCH` | `/service/@id/tinopay/webhooks/@webhook` | [Update a webhook](/en/docs/api/tinopay-patch-webhooks-webhook) | — |
| `DELETE` | `/service/@id/tinopay/webhooks/@webhook` | [Delete a webhook](/en/docs/api/tinopay-delete-webhooks-webhook) | — |
| `GET` | `/service/@id/tinopay/webhook-logs` | [Webhook logs](/en/docs/api/tinopay-get-webhook-logs) | — |
| `POST` | `/service/@id/tinopay/webhook-logs/@log/resend` | [Resend a webhook log](/en/docs/api/tinopay-post-webhook-logs-log-resend) | — |
## Transactions
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/transactions` | [Transaction history](/en/docs/api/tinopay-get-transactions) | — |
| `GET` | `/service/@id/tinopay/transactions/summary` | [Transaction summary](/en/docs/api/tinopay-get-transactions-summary) | — |
| `GET` | `/service/@id/tinopay/transactions/@transaction` | [Transaction details](/en/docs/api/tinopay-get-transactions-transaction) | — |
| `POST` | `/service/@id/tinopay/transactions/@transaction/resend-webhook` | [Resend webhooks for a transaction](/en/docs/api/tinopay-post-transactions-transaction-resend-webhook) | — |
## Notifications
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `POST` | `/service/@id/tinopay/notification-apps/bot-info` | [Check a bot token](/en/docs/api/tinopay-post-notification-apps-bot-info) | — |
| `POST` | `/service/@id/tinopay/notification-apps/find-chats` | [Find a bot's chats](/en/docs/api/tinopay-post-notification-apps-find-chats) | — |
| `POST` | `/service/@id/tinopay/notification-apps/test` | [Send a test message](/en/docs/api/tinopay-post-notification-apps-test) | — |
| `GET` | `/service/@id/tinopay/notification-apps` | [List notification apps](/en/docs/api/tinopay-get-notification-apps) | — |
| `POST` | `/service/@id/tinopay/notification-apps` | [Create a notification app](/en/docs/api/tinopay-post-notification-apps) | — |
| `GET` | `/service/@id/tinopay/notification-apps/@app` | [Notification app details](/en/docs/api/tinopay-get-notification-apps-app) | — |
| `PATCH` | `/service/@id/tinopay/notification-apps/@app` | [Update a notification app](/en/docs/api/tinopay-patch-notification-apps-app) | — |
| `DELETE` | `/service/@id/tinopay/notification-apps/@app` | [Delete a notification app](/en/docs/api/tinopay-delete-notification-apps-app) | — |
## Sapo integration
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `POST` | `/service/@id/tinopay/sapo-integrations/test` | [Test the Sapo connection](/en/docs/api/tinopay-post-sapo-integrations-test) | — |
| `GET` | `/service/@id/tinopay/sapo-integrations` | [List Sapo integrations](/en/docs/api/tinopay-get-sapo-integrations) | — |
| `POST` | `/service/@id/tinopay/sapo-integrations` | [Create a Sapo integration](/en/docs/api/tinopay-post-sapo-integrations) | — |
| `GET` | `/service/@id/tinopay/sapo-integrations/@integration` | [Get a Sapo integration](/en/docs/api/tinopay-get-sapo-integrations-integration) | — |
| `PATCH` | `/service/@id/tinopay/sapo-integrations/@integration` | [Update a Sapo integration](/en/docs/api/tinopay-patch-sapo-integrations-integration) | — |
| `DELETE` | `/service/@id/tinopay/sapo-integrations/@integration` | [Delete a Sapo integration](/en/docs/api/tinopay-delete-sapo-integrations-integration) | — |
| `GET` | `/service/@id/tinopay/sapo-order-logs` | [Sapo reconciliation log](/en/docs/api/tinopay-get-sapo-order-logs) | — |
## API tokens
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/tinopay/api-tokens` | [List API tokens](/en/docs/api/tinopay-get-api-tokens) | — |
| `POST` | `/service/@id/tinopay/api-tokens` | [Create an API token](/en/docs/api/tinopay-post-api-tokens) | — |
| `DELETE` | `/service/@id/tinopay/api-tokens/@token` | [Revoke an API token](/en/docs/api/tinopay-delete-api-tokens-token) | — |
---
# Tino Uptime
Monitor websites, servers, ports, DNS, SSL and domains; uptime, incidents, alerts by email, Telegram, Discord, Slack, webhook, Zalo.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Guides
- [Getting started with Tino Uptime](/en/docs/uptime-start)
## Uptime plan
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime` | [Tino Uptime plan overview](/en/docs/api/uptime-get-overview) | — |
| `GET` | `/service/@id/uptime/usage` | [Usage against plan limits](/en/docs/api/uptime-get-usage) | — |
## Monitors
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime/monitors` | [List monitors](/en/docs/api/uptime-get-monitors) | — |
| `POST` | `/service/@id/uptime/monitors` | [Create a monitor](/en/docs/api/uptime-post-monitors) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor` | [Monitor details](/en/docs/api/uptime-get-monitors-monitor) | — |
| `PUT` | `/service/@id/uptime/monitors/@monitor` | [Update a monitor](/en/docs/api/uptime-put-monitors-monitor) | — |
| `DELETE` | `/service/@id/uptime/monitors/@monitor` | [Delete a monitor](/en/docs/api/uptime-delete-monitors-monitor) | — |
| `POST` | `/service/@id/uptime/monitors/@monitor/pause` | [Pause a monitor](/en/docs/api/uptime-post-monitors-monitor-pause) | — |
| `POST` | `/service/@id/uptime/monitors/@monitor/resume` | [Resume a monitor](/en/docs/api/uptime-post-monitors-monitor-resume) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor/uptime` | [Monitor uptime](/en/docs/api/uptime-get-monitors-monitor-uptime) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor/results` | [Monitor check results](/en/docs/api/uptime-get-monitors-monitor-results) | — |
| `GET` | `/service/@id/uptime/monitors/@monitor/incidents` | [Monitor incidents](/en/docs/api/uptime-get-monitors-monitor-incidents) | — |
## Incidents
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime/incidents` | [List incidents](/en/docs/api/uptime-get-incidents) | — |
| `GET` | `/service/@id/uptime/incidents/@incident` | [Incident details](/en/docs/api/uptime-get-incidents-incident) | — |
| `POST` | `/service/@id/uptime/incidents/@incident/ack` | [Acknowledge an incident](/en/docs/api/uptime-post-incidents-incident-ack) | — |
## Alert channels
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/uptime/channels` | [List alert channels](/en/docs/api/uptime-get-channels) | — |
| `POST` | `/service/@id/uptime/channels` | [Create an alert channel](/en/docs/api/uptime-post-channels) | — |
| `POST` | `/service/@id/uptime/channels/@channel/verify` | [Verify an alert channel](/en/docs/api/uptime-post-channels-channel-verify) | — |
| `POST` | `/service/@id/uptime/channels/@channel/test` | [Send a test message](/en/docs/api/uptime-post-channels-channel-test) | — |
| `DELETE` | `/service/@id/uptime/channels/@channel` | [Delete an alert channel](/en/docs/api/uptime-delete-channels-channel) | — |
---
# TinoPage Builder
AI-built websites: limits of the purchased service, AI website creation, publishing, pausing, deleting, custom domains, contacts and orders, one-time sign-in.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Guides
- [Getting started with TinoPage Builder](/en/docs/builder-start)
## Builder account
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/overview` | [TinoPage Builder account overview](/en/docs/api/builder-get-overview) | — |
| `POST` | `/service/@id/builder/login` | [One-time sign-in ticket](/en/docs/api/builder-post-login) | — |
## Websites
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/sites` | [List websites](/en/docs/api/builder-get-sites) | — |
| `GET` | `/service/@id/builder/sites/@site` | [Website details](/en/docs/api/builder-get-sites-site) | — |
| `POST` | `/service/@id/builder/sites` | [Create a website with AI](/en/docs/api/builder-post-sites) | — |
| `GET` | `/service/@id/builder/site-creation` | [Website creation progress](/en/docs/api/builder-get-site-creation) | — |
| `POST` | `/service/@id/builder/sites/@site/publish` | [Publish a website](/en/docs/api/builder-post-sites-site-publish) | — |
| `POST` | `/service/@id/builder/sites/@site/unpublish` | [Pause a website](/en/docs/api/builder-post-sites-site-unpublish) | — |
| `DELETE` | `/service/@id/builder/sites/@site` | [Delete a website](/en/docs/api/builder-delete-sites-site) | — |
## Custom domains
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/sites/@site/domains` | [Custom domains of a website](/en/docs/api/builder-get-sites-site-domains) | — |
| `POST` | `/service/@id/builder/sites/@site/domains` | [Add a custom domain](/en/docs/api/builder-post-sites-site-domains) | — |
| `POST` | `/service/@id/builder/sites/@site/domains/@hostname/verify` | [Check DNS again](/en/docs/api/builder-post-sites-site-domains-hostname-verify) | — |
| `DELETE` | `/service/@id/builder/sites/@site/domains/@hostname` | [Remove a custom domain](/en/docs/api/builder-delete-sites-site-domains-hostname) | — |
## Contacts & orders
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/service/@id/builder/sites/@site/leads` | [Contacts and orders](/en/docs/api/builder-get-sites-site-leads) | — |
---
# TinoWAF
Website firewall: add websites, protection level, emergency anti-attack mode, domain names and DNS records, origin servers, event log, IP block/allow list.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## TinoWAF
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/service/@id/waf/events` | [Event summary](/en/docs/api/get-service-id-waf-events) | `waf:read` |
| `GET` | `/v1/service/@id/waf/ip-rules` | [List IP rules](/en/docs/api/get-service-id-waf-ip-rules) | `waf:read` |
| `POST` | `/v1/service/@id/waf/ip-rules` | [Block or allow an IP](/en/docs/api/post-service-id-waf-ip-rules) | `waf:write` |
| `DELETE` | `/v1/service/@id/waf/ip-rules/@rule` | [Delete an IP rule](/en/docs/api/delete-service-id-waf-ip-rules-rule) | `waf:write` |
| `POST` | `/v1/service/@id/waf/ip-rules/apply` | [Re-apply IP rules](/en/docs/api/post-service-id-waf-ip-rules-apply) | `waf:write` |
| `GET` | `/v1/service/@id/waf/overview` | [TinoWAF overview](/en/docs/api/get-service-id-waf-overview) | `waf:read` |
| `GET` | `/v1/service/@id/waf/zones` | [List websites](/en/docs/api/get-service-id-waf-zones) | `waf:read` |
| `POST` | `/v1/service/@id/waf/zones` | [Add a website](/en/docs/api/post-service-id-waf-zones) | `waf:write` |
| `GET` | `/v1/service/@id/waf/zones/@zone` | [Get a website](/en/docs/api/get-service-id-waf-zones-zone) | `waf:read` |
| `DELETE` | `/v1/service/@id/waf/zones/@zone` | [Delete a website](/en/docs/api/delete-service-id-waf-zones-zone) | `waf:write` |
| `POST` | `/v1/service/@id/waf/zones/@zone/check` | [Check DNS now](/en/docs/api/post-service-id-waf-zones-zone-check) | `waf:write` |
| `GET` | `/v1/service/@id/waf/zones/@zone/events` | [List a website's events](/en/docs/api/get-service-id-waf-zones-zone-events) | `waf:read` |
| `GET` | `/v1/service/@id/waf/zones/@zone/hostnames` | [List a website's domain names](/en/docs/api/get-service-id-waf-zones-zone-hostnames) | `waf:read` |
| `POST` | `/v1/service/@id/waf/zones/@zone/hostnames` | [Add a domain name to a website](/en/docs/api/post-service-id-waf-zones-zone-hostnames) | `waf:write` |
| `DELETE` | `/v1/service/@id/waf/zones/@zone/hostnames/@hostname` | [Remove a domain name from a website](/en/docs/api/delete-service-id-waf-zones-zone-hostnames-hostname) | `waf:write` |
| `GET` | `/v1/service/@id/waf/zones/@zone/origin` | [Get a website's origin servers](/en/docs/api/get-service-id-waf-zones-zone-origin) | `waf:read` |
| `PUT` | `/v1/service/@id/waf/zones/@zone/origin` | [Update origin servers](/en/docs/api/put-service-id-waf-zones-zone-origin) | `waf:write` |
| `PUT` | `/v1/service/@id/waf/zones/@zone/under-attack` | [Turn emergency anti-attack mode on or off](/en/docs/api/put-service-id-waf-zones-zone-under-attack) | `waf:write` |
| `PUT` | `/v1/service/@id/waf/zones/@zone/waf-mode` | [Change the protection level](/en/docs/api/put-service-id-waf-zones-zone-waf-mode) | `waf:write` |
---
# Tickets & support
Tickets, departments, knowledgebase and news.
Every route is called through the gateway `https://api.tino.vn/v1` with `Authorization: Bearer `; see [Calling the Tino API](/en/docs/api).
## Tickets & support
| Method | Path | Name | Scope |
| --- | --- | --- | --- |
| `GET` | `/v1/knowledgebase` | [Knowledge base categories](/en/docs/api/get-knowledgebase) | `tickets:read` |
| `GET` | `/v1/knowledgebase/@category_id` | [Articles in a category](/en/docs/api/get-knowledgebase-category-id) | `tickets:read` |
| `GET` | `/v1/knowledgebase/article/@article_id` | [Knowledge base article](/en/docs/api/get-knowledgebase-article-article-id) | `tickets:read` |
| `GET` | `/v1/news` | [List news](/en/docs/api/get-news) | `tickets:read` |
| `GET` | `/v1/news/@news_id` | [News details](/en/docs/api/get-news-news-id) | `tickets:read` |
| `GET` | `/v1/ticket/attachment/@file` | [Download ticket attachment](/en/docs/api/get-ticket-attachment-file) | `tickets:read` |
| `GET` | `/v1/ticket/departments` | [Support departments](/en/docs/api/get-ticket-departments) | `tickets:read` |
| `GET` | `/v1/tickets` | [List tickets](/en/docs/api/get-tickets) | `tickets:read` |
| `POST` | `/v1/tickets` | [Create ticket](/en/docs/api/post-tickets) | `tickets:write` |
| `GET` | `/v1/tickets/@number` | [Ticket details](/en/docs/api/get-tickets-number) | `tickets:read` |
| `POST` | `/v1/tickets/@number` | [Reply to ticket](/en/docs/api/post-tickets-number) | `tickets:write` |
| `PUT` | `/v1/tickets/@number/close` | [Close ticket](/en/docs/api/put-tickets-number-close) | `tickets:write` |
| `PUT` | `/v1/tickets/@number/open` | [Reopen ticket](/en/docs/api/put-tickets-number-open) | `tickets:write` |
---
# Account overview
`GET /v1/clientarea` · scope `profile:read`
Counts of invoices, domains, tickets and services of the account.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/clientarea` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
This route takes no parameters.
## Example request
```bash
curl "https://api.tino.vn/v1/clientarea" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.get_summary()`, Node.js `await client.account.getSummary();`, PHP `$client->account->getSummary();`.
## Example response
```json
{
"total": {
"invoices": 1,
"domains": 1,
"tickets": 3,
"services": 3
}
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Service categories
`GET /v1/clientarea/nav/service` · scope `profile:read`
Categories and subcategories of service types, used to build the services menu.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/clientarea/nav/service` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
This route takes no parameters.
## Example request
```bash
curl "https://api.tino.vn/v1/clientarea/nav/service" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.get_service_nav()`, Node.js `await client.account.getServiceNav();`, PHP `$client->account->getServiceNav();`.
## Example response
```json
{
"categories": [
{
"id": 84,
"name": "Cloud Instance",
"description": "",
"slug": "cloud-instance",
"subcategories": [
{
"id": 93,
"name": "Hermes Agent",
"description": "",
"slug": "hermes-agent"
},
{
"id": 70,
"name": "NVMe VPS ",
"description": "",
"slug": "nvme-vps"
},
"… 3 more item(s)"
]
},
{
"id": 54,
"name": "Web Hosting",
"description": "",
"slug": "web-hosting",
"subcategories": [
{
"id": 74,
"name": "Hosting NVMe",
"description": "Trải nghiệm tốc độ siêu nhanh với ổ cứng NVMe Enterprise chuẩn U.2 với giá tốt nhất. Phần cứng máy chủ cao cấp và mạnh mẽ với vi xử lý Intel Xeon Scalable Gen 2, máy chủ Dell chuyên dụng cho mọi nhà.
",
"slug": "hosting-nvme"
},
{
"id": 53,
"name": "Business Hosting",
"description": "\nTrải nghiệm tốc độ siêu nhanh với ổ cứng NVMe Enterprise chuẩn U.2 với giá tốt nhất. Phần cứng máy chủ cao cấp và mạnh mẽ với vi xử lý Intel Xeon Scalable Gen 2, máy chủ Dell chuyên dụng dành cho doanh nghiệp.
\n",
"slug": "business-hosting"
},
"… 4 more item(s)"
]
},
"… 7 more item(s)"
… 2 more line(s)
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Search services and products
`POST /v1/clientarea/search/@query` · scope `profile:read`
Search the account's services and products for the string `@query` (for example a domain name). Read-only, even though it is `POST`.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/clientarea/search/@query` |
| Method | `POST` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `query` | path | string | yes | Text to search for |
## Example request
```bash
curl -X POST "https://api.tino.vn/v1/clientarea/search/example.com" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): no dedicated function yet, use `client.request("POST", "/clientarea/search/@query")`.
## Example response
No live sample: the route changes real data or the test account has no data for it. The answer is a JSON object; errors follow the common shape in [Calling the Tino API](/en/docs/api#errors).
---
# List statuses
`GET /v1/clientarea/statuses` · scope `profile:read`
Status values of invoices, domains, tickets, services and estimates.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/clientarea/statuses` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
This route takes no parameters.
## Example request
```bash
curl "https://api.tino.vn/v1/clientarea/statuses" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.list_statuses()`, Node.js `await client.account.listStatuses();`, PHP `$client->account->listStatuses();`.
## Example response
```json
{
"invoice": [
"Paid",
"Unpaid",
"… 4 more item(s)"
],
"domain": [
"Pending",
"Active",
"… 4 more item(s)"
],
"ticket": [
"Open",
"Closed",
"… 4 more item(s)"
],
"service": [
"Pending",
"Active",
"… 4 more item(s)"
],
"estimates": [
"Draft",
"Sent",
"… 3 more item(s)"
]
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# List contacts
`GET /v1/contact` · scope `profile:read`
The account's contacts (sub-accounts).
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/contact` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | Zero-based page index. page=0 is the first page; the response echoes it 1-based in page.current. Default: `0`. |
| `perpage` | query | integer | no | Rows per page. Defaults to the account's RecordsPerPage setting. |
| `orderby` | query | string | no | Sort expression in the form `field\|DESC` or `field\|ASC`, e.g. `date\|DESC`. |
| `filter` | query | object | no | Endpoint-specific filters, serialised as filter[key]=value. |
## Example request
```bash
curl "https://api.tino.vn/v1/contact" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.list_contacts()`, Node.js `await client.account.listContacts();`, PHP `$client->account->listContacts();`.
## Example response
```json
{
"contacts": null,
"page": {
"current": 0,
"total": 0
}
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Contact details
`GET /v1/contact/@id` · scope `profile:read`
Details and permissions of a contact.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/contact/@id` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Contact ID |
## Example request
```bash
curl "https://api.tino.vn/v1/contact/12" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.get_contact(id)`, Node.js `await client.account.getContact(id);`, PHP `$client->account->getContact($id);`.
## Example response
```json
{
"privileges": {
"billing": [
"emails",
"newinvoices",
"… 11 more item(s)"
],
"support": [
"newticket",
"tickets",
"… 3 more item(s)"
],
"disabled_support_departments": [
2,
3,
"… 5 more item(s)"
],
"misc": [
"editmain",
"manageservicegroups",
"… 9 more item(s)"
],
"services": {
"full": 1,
"100001": [
"basic",
"billing",
"… 18 more item(s)"
],
"100002": [
"basic",
"billing",
"… 23 more item(s)"
],
"100003": [
"basic",
"billing",
"… 5 more item(s)"
]
},
"domains": {
"full": 1,
"200001": [
"basic",
"renew",
… 25 more line(s)
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Permissions available for contacts
`GET /v1/contact/privileges` · scope `profile:read`
Permissions that can be granted to contacts. Each domain and service may have additional permissions, depending on its features.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/contact/privileges` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
This route takes no parameters.
## Example request
```bash
curl "https://api.tino.vn/v1/contact/privileges" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.get_contact_privileges()`, Node.js `await client.account.getContactPrivileges();`, PHP `$client->account->getContactPrivileges();`.
## Example response
```json
{
"privileges": {
"billing": [
"emails",
"newinvoices",
"… 11 more item(s)"
],
"support": [
"newticket",
"tickets",
"… 3 more item(s)"
],
"disabled_support_departments": [
2,
3,
"… 5 more item(s)"
],
"misc": [
"editmain",
"manageservicegroups",
"… 9 more item(s)"
],
"services": {
"full": 1,
"100001": [
"basic",
"billing",
"… 18 more item(s)"
],
"100002": [
"basic",
"billing",
"… 23 more item(s)"
],
"100003": [
"basic",
"billing",
"… 5 more item(s)"
]
},
"domains": {
"full": 1,
"200001": [
"basic",
"renew",
… 25 more line(s)
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# List contacts (contacts)
`GET /v1/contacts` · scope `profile:read`
The account's contacts (sub-accounts).
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/contacts` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | Zero-based page index. page=0 is the first page; the response echoes it 1-based in page.current. Default: `0`. |
| `perpage` | query | integer | no | Rows per page. Defaults to the account's RecordsPerPage setting. |
| `orderby` | query | string | no | Sort expression in the form `field\|DESC` or `field\|ASC`, e.g. `date\|DESC`. |
| `filter` | query | object | no | Endpoint-specific filters, serialised as filter[key]=value. |
## Example request
```bash
curl "https://api.tino.vn/v1/contacts" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.list_contacts_legacy()`, Node.js `await client.account.listContactsLegacy();`, PHP `$client->account->listContactsLegacy();`.
## Example response
```json
{
"contacts": null,
"page": {
"current": 0,
"total": 0
}
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Account details
`GET /v1/details` · scope `profile:read`
Registration details of the account.
`verify.phone` (bool) and the `phone` block `{status, verified_at}` show the phone number status; `status` is `verified`, `unverified`, `taken_by_other` or `missing`. `update_required` at the root refers to the profile (ID document number, address), not the phone number.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/details` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
This route takes no parameters.
## Example request
```bash
curl "https://api.tino.vn/v1/details" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.get_details()`, Node.js `await client.account.getDetails();`, PHP `$client->account->getDetails();`.
## Example response
```json
{
"client": {
"id": "700001",
"group_id": "0",
"email": "user@example.com",
"lastlogin": "2026-08-02 10:00:00",
"ip": "203.0.113.10",
"host": "host.example",
"firstname": "Nguyen Van",
"lastname": "A",
"companyname": "Example Company Ltd",
"address1": "123 Example Street",
"city": "Phường Bến Thành",
"state": "Thành phố Hồ Chí Minh",
"country": "VN",
"phonenumber": "+84.900000000",
"language": "vietnamese",
"company": false,
"currency_id": "0",
"taxid": "0100000000",
"nationalid": "000000000000",
"birthday": "01/01/1990",
"…": "3 more key(s)"
},
"update_required": true,
"currency": {
"code": "đ",
"iso": "VND",
"sign": "",
"format": "1,234.56",
"decimal": "0",
"id": 0
},
"verify": {
"email": "user@example.com"
}
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# List notifications
`GET /v1/notifications` · scope `profile:read`
All notifications on the account's control panel.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/notifications` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `rel_type` | query | string | no | Optional, return only by relation type |
| `rel_id` | query | string | no | Optional, return only by relation id |
| `page` | query | integer | no | Zero-based page index. page=0 is the first page; the response echoes it 1-based in page.current. Default: `0`. |
| `perpage` | query | integer | no | Rows per page. Defaults to the account's RecordsPerPage setting. |
| `orderby` | query | string | no | Sort expression in the form `field\|DESC` or `field\|ASC`, e.g. `date\|DESC`. |
| `filter` | query | object | no | Endpoint-specific filters, serialised as filter[key]=value. |
## Example request
```bash
curl "https://api.tino.vn/v1/notifications" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.list_notifications()`, Node.js `await client.account.listNotifications();`, PHP `$client->account->listNotifications();`.
## Example response
```json
{
"notifications": [
{
"id": 920001,
"client_id": "",
"rel_type": "account",
"rel_id": "100001",
"seen": "0",
"subject": "Thông tin tài khoản Hosting tại TinoGroup.",
"body": "Dear Nguyen Van A,
\n
\nThông tin tài khoản hosting của bạn đã được khởi tạo.
\n
\nThông tin dịch vụ
\n
\nDịch vụ: WH 1
\nTên miền: example.com
\nThanh toán lần đầu: 79,000 đ
\nThanh toán gia hạn: 79,000 đ
",
"rel_type": "ticket",
"rel_id": "445776",
"seen": "0",
"subject": "Nhân viên hỗ trợ đã phản hồi ticket",
"body": "On 24 Jul 2026, at 20:18, Nguyen Van A <user@example.com> wrote: Nơi tiếp nhận những yêu cầu hỗ trợ bản khai, xác thực tên miền. Khách hàng có thể liên hệ trực tiếp qua hotline 0364.333.333 nhấn phím 2 \n\nBest Regards!\n\n/----------------…",
"metadata": "[]",
"type": "direct",
"date_added": "2026-07-24 10:00:00",
"date_seen": null,
"url": false
},
"… 12 more item(s)"
],
"page": {
"total": 14,
"pages": 1,
"current": 1
}
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Mark notification as read
`PUT /v1/notifications/@id/ack` · scope `profile:write`
Mark notification `@id` as read. Also send `seen_all=true` to mark all notifications as read.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/notifications/@id/ack` |
| Method | `PUT` |
| Scope | `profile:write` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Notification ID |
## Example request
```bash
curl -X PUT "https://api.tino.vn/v1/notifications/920001/ack" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:write`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.acknowledge_notification(id)`, Node.js `await client.account.acknowledgeNotification(id);`, PHP `$client->account->acknowledgeNotification($id);`.
## Example response
```json
{
"id": "171464",
"success": true
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Unread notifications
`GET /v1/notifications/new` · scope `profile:read`
Only new (unread) notifications.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/notifications/new` |
| Method | `GET` |
| Scope | `profile:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `rel_type` | query | string | no | Optional, return only by relation type |
| `rel_id` | query | string | no | Optional, return only by relation id |
| `page` | query | integer | no | Zero-based page index. page=0 is the first page; the response echoes it 1-based in page.current. Default: `0`. |
| `perpage` | query | integer | no | Rows per page. Defaults to the account's RecordsPerPage setting. |
| `orderby` | query | string | no | Sort expression in the form `field\|DESC` or `field\|ASC`, e.g. `date\|DESC`. |
| `filter` | query | object | no | Endpoint-specific filters, serialised as filter[key]=value. |
## Example request
```bash
curl "https://api.tino.vn/v1/notifications/new" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `profile:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.account.list_new_notifications()`, Node.js `await client.account.listNewNotifications();`, PHP `$client->account->listNewNotifications();`.
## Example response
```json
{
"notifications": [
{
"id": 920001,
"rel_type": "account",
"rel_id": "100001",
"seen": "0",
"subject": "Thông tin tài khoản Hosting tại TinoGroup.",
"body": "Dear Nguyen Van A,
\n
\nThông tin tài khoản hosting của bạn đã được khởi tạo.
\n
\nThông tin dịch vụ
\n
\nDịch vụ: WH 1
\nTên miền: example.com
\nThanh toán lần đầu: 79,000 đ
\nThanh toán gia hạn: 79,000 đ
# List services
`GET /v1/service` · scope `services:read`
All services of the account.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service` |
| Method | `GET` |
| Scope | `services:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `page` | query | integer | no | Zero-based page index. page=0 is the first page; the response echoes it 1-based in page.current. Default: `0`. |
| `perpage` | query | integer | no | Rows per page. Defaults to the account's RecordsPerPage setting. |
| `orderby` | query | string | no | Sort expression in the form `field\|DESC` or `field\|ASC`, e.g. `date\|DESC`. |
| `filter` | query | object | no | Endpoint-specific filters, serialised as filter[key]=value. |
## Example request
```bash
curl "https://api.tino.vn/v1/service" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `services:read`.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.list()`, Node.js `await client.services.list();`, PHP `$client->services->list();`.
## Example response
```json
{
"services": [
{
"id": "100001",
"domain": "example.com",
"total": "79000.00",
"status": "Active",
"billingcycle": "Monthly",
"next_due": "2026-08-26",
"expires": "2026-08-26",
"category": "Platinum Hosting",
"category_url": "platinum-hosting",
"name": "WH 1"
},
{
"id": "100002",
"domain": "vps-demo.example.net",
"total": "199000.00",
"status": "Active",
"billingcycle": "Monthly",
"next_due": "2026-08-26",
"expires": "2026-08-26",
"category": "NVMe VPS ",
"category_url": "nvme-vps",
"name": "VPS 20 GB NVMe"
},
"… 9 more item(s)"
]
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Service details
`GET /v1/service/@id` · scope `services:read`
Details of service `@id`: plan, billing cycle, expiry, status, service login details.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id` |
| Method | `GET` |
| Scope | `services:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Service ID |
## Example request
```bash
curl "https://api.tino.vn/v1/service/100001" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `services:read`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.get(id)`, Node.js `await client.services.get(id);`, PHP `$client->services->get($id);`.
## Example response
```json
{
"service": {
"id": "100001",
"product_id": "2122",
"date_created": "2026-07-26",
"domain": "example.com",
"firstpayment": "79000.00",
"total": "79000.00",
"billingcycle": "Monthly",
"next_due": "2026-08-26",
"due_day": "26",
"expires": "2026-08-26",
"autorenew": "1",
"next_invoice": "2026-08-11",
"status": "Active",
"label": "",
"username": "exampleuser",
"password": "",
"rootpassword": null,
"commitment_date": null,
"date_suspended": "0000-00-00",
"date_terminated": "0000-00-00",
"…": "9 more key(s)"
},
"addons": [],
"haveaddons": [],
"unpaid_invoice": []
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Request service cancellation
`POST /v1/service/@id/cancel` · scope `services:write`
Send a request to cancel the service.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id/cancel` |
| Method | `POST` |
| Scope | `services:write` |
| Header | `Authorization: Bearer ` |
| Content-Type | `application/json` |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Service ID |
| `immediate` | body | string | no | set to false to terminate service at the end of billing date, true - terminate immediately |
| `reason` | body | string | no | Reason for this request |
## Example request
```bash
curl -X POST "https://api.tino.vn/v1/service/100001/cancel" \
-H "Authorization: Bearer $TINO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"immediate": "",
"reason": ""
}'
```
`$TINO_TOKEN` is a token with the scope `services:write`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.cancel(id, id=...)`, Node.js `await client.services.cancel(id, { id: ... });`, PHP `$client->services->cancel($id, ['id' => ...]);`.
## Example response
```json
{
"info": [
"cancell_sent"
]
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Billing cycles
`GET /v1/service/@id/cycle` · scope `services:read`
The current billing cycle and the cycles it can be changed to.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id/cycle` |
| Method | `GET` |
| Scope | `services:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Service ID |
## Example request
```bash
curl "https://api.tino.vn/v1/service/100001/cycle" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `services:read`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.get_billing_cycles(id)`, Node.js `await client.services.getBillingCycles(id);`, PHP `$client->services->getBillingCycles($id);`.
## Example response
```json
{
"current": "m",
"cycles": []
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Change billing cycle
`POST /v1/service/@id/cycle` · scope `services:write`
Change the service's recurring billing cycle.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id/cycle` |
| Method | `POST` |
| Scope | `services:write` |
| Header | `Authorization: Bearer ` |
| Content-Type | `application/json` |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Service ID |
| `cycle` | body | string | no | New billing cycle |
## Example request
```bash
curl -X POST "https://api.tino.vn/v1/service/100001/cycle" \
-H "Authorization: Bearer $TINO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"cycle": ""
}'
```
`$TINO_TOKEN` is a token with the scope `services:write`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.set_billing_cycle(id, id=...)`, Node.js `await client.services.setBillingCycle(id, { id: ... });`, PHP `$client->services->setBillingCycle($id, ['id' => ...]);`.
## Example response
No live sample: the route changes real data or the test account has no data for it. The answer is a JSON object; errors follow the common shape in [Calling the Tino API](/en/docs/api#errors).
---
# Hosting email accounts
`GET /v1/service/@id/emails` · scope `services:read`
Email accounts on the service's cPanel hosting (`listentrys`).
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id/emails` |
| Method | `GET` |
| Scope | `services:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Service ID |
| `Name` | undefined | Type | no | Description |
| `---` | undefined | --- | no | --- |
| `module` | query | string | yes | Service type module (Cpanel2\|DirectAdmin2\|Zimbra\|Wilduck) |
## Example request
```bash
curl "https://api.tino.vn/v1/service/100001/emails" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `services:read`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.list_email_accounts(id)`, Node.js `await client.services.listEmailAccounts(id);`, PHP `$client->services->listEmailAccounts($id);`.
## Example response
```json
{
"success": true,
"listentrys": null
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Service invoices
`GET /v1/service/@id/invoices` · scope `services:read`
Invoices related to the service (`rel_invoices`).
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id/invoices` |
| Method | `GET` |
| Scope | `services:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Service ID |
## Example request
```bash
curl "https://api.tino.vn/v1/service/100001/invoices" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `services:read`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.list_invoices(id)`, Node.js `await client.services.listInvoices(id);`, PHP `$client->services->listInvoices($id);`.
## Example response
```json
{
"success": true,
"rel_invoices": null
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Get service label
`GET /v1/service/@id/label` · scope `services:read`
The service's current custom label.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id/label` |
| Method | `GET` |
| Scope | `services:read` |
| Header | `Authorization: Bearer ` |
| Content-Type | — (no body) |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | integer | yes | Service ID |
## Example request
```bash
curl "https://api.tino.vn/v1/service/100001/label" \
-H "Authorization: Bearer $TINO_TOKEN"
```
`$TINO_TOKEN` is a token with the scope `services:read`. Path parameters in the example are sample values.
SDK ([SDK & examples](/en/docs/sdk)): Python `client.services.get_label(id)`, Node.js `await client.services.getLabel(id);`, PHP `$client->services->getLabel($id);`.
## Example response
```json
{
"label": ""
}
```
Live sample from a test account with personal data masked; long arrays and strings are trimmed (`… N more item(s)`, `… N more line(s)`).
---
# Set service label
`POST /v1/service/@id/label` · scope `services:write`
Set a custom label to identify the service.
## HTTP request
| Item | Value |
| --- | --- |
| URL | `https://api.tino.vn/v1/service/@id/label` |
| Method | `POST` |
| Scope | `services:write` |
| Header | `Authorization: Bearer ` |
| Content-Type | `application/json` |
| Tokens that can call it | app tokens (`tinoat_`), personal API tokens (`tinopat_`) |
## Parameters
| Name | In | Type | Required | Description |
| --- | --- | --- | --- | --- |
| `id` | path | string | yes | Service ID |
| `label` | body | string | no | New label |
## Example request
```bash
curl -X POST "https://api.tino.vn/v1/service/100001/label" \
-H "Authorization: Bearer $TINO_TOKEN" \
-H "Content-Type: application/json" \
-d '{
"label": "