# 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/<service id>/tinopay/…` |
| Authentication | `Authorization: Bearer <token>`: 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/<service id>/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/<service id>/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/<id>` | `service.ptype` | `"tinopaypanel"` |

2. When opening its management, call [Merchant details](/en/docs/api/tinopay-get-merchant) (`GET /service/<id>/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 <token>`), 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.
