Bỏ qua để đến Nội dung
Tiny REST API — Tài liệu sử dụng | Zotech Docs

Tiny REST API — Tài liệu sử dụng

Hướng dẫn kết nối và sử dụng REST API của module tiny_rest_api — xác thực, CRUD theo model, rate limit, và ví dụ chi tiết với cosan.order, cosan.product.

Tiny REST API
Base URL: https://{your-odoo-domain} Module: tiny_rest_api Phiên bản tài liệu: 1.0

Tài liệu này cung cấp thông tin cần thiết để làm việc với REST API của hệ thống (module tiny_rest_api). Mỗi endpoint gồm ví dụ request (cURL / JavaScript), ví dụ response, và mô tả chi tiết Headers / Params.

1. Giới thiệu

API cho phép đọc (GET), tạo mới (POST), cập nhật (PUT) và xoá (DELETE) dữ liệu trên bất kỳ model Odoo nào đã được cấu hình cho phép truy cập (qua rest.api.connect), có kiểm soát field được đọc/ghi (allowlist / blocklist) và có rate limit theo từng API key.

Ngoài các API REST theo model, hệ thống còn có API POST /api/v1/auth/login để đăng nhập bằng login/password và lấy API key (token) dùng cho các request tiếp theo, thay cho việc tạo token thủ công.

2. Xác thực (Authenticating requests)

Tất cả các endpoint dưới /api/v1/... yêu cầu xác thực bằng API key, gửi qua header:

X-API-KEY: {token}
Đặc điểmMô tả
API key không hợp lệKhông tồn tại, sai, hoặc đã hết hạn → trả về lỗi 403 với status: error
Ngôn ngữ trả vềCó thể chỉ định qua header tuỳ chọn X-LANG (áp dụng cho field dịch, ví dụ state selection)
Ngôn ngữ mặc địnhNếu không truyền X-LANG, hệ thống dùng ngôn ngữ mặc định của user gắn với token (hoặc vi_VN)
X-LANG: vi_VN
API Key

API key sẽ được Zotech cấp — khách hàng không tự tạo/đăng nhập lấy token trên hệ thống. Vui lòng liên hệ đội ngũ Zotech để được cấp API key cho môi trường test/production.

2.1. Rate limit

Nếu token được cấu hình rate_limit > 0, mỗi response GET/POST/PUT/DELETE hợp lệ sẽ kèm theo header:

X-RateLimit-Limit: 1000
X-RateLimit-Remaining: 998
X-RateLimit-Reset: 3421
Vượt rate limit

Khi vượt quá giới hạn, request sẽ bị từ chối với lỗi 403 và message Rate limit exceeded. Try again in {n} seconds.

3. Ví dụ: Đơn hàng Cosan (cosan.order)

Model cosan.order (Đơn hàng Cosan) lưu đơn hàng/vận đơn đồng bộ từ đối tác vận chuyển Cosan (địa chỉ giao, tiền COD, trạng thái giao hàng...) và liên kết với sale.order nội bộ qua odoo_order_id — xem Bảng trường cosan.order bên dưới để tra field khi truyền fields=/include=.

GET

Danh sách đơn hàng Cosan

/api/v1/cosan.order

Yêu cầu xác thực

curl --request GET \
    "https://your-domain.odoo.com/api/v1/cosan.order" \
    --header "X-API-KEY: {token}" \
    --header "Content-Type: application/json" \
    --data "{
    \"fields\": \"id,order_code,cpn_code,partner_name,phone,address,total_amount,amount_cod,delivery_state,sync_state,create_time\",
    \"domain\": [[\"sync_state\", \"=\", \"pending\"]],
    \"page\": 1,
    \"page_size\": 50
}"
const body = {
  fields: "id,order_code,cpn_code,partner_name,phone,address,total_amount,amount_cod,delivery_state,sync_state,create_time",
  domain: [["sync_state", "=", "pending"]],
  page: 1,
  page_size: 50,
};

fetch("https://your-domain.odoo.com/api/v1/cosan.order", {
  method: "GET",
  headers: { "X-API-KEY": "{token}", "Content-Type": "application/json" },
  body: JSON.stringify(body),
}).then(r => r.json());
Example response — 200
{
  "model": "cosan.order",
  "page": 1,
  "page_size": 50,
  "total_records": 34,
  "total_pages": 1,
  "has_next": false,
  "has_previous": false,
  "records": [
    {
      "id": 6021,
      "order_code": "CSN2607880123",
      "cpn_code": "CPN881029341",
      "partner_name": "Nguyễn Văn A",
      "phone": "0909123456",
      "address": "12 Nguyễn Huệ, P. Bến Nghé, Q.1, TP.HCM",
      "total_amount": 500000,
      "amount_cod": 500000,
      "delivery_state": "delivering",
      "sync_state": { "value": "pending", "label": "Chờ đồng bộ" },
      "create_time": "2026-07-28 09:10:00"
    }
  ]
}
Tham sốKiểuGợi ý hay dùng
fieldsstringid,order_code,cpn_code,partner_name,phone,address,total_amount,amount_cod,delivery_state,order_state,sync_state,create_time,date_delivered
includestringline_ids — mở rộng chi tiết sản phẩm trong đơn (model cosan.order.line) thành object đầy đủ
domainlistTheo mã đơn: [["order_code","=","CSN2607880123"]] · theo mã vận đơn: [["cpn_code","=","CPN881029341"]] · theo trạng thái đồng bộ: [["sync_state","=","pending"]] · theo ngày tạo: [["create_time",">=","2026-07-01 00:00:00"],["create_time","<=","2026-07-31 23:59:59"]]
GET

Chi tiết 1 đơn hàng Cosan

/api/v1/cosan.order/{record_id}

Yêu cầu xác thực

curl --request GET \
    "https://your-domain.odoo.com/api/v1/cosan.order/6021" \
    --header "X-API-KEY: {token}" \
    --header "Content-Type: application/json" \
    --data "{
    \"include\": \"line_ids\"
}"
fetch("https://your-domain.odoo.com/api/v1/cosan.order/6021", {
  method: "GET",
  headers: { "X-API-KEY": "{token}", "Content-Type": "application/json" },
  body: JSON.stringify({ include: "line_ids" }),
}).then(r => r.json());
Example response — 200
{
  "model": "cosan.order",
  "record": {
    "id": 6021,
    "order_code": "CSN2607880123",
    "cpn_code": "CPN881029341",
    "odoo_order_id": [201215, "D_SO00201091"],
    "partner_name": "Nguyễn Văn A",
    "phone": "0909123456",
    "address": "12 Nguyễn Huệ, P. Bến Nghé, Q.1, TP.HCM",
    "total_amount": 500000,
    "amount_cod": 500000,
    "delivery_state": "delivering",
    "order_state": "confirmed",
    "sync_state": { "value": "pending", "label": "Chờ đồng bộ" },
    "create_time": "2026-07-28 09:10:00",
    "date_delivered": false,
    "line_ids": [
      {
        "id": 15092,
        "product_id": [3391, "Áo thun nam form rộng"],
        "default_code": "26AG06DENM",
        "name": "Áo thun nam form rộng",
        "invoice_name": "Áo thun nam",
        "barcode": "8938501234567",
        "quantity": 2,
        "tax": 8,
        "uom": "Cái",
        "currency_id": [1, "VND"],
        "price": 500000,
        "total_discount": 0
      }
    ]
  }
}

Response mẫu (404):

{
    "status": "error",
    "message": "Record with ID 6021 not found"
}
POST

Tạo mới đơn hàng Cosan

/api/v1/cosan.order

Yêu cầu xác thực

curl --request POST \
    "https://your-domain.odoo.com/api/v1/cosan.order" \
    --header "X-API-KEY: {token}" \
    --header "Content-Type: application/json" \
    --data "{
    \"order_code\": \"CSN2607880126\",
    \"cpn_code\": \"861730159299\",
    \"partner_name\": \"Nguyễn Văn A\",
    \"phone\": \"0909123456\",
    \"address\": \"12 Nguyễn Huệ, P. Bến Nghé, Q.1, TP.HCM\",
    \"amount_cod\": 500000,
    \"total_amount\": 500000,
    \"order_state\": \"Đang giao\",
    \"delivery_state\": \"Đang giao\",
    \"create_time\": \"2026-08-19\",
    \"date_delivered\": \"2026-08-19\",
    \"line_ids\": [
        [0, 0, {
            \"default_code\": \"26AG06DENM\",
            \"name\": \"Áo thun nam form rộng\",
            \"invoice_name\": \"Áo thun nam\",
            \"barcode\": \"8938501234567\",
            \"quantity\": 2,
            \"tax\": 8,
            \"uom\": \"Cái\",
            \"price\": 500000,
            \"total_discount\": 0
        }]
    ]
}"
const body = {
  order_code: "CSN2607880126",
  cpn_code: "861730159299",
  partner_name: "Nguyễn Văn A",
  phone: "0909123456",
  address: "12 Nguyễn Huệ, P. Bến Nghé, Q.1, TP.HCM",
  amount_cod: 500000,
  total_amount: 500000,
  order_state: "Đang giao",
  delivery_state: "Đang giao",
  create_time: "2026-08-19",
  date_delivered: "2026-08-19",
  line_ids: [
    [0, 0, {
      default_code: "26AG06DENM",
      name: "Áo thun nam form rộng",
      invoice_name: "Áo thun nam",
      barcode: "8938501234567",
      quantity: 2,
      tax: 8,
      uom: "Cái",
      price: 500000,
      total_discount: 0,
    }],
  ],
};

fetch("https://your-domain.odoo.com/api/v1/cosan.order", {
  method: "POST",
  headers: { "X-API-KEY": "{token}", "Content-Type": "application/json" },
  body: JSON.stringify(body),
}).then(r => r.json());
Example response — 201
{
  "status": "success",
  "id": 6099,
  "display_name": "CSN2607880126"
}
Example response — 400 (thiếu field bắt buộc)
{
  "status": "error",
  "message": "Missing required field: order_code"
}

Field line_ids dùng cú pháp lệnh one2many chuẩn của Odoo — [0, 0, {...}] để thêm dòng mới; mỗi dòng thuộc model cosan.order.line với các field product_id, default_code, name, invoice_name, barcode, quantity, tax, uom, currency_id, price, total_discount. Field order_id trên cosan.order.line (many2one trỏ về cosan.order) được hệ thống tự gán, không cần truyền.

PUT

Cập nhật địa chỉ / SĐT giao hàng

/api/v1/cosan.order/{record_id}

Yêu cầu xác thực

curl --request PUT \
    "https://your-domain.odoo.com/api/v1/cosan.order/6021" \
    --header "X-API-KEY: {token}" \
    --header "Content-Type: application/json" \
    --data "{
    \"address\": \"25 Lê Lợi, P. Bến Thành, Q.1, TP.HCM\",
    \"phone\": \"0909999999\"
}"
fetch("https://your-domain.odoo.com/api/v1/cosan.order/6021", {
  method: "PUT",
  headers: { "X-API-KEY": "{token}", "Content-Type": "application/json" },
  body: JSON.stringify({
    address: "25 Lê Lợi, P. Bến Thành, Q.1, TP.HCM",
    phone: "0909999999",
  }),
}).then(r => r.json());
Example response — 200
{
  "status": "success",
  "id": 6021,
  "display_name": "CSN2607880123"
}
Example response — 403 (field bị chặn)
{
  "status": "error",
  "message": "Fields not allowed via API: total_amount, amount_cod, sync_state"
}

Các field số liệu/trạng thái (total_amount, amount_cod, delivery_state, order_state, sync_state...) do đối tác vận chuyển Cosan đồng bộ về và không nên ghi tay. Sau khi tạo đơn qua POST, API chỉ mở PUT cho các field liên hệ giao hàng (address, phone) để kịp chỉnh sửa trước khi đơn được lấy hàng.

Bảng trường cosan.order (Đơn hàng Cosan)

Tổng hợp toàn bộ field khả dụng của model cosan.order, dùng để tham chiếu khi truyền fields= / include= cho các API GET, hoặc khi build body cho PUT.

Tên trườngNhãn trườngLoại trườngĐối tượng liên quanĐã lưuLập chỉ mụcChỉ đọc
addressĐịa chỉcharKhôngKhông
amount_codCODtiền tệKhôngKhông
cpn_codeMã CPNcharKhôngKhông
create_timeNgày tạongày giờKhôngKhông
date_deliveredNgày giao hàng thành côngngàyKhôngKhông
delivery_stateTrạng thái vận chuyểncharKhôngKhông
line_idsChi tiết đơn hàngone2manycosan.order.lineKhôngKhông
odoo_order_idĐơn hàng nội bộmany2onesale.orderKhôngKhông
order_codeMã đơn hàng (Cosan)charKhông
order_stateTrạng thái đơn hàngcharKhôngKhông
partner_nameTên khách hàngcharKhôngKhông
phoneSố điện thoạicharKhôngKhông
sync_stateTrạng thái đồng bộlựa chọnKhôngKhông
total_amountTổng tiềntiền tệKhôngKhông

4. Ví dụ: Sản phẩm Cosan (cosan.product)

Model cosan.product (Sản phẩm Cosan) lưu thông tin sản phẩm đồng bộ với đối tác vận chuyển Cosan — kích thước, cân nặng dùng để tính phí vận chuyển — và liên kết với product.product nội bộ qua odoo_product_id — xem Bảng trường cosan.product bên dưới để tra field khi truyền fields=/include=.

GET

Danh sách sản phẩm Cosan

/api/v1/cosan.product

Yêu cầu xác thực

curl --request GET \
    "https://your-domain.odoo.com/api/v1/cosan.product" \
    --header "X-API-KEY: {token}" \
    --header "Content-Type: application/json" \
    --data "{
    \"fields\": \"id,product_id,default_code,name,barcode,weight,height,length,width,total_price,odoo_product_id\",
    \"domain\": [[\"default_code\", \"=\", \"26AG06DENM\"]],
    \"page\": 1,
    \"page_size\": 50
}"
const body = {
  fields: "id,product_id,default_code,name,barcode,weight,height,length,width,total_price,odoo_product_id",
  domain: [["default_code", "=", "26AG06DENM"]],
  page: 1,
  page_size: 50,
};

fetch("https://your-domain.odoo.com/api/v1/cosan.product", {
  method: "GET",
  headers: { "X-API-KEY": "{token}", "Content-Type": "application/json" },
  body: JSON.stringify(body),
}).then(r => r.json());
Example response — 200
{
  "model": "cosan.product",
  "page": 1,
  "page_size": 50,
  "total_records": 1,
  "total_pages": 1,
  "has_next": false,
  "has_previous": false,
  "records": [
    {
      "id": 9042,
      "product_id": "CSN-P-30281",
      "default_code": "26AG06DENM",
      "name": "Áo thun nam form rộng",
      "barcode": "8938501234567",
      "weight": 0.2,
      "height": 3,
      "length": 30,
      "width": 25,
      "total_price": 250000,
      "odoo_product_id": [3391, "Áo thun nam form rộng"]
    }
  ]
}
Tham sốKiểuGợi ý hay dùng
fieldsstringid,product_id,default_code,name,barcode,description,weight,height,length,width,total_price,invoice_name,odoo_product_id,sync_message
domainlistTheo SKU nội bộ: [["default_code","=","26AG06DENM"]] · theo ID sản phẩm bên Cosan: [["product_id","=","CSN-P-30281"]] · theo barcode: [["barcode","=","8938501234567"]]
GET

Chi tiết 1 sản phẩm Cosan

/api/v1/cosan.product/{record_id}

Yêu cầu xác thực

curl --request GET \
    "https://your-domain.odoo.com/api/v1/cosan.product/9042" \
    --header "X-API-KEY: {token}" \
    --header "Accept: application/json"
fetch("https://your-domain.odoo.com/api/v1/cosan.product/9042", {
  headers: { "X-API-KEY": "{token}" },
}).then(r => r.json());
Example response — 200
{
  "model": "cosan.product",
  "record": {
    "id": 9042,
    "product_id": "CSN-P-30281",
    "default_code": "26AG06DENM",
    "name": "Áo thun nam form rộng",
    "description": "Áo thun cotton, form rộng, unisex",
    "barcode": "8938501234567",
    "invoice_name": "Áo thun nam",
    "weight": 0.2,
    "height": 3,
    "length": 30,
    "width": 25,
    "total_price": 250000,
    "odoo_product_id": [3391, "Áo thun nam form rộng"],
    "sync_message": false
  }
}

Response mẫu (404):

{
    "status": "error",
    "message": "Record with ID 9042 not found"
}
POST

Tạo mới sản phẩm Cosan

/api/v1/cosan.product

Yêu cầu xác thực

curl --request POST \
    "https://your-domain.odoo.com/api/v1/cosan.product" \
    --header "X-API-KEY: {token}" \
    --header "Content-Type: application/json" \
    --data "{
    \"name\": \"Sản phẩm 3\",
    \"default_code\": \"003\",
    \"product_id\": 2,
    \"barcode\": \"0052566\",
    \"invoice_name\": \"Sản phẩm 3\",
    \"description\": \"Sản phẩm 3\",
    \"total_price\": 500000
}"
const body = {
  name: "Sản phẩm 3",
  default_code: "003",
  product_id: 2,
  barcode: "0052566",
  invoice_name: "Sản phẩm 3",
  description: "Sản phẩm 3",
  total_price: 500000,
};

fetch("https://your-domain.odoo.com/api/v1/cosan.product", {
  method: "POST",
  headers: { "X-API-KEY": "{token}", "Content-Type": "application/json" },
  body: JSON.stringify(body),
}).then(r => r.json());
Example response — 201
{
  "status": "success",
  "id": 9051,
  "display_name": "Sản phẩm 3"
}
Example response — 400 (thiếu field bắt buộc)
{
  "status": "error",
  "message": "Missing required field: default_code"
}
PUT

Cập nhật kích thước / cân nặng

/api/v1/cosan.product/{record_id}

Yêu cầu xác thực

curl --request PUT \
    "https://your-domain.odoo.com/api/v1/cosan.product/9042" \
    --header "X-API-KEY: {token}" \
    --header "Content-Type: application/json" \
    --data "{
    \"weight\": 0.25,
    \"height\": 4,
    \"length\": 32,
    \"width\": 26
}"
fetch("https://your-domain.odoo.com/api/v1/cosan.product/9042", {
  method: "PUT",
  headers: { "X-API-KEY": "{token}", "Content-Type": "application/json" },
  body: JSON.stringify({ weight: 0.25, height: 4, length: 32, width: 26 }),
}).then(r => r.json());
Example response — 200
{
  "status": "success",
  "id": 9042,
  "display_name": "Áo thun nam form rộng"
}

Kích thước/cân nặng khai báo ở đây được Cosan dùng để tính phí vận chuyển — nên cập nhật đúng số đo thực tế đóng gói (bao gồm hộp/bao bì), không phải kích thước sản phẩm thô. Field odoo_product_id dùng để đối chiếu 1-1 với product.product nội bộ; sửa liên kết này qua PUT nếu hệ thống map nhầm sản phẩm.

Bảng trường cosan.product (Sản phẩm Cosan)

Tổng hợp toàn bộ field khả dụng của model cosan.product, dùng để tham chiếu khi truyền fields= / include= cho các API GET, hoặc khi build body cho PUT.

Tên trườngNhãn trườngLoại trườngĐối tượng liên quanĐã lưuLập chỉ mụcChỉ đọc
barcodeBarcodecharKhôngKhông
default_codeMã sản phẩmcharKhông
descriptionMô tảcharKhôngKhông
heightChiều caodự trữKhôngKhông
idIDsố nguyênKhông
invoice_nameTên xuất hoá đơncharKhôngKhông
lengthChiều dàidự trữKhôngKhông
nameTên sản phẩmcharKhông
odoo_product_idSản phẩmmany2oneproduct.productKhôngKhông
product_idID sản phẩm (Cosan)charKhôngKhông
sync_messageGhi chú đồng bộvăn bảnKhôngKhông
total_priceGiádự trữKhôngKhông
weightCân nặngdự trữKhôngKhông
widthChiều rộngdự trữKhôngKhông

5. Cấu hình Webhook Cosan

Trước khi hàm action_push_webhook_cosan hoạt động, cần khai báo URL webhook tại màn hình cấu hình module Cosan.

1
Vào Cosan → Cấu hình

Mở bản ghi cấu hình (mặc định tên Default).

2
Nhập URL vào ô Cấu hình webhook

URL endpoint nhận payload trạng thái hoá đơn (nơi hệ thống ngoài lắng nghe).

3
Lưu lại

Giá trị này được đọc và truyền vào tham số webhook_url khi gọi action_push_webhook_cosan.

Chưa cấu hình

Nếu ô này trống, hàm sẽ gọi requests.post(None, ...) và raise lỗi MissingSchema. Nên validate trước khi gọi webhook.

Khi hàm chạy, hệ thống gửi POST tới webhook_url đã cấu hình ở trên, kèm payload JSON tổng hợp từ hoá đơn và sale order liên kết:

POST

Payload đẩy sang Cosan

{webhook_url} — cấu hình tại mục 5

{
  "order_code": "CSN2607880123",
  "invoice_no": "INV/2026/00845",
  "state_push_invoice": "done",
  "state_push_invoice_name": "Thành công",
  "state_publish": "4",
  "state_publish_name": "Thành công",
  "sync_invoice": {
    "success": true,
    "data": "[{\"RefID\":\"c8772cdb-b225-4b63-a4b8-064e49c51b7b\",\"InvSeries\":\"1C26TUT\",\"InvDate\":\"2026-08-24T00:00:00+07:00\",\"EInvoiceStatus\":1}]",
    "error": null,
    "error_description": null,
    "errorCode": []
  }
}
TrườngGiá trịÝ nghĩa
order_codeMã đơn hàng sàn TMĐT hoặc mã đơn do Cosan bắn sang
invoice_noSố hoá đơn điện tử do MISA trả về
state_push_invoice
field state_push_invoice
newMới — chưa đẩy hoá đơn điện tử
processingĐang thực hiện
doneThành công
errorLỗi
state_publish
field publish_status
0Chưa phát hành
3Đã phát hành

state_push_invoice_namestate_publish_name là nhãn (label) tiếng Việt tương ứng với 2 field trên, đã được resolve sẵn trong payload — bên nhận không cần tự map lại.