# 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/<service id>/builder/…` |
| Authentication | `Authorization: Bearer <token>`: 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/<service id>/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/<id>/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.<field>`). A missing required field or an invalid phone or email answers 422 `VALIDATION_FAILED` with `details` per `intake.<field>` 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.<field>`, `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 |
