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 called with an API token and no-code integrations (Google Sheets, WordPress / WooCommerce, HostBill, Sapo).
Updated: 2026-10-25. Sources: Tino's TinoPay module and the TinoPay API v1 documentation.
Setup
- Order a TinoPay service on tino.vn: each service is one merchant.
- Link a bank account. For a BIDV business account, send a link request; staff set up the service with BIDV, and the account then appears in the bank account list.
- Create 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. - Set payment codes (
pay_codes) if you want TinoPay to extract order codes from transfer descriptions. - Create a webhook to receive every transaction: see Receiving TinoPay webhooks.
- To post every transaction to a chat: see Telegram, Zalo and Lark notifications. To read transactions yourself through TinoPay's public API: create an API token.
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 |
PATCHrequires a JSON body: the API does not read form bodies (application/x-www-form-urlencoded) onPATCH. WithPATCH, 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 404RESOURCE_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 (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 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 gets one with a one-time code sign-in:
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.
- 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) | product_type | "tinopay" |
GET https://api.tino.vn/service/<id> | service.ptype | "tinopaypanel" |
- When opening its management, call Merchant details (
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 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.
{ "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, 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 (
{name}) returns the 64-charactertokenonly 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 has no token. - Not retried on network errors (each call creates a token): read the list again and revoke any extra token. Revoking takes effect immediately.
Errors
{ "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
POSTis not deduplicated for you: repeating aPOST(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.
- A webhook's
tokenis 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.