Trong quá trình triển khai hệ thống RAG (Retrieval-Augmented Generation) cho một nền tảng thương mại điện tử quy mô lớn tại Việt Nam, đội ngũ kỹ thuật của chúng tôi đã phải đối mặt với một vấn đề tưởng chừng đơn giản nhưng lại gây ra hàng trăm lỗi runtime: sai kiểu dữ liệu khi truyền biến giữa các module trong Dify. Bài viết này sẽ chia sẻ kinh nghiệm thực chiến và cách chúng tôi giải quyết triệt để vấn đề này.
Tại Sao Biến và Kiểu Dữ Liệu Quan Trọng Trong Dify?
Dify sử dụng hệ thống biến (variables) như là "máu" chảy qua các workflow. Mỗi khi bạn kết nối một node LLM với database, một form nhập liệu với API endpoint, hay một template với dữ liệu động — tất cả đều phụ thuộc vào việc kiểu dữ liệu có được xử lý đúng hay không.
Trong dự án thương mại điện tử của chúng tôi, việc sai kiểu dữ liệu đã gây ra:
- 3 ngày downtime không mong muốn
- 500+ tickets hỗ trợ từ khách hàng
- Chi phí xử lý khủng hoảng lên tới $2,000
Các Loại Biến Trong Dify
2.1. Biến Nguyên Thủy (Primitive Variables)
Dify hỗ trợ 4 kiểu biến nguyên thủy chính:
- Text — Chuỗi văn bản, có thể chứa unicode, emoji, JSON string
- Number — Số nguyên hoặc số thực, precision tùy cấu hình
- Boolean — True/False hoặc 1/0
- Object/Array — Cấu trúc phức tạp, nested data
2.2. Biến Hệ Thống (System Variables)
# Các biến hệ thống được Dify tự động inject
Không cần khai báo, chỉ cần sử dụng
Biến ngữ cảnh hội thoại
sys.query # Câu hỏi của user
sys.conversation_id # ID cuộc hội thoại
sys.user_id # ID người dùng
Biến thời gian
sys.timestamp # Unix timestamp (milisecond)
sys.ecs # ISO 8601 datetime string
Biến files
sys.files # Array các file đã upload
sys.file_urls # URLs của các file
Kinh Nghiệm Thực Chiến: Case Study Thương Mại Điện Tử
Chúng tôi xây dựng một chatbot tư vấn sản phẩm sử dụng HolySheep AI làm backend LLM. Hệ thống này cần xử lý:
- Tìm kiếm sản phẩm theo mô tả tự nhiên
- Tính giá với nhiều loại tiền tệ
- Kiểm tra tồn kho real-time
- Tạo đơn hàng tự động
3.1. Cấu Hình API Integration
# File: dify_config.py
Kết nối Dify với HolySheep AI qua webhook
import requests
import json
from datetime import datetime
class DifyHolySheepConnector:
"""Kết nối Dify workflow với HolySheep AI API"""
BASE_URL = "https://api.holysheep.ai/v1"
def __init__(self, api_key: str):
self.api_key = api_key
self.headers = {
"Authorization": f"Bearer {api_key}",
"Content-Type": "application/json"
}
def chat_completion(self, messages: list,
model: str = "gpt-4.1",
temperature: float = 0.7,
max_tokens: int = 2000) -> dict:
"""
Gọi HolySheep AI chat completion
Pricing 2026: GPT-4.1 $8/MTok, Claude Sonnet 4.5 $15/MTok
"""
endpoint = f"{self.BASE_URL}/chat/completions"
payload = {
"model": model,
"messages": messages,
"temperature": temperature,
"max_tokens": max_tokens
}
response = requests.post(
endpoint,
headers=self.headers,
json=payload,
timeout=30
)
if response.status_code == 200:
return response.json()
else:
raise Exception(f"API Error: {response.status_code} - {response.text}")
def product_search_with_context(self, query: str,
user_id: str,
conversation_history: list) -> dict:
"""
Tìm kiếm sản phẩm với context từ conversation
"""
system_prompt = """Bạn là trợ lý tư vấn sản phẩm thông minh.
Phân tích câu hỏi và trả về JSON với các trường:
- search_terms: array các từ khóa tìm kiếm
- price_min, price_max: number (USD)
- category: string hoặc null
- attributes: object các thuộc tính mong muốn
"""
messages = [
{"role": "system", "content": system_prompt},
{"role": "user", "content": f"User ID: {user_id}\nQuery: {query}"}
]
# Thêm context nếu có
if conversation_history:
context = "\n".join([
f"Q: {h['query']} | A: {h['response'][:100]}"
for h in conversation_history[-3:]
])
messages.insert(1, {
"role": "assistant",
"content": f"Context:\n{context}"
})
return self.chat_completion(messages, model="gpt-4.1")
Sử dụng
connector = DifyHolySheepConnector("YOUR_HOLYSHEEP_API_KEY")
result = connector.product_search_with_context(
query="tai nghe chống ồn giá dưới 200 đô",
user_id="user_12345",
conversation_history=[]
)
print(result)
3.2. Data Conversion Module
# File: data_converter.py
Xử lý chuyển đổi kiểu dữ liệu an toàn
from typing import Any, Union, List, Dict, Optional
from datetime import datetime
import json
class DataTypeConverter:
"""Converter chuyên dụng cho Dify workflow"""
@staticmethod
def safe_to_string(value: Any) -> str:
"""Chuyển mọi kiểu thành string an toàn"""
if value is None:
return ""
if isinstance(value, (int, float)):
return str(value)
if isinstance(value, bool):
return "true" if value else "false"
if isinstance(value, (list, dict)):
return json.dumps(value, ensure_ascii=False, default=str)
return str(value)
@staticmethod
def safe_to_number(value: Any, default: float = 0.0) -> float:
"""Chuyển thành number, handle edge cases"""
if value is None:
return default
# Xử lý string có format tiền tệ
if isinstance(value, str):
# Loại bỏ ký hiệu tiền tệ
cleaned = value.replace("$", "").replace("¥", "")
cleaned = cleaned.replace(",", "").replace("VND", "").strip()
try:
return float(cleaned)
except ValueError:
return default
try:
return float(value)
except (ValueError, TypeError):
return default
@staticmethod
def safe_to_boolean(value: Any) -> bool:
"""Chuyển thành boolean"""
if isinstance(value, bool):
return value
if isinstance(value, str):
return value.lower() in ("true", "1", "yes", "on")
return bool(value)
@staticmethod
def parse_json_safe(value: str, default: Any = None) -> Any:
"""Parse JSON với error handling"""
if isinstance(value, (dict, list)):
return value
try:
return json.loads(value)
except (json.JSONDecodeError, TypeError):
return default
@staticmethod
def normalize_product_data(raw_data: Dict) -> Dict:
"""
Normalize dữ liệu sản phẩm từ nhiều nguồn
Đây là function cốt lõi giúp đội ngũ của tôi giảm 90% lỗi
"""
return {
# String fields - luôn convert về string
"id": DataTypeConverter.safe_to_string(raw_data.get("id")),
"name": DataTypeConverter.safe_to_string(raw_data.get("name")),
"sku": DataTypeConverter.safe_to_string(raw_data.get("sku")),
# Number fields - handle currency conversion
# HolySheep pricing: ¥1 = $1
"price_usd": DataTypeConverter.safe_to_number(
raw_data.get("price_usd") or raw_data.get("price_cny")
),
"price_vnd": DataTypeConverter.safe_to_number(
raw_data.get("price_vnd")
),
"stock": int(DataTypeConverter.safe_to_number(
raw_data.get("stock"), 0
)),
# Boolean fields
"in_stock": DataTypeConverter.safe_to_boolean(
raw_data.get("in_stock") or raw_data.get("available")
),
"on_sale": DataTypeConverter.safe_to_boolean(
raw_data.get("on_sale")
),
# Array fields - ensure always array
"images": raw_data.get("images") if isinstance(
raw_data.get("images"), list
) else [],
"tags": raw_data.get("tags") if isinstance(
raw_data.get("tags"), list
) else [],
# Object - parse JSON nếu cần
"attributes": DataTypeConverter.parse_json_safe(
raw_data.get("attributes"), {}
),
# Datetime - normalize về ISO format
"created_at": DataTypeConverter.to_iso_datetime(
raw_data.get("created_at")
),
}
@staticmethod
def to_iso_datetime(value: Any) -> str:
"""Chuyển mọi datetime format về ISO 8601"""
if value is None:
return datetime.now().isoformat()
if isinstance(value, datetime):
return value.isoformat()
if isinstance(value, (int, float)):
# Assume timestamp in seconds or milliseconds
try:
ts = float(value)
if ts > 1e12: # milliseconds
ts = ts / 1000
return datetime.fromtimestamp(ts).isoformat()
except:
return datetime.now().isoformat()
return str(value)
Test converter với dữ liệu thực tế
test_data = {
"id": 12345,
"name": "Tai nghe Sony WH-1000XM5",
"price_usd": "299.99",
"price_cny": "¥1999",
"stock": "150",
"in_stock": "true",
"attributes": '{"color": "black", "bluetooth": "5.2"}',
"created_at": 1735689600 # Unix timestamp
}
normalized = DataTypeConverter.normalize_product_data(test_data)
print(json.dumps(normalized, indent=2, ensure_ascii=False))
Data Transformation Trong Dify Workflow
4.1. Template Variable Transformation
# Ví dụ: Template Dify với transform filter
File: dify_template_vars.yaml
variables:
# Input từ user (string)
user_query:
type: text
required: true
# Số tiền user nhập (string từ form)
user_budget:
type: text
required: false
# Chuyển đổi trong template
- name: budget_number
source: user_budget
transform: |
{% raw %}
{{ user_budget | replace('$', '') | replace(',', '') | float | round(2) }}
{% endraw %}
# Boolean từ checkbox
- name: need_warranty
type: boolean
source: warranty_checkbox
transform: |
{% raw %}
{{ warranty_checkbox in ['true', '1', 'yes', true] }}
{% endraw %}
# Array join
- name: product_tags_string
type: text
source: product_tags
transform: |
{% raw %}
{{ product_tags | join(', ') }}
{% endraw %}
Prompt với transformed variables
system_prompt: |
Bạn là trợ lý mua sắm thông minh.
Ngân sách của khách: ${{ budget_number }}
Yêu cầu bảo hành: {% raw %}{% if need_warranty %}Có{% else %}Không{% endif %}{% endraw %}
Tags sản phẩm: {% raw %}{{ product_tags_string }}{% endraw %}
Hãy đề xuất sản phẩm phù hợp.
4.2. Code Node Transformation
# Dify Code Node - JavaScript transformation
Node này xử lý data từ multiple sources
def transform_order_data(event, context):
"""
Transform dữ liệu đơn hàng từ nhiều nguồn
Input: {order_raw, customer_data, inventory_status}
Output: {order_final}
"""
order_raw = event.get("order_raw", {})
customer = event.get("customer_data", {})
inventory = event.get("inventory_status", {})
# Parse order items - luôn đảm bảo array
items = order_raw.get("items", [])
if isinstance(items, str):
try:
items = json.loads(items)
except:
items = []
# Transform từng item
transformed_items = []
total_amount = 0
for item in items:
quantity = int(float(item.get("quantity", 1)))
unit_price = float(item.get("price", 0))
# Check inventory
sku = str(item.get("sku", ""))
in_stock = inventory.get(sku, {}).get("available", False)
stock_qty = int(float(inventory.get(sku, {}).get("qty", 0)))
# Calculate with tax (VAT 10%)
subtotal = quantity * unit_price
tax = subtotal * 0.10
total = subtotal + tax
transformed_items.append({
"sku": sku,
"name": str(item.get("name", "")),
"quantity": quantity,
"unit_price": unit_price,
"subtotal": round(subtotal, 2),
"tax": round(tax, 2),
"total": round(total, 2),
"in_stock": bool(in_stock),
"available_qty": stock_qty,
"can_fulfill": stock_qty >= quantity
})
total_amount += total
# Build final order
order_final = {
"order_id": str(order_raw.get("id", "")),
"customer_id": str(customer.get("id", "")),
"customer_name": str(customer.get("name", "")),
"customer_phone": str(customer.get("phone", "")),
"items": transformed_items,
"item_count": len(transformed_items),
"subtotal": round(total_amount / 1.10, 2),
"tax": round(total_amount - (total_amount / 1.10), 2),
"total_usd": round(total_amount, 2),
# Convert to VND: $1 = ¥1, 1¥ ≈ 24,500 VND (2026)
"total_vnd": round(total_amount * 24500),
"currency": "USD",
"can_process": all(item["can_fulfill"] for item in transformed_items),
"created_at": datetime.now().isoformat(),
"notes": str(order_raw.get("notes", ""))
}
return order_final
Python equivalent cho Dify Python Node
def python_transform_order(order_raw, customer_data, inventory_status):
"""Python version của transformation trên"""
items = order_raw.get("items", [])
if isinstance(items, str):
import json
items = json.loads(items)
transformed_items = []
total = 0
for item in items:
qty = int(float(item.get("quantity", 1)))
price = float(item.get("price", 0))
subtotal = qty * price
tax = subtotal * 0.10
transformed_items.append({
"sku": str(item.get("sku", "")),
"quantity": qty,
"unit_price": price,
"subtotal": round(subtotal, 2),
"tax": round(tax, 2),
"total": round(subtotal + tax, 2),
"in_stock": bool(inventory_status.get(item.get("sku", {}), {}).get("available"))
})
total += subtotal + tax
return {
"order_id": str(order_raw.get("id")),
"customer_id": str(customer_data.get("id")),
"items": transformed_items,
"total_usd": round(total, 2),
"total_vnd": round(total * 24500),
"can_process": all(i["in_stock"] for i in transformed_items)
}
Lỗi Thường Gặp và Cách Khắc Phục
5.1. Lỗi "Cannot read property 'xxx' of undefined"
Nguyên nhân: Biến chưa được khởi tạo hoặc data source trả về null.
# ❌ SAI - Gây lỗi khi items là null/undefined
total = order.items.reduce((sum, item) => sum + item.price, 0)
✅ ĐÚNG - Safe navigation với fallback
total = (order.items || []).reduce((sum, item) => sum + (item?.price || 0), 0)
Python version
❌ SAI
total = sum(item['price'] for item in order['items'])
✅ ĐÚNG
items = order.get('items') or []
total = sum(item.get('price', 0) for item in items)
5.2. Lỗi "TypeError: string is not a function"
Nguyên nhân: Gọi method trên string thay vì array/object.
# ❌ SAI - Gọi .map() trên string
product_ids = request.params.ids # "123,456,789" (string)
product_list = product_ids.map(id => parseInt(id)) # Lỗi!
✅ ĐÚNG - Parse string thành array trước
product_ids = request.params.ids
if (typeof product_ids === 'string') {
product_ids = product_ids.split(',').map(s => s.trim())
}
product_list = product_ids.map(id => parseInt(id))
Dify Jinja2 template
{% raw %}
❌ SAI
{% for id in product_ids %}
{{ id }}
{% endfor %}
✅ ĐÚNG
{% set ids_array = product_ids.split(',') if product_ids is string else product_ids %}
{% for id in ids_array %}
{{ id }}
{% endfor %}
{% endraw %}
5.3. Lỗi "Invalid datetime format"
Nguyên nhân: Dify và API endpoint expect các datetime format khác nhau.
# ❌ SAI - ISO string không parse được ở một số endpoint
created_at = "2025-01-15T10:30:00+07:00" # ISO 8601 với timezone
Backend expect: "2025-01-15 10:30:00" hoặc Unix timestamp
✅ ĐÚNG - Chuyển đổi linh hoạt theo target
from datetime import datetime, timezone
def normalize_datetime(value, target_format="iso"):
"""Normalize datetime về format phù hợp"""
if value is None:
return None
# Parse various formats
formats = [
"%Y-%m-%dT%H:%M:%S%z", # ISO with timezone
"%Y-%m-%dT%H:%M:%S", # ISO without timezone
"%Y-%m-%d %H:%M:%S", # SQL datetime
"%d/%m/%Y %H:%M:%S", # Vietnamese format
"%d-%m-%Y", # Simple date
]
dt = None
for fmt in formats:
try:
dt = datetime.strptime(str(value), fmt)
break
except ValueError:
continue
# Handle Unix timestamp
if dt is None:
try:
ts = float(value)
if ts > 1e12:
ts = ts / 1000
dt = datetime.fromtimestamp(ts)
except:
return None
# Add timezone if missing
if dt.tzinfo is None:
dt = dt.replace(tzinfo=timezone.utc)
# Return in requested format
if target_format == "iso":
return dt.isoformat()
elif target_format == "sql":
return dt.strftime("%Y-%m-%d %H:%M:%S")
elif target_format == "timestamp":
return int(dt.timestamp() * 1000)
elif target_format == "date":
return dt.strftime("%Y-%m-%d")
return str(dt)
Test với nhiều format
test_values = [
"2025-01-15T10:30:00+07:00", # ISO with tz
"2025-01-15 10:30:00", # SQL
"15/01/2025 10:30:00", # Vietnamese
"1736918400", # Unix timestamp (s)
"1736918400000", # Unix timestamp (ms)
]
for val in test_values:
print(f"{val} -> ISO: {normalize_datetime(val, 'iso')}")
5.4. Lỗi "Number precision lost"
Nguyên nhân: Float to Int conversion mất precision hoặc overflow.
# ❌ SAI - Mất precision với số lớn
price = float("1999.99")
final_price = int(price * 100) # 199998 thay vì 199999
✅ ĐÚNG - Sử dụng Decimal cho financial calculations
from decimal import Decimal, ROUND_HALF_UP
def safe_currency_calc(amount_str, tax_rate=0.1):
"""
Tính tiền an toàn, tránh floating point errors
"""
# Parse string thành Decimal
amount = Decimal(str(amount_str))
# Tính tax với precision cố định
tax = (amount * Decimal(str(tax_rate))).quantize(
Decimal('0.01'),
rounding=ROUND_HALF_UP
)
total = (amount + tax).quantize(
Decimal('0.01'),
rounding=ROUND_HALF_UP
)
return {
"amount": float(amount),
"tax": float(tax),
"total": float(total),
"amount_cents": int(amount * 100),
"total_cents": int(total * 100)
}
Test
result = safe_currency_calc("1999.99", 0.10)
print(f"Amount: ${result['amount']}")
print(f"Tax: ${result['tax']}")
print(f"Total: ${result['total']}")
print(f"Total in cents: {result['total_cents']}")
JavaScript equivalent
function safeCurrencyCalc(amount, taxRate = 0.10) {
// Sử dụng integer math cho precision
const cents = Math.round(parseFloat(amount) * 100);
const taxCents = Math.round(cents * taxRate);
const totalCents = cents + taxCents;
return {
amount: cents / 100,
tax: taxCents / 100,
total: totalCents / 100,
amountCents: cents,
totalCents: totalCents
};
}
Best Practices Tổng Hợp
6.1. Defensive Programming Pattern
# File: validators.py
Validation layer cho tất cả data đi vào Dify workflow
from typing import Any, Callable, TypeVar, Optional
from dataclasses import dataclass
from decimal import Decimal
T = TypeVar('T')
@dataclass
class ValidationResult:
success: bool
value: Any = None
error: str = None
def safe_transform(
transform_fn: Callable[[Any], T],
default: T,
*args,
**kwargs
) -> T:
"""
Wrap any transformation với error handling
"""
try:
result = transform_fn(*args, **kwargs)
return result
except Exception as e:
print(f"Transform error: {e}")
return default
def validate_product_id(value: Any) -> ValidationResult:
"""Validate và normalize product ID"""
if value is None:
return ValidationResult(False, error="Product ID is required")
# Convert to string
str_id = str(value).strip()
# Check empty
if not str_id:
return ValidationResult(False, error="Product ID cannot be empty")
# Check numeric
try:
int_id = int(str_id)
if int_id <= 0:
return ValidationResult(False, error="Product ID must be positive")
return ValidationResult(True, value=int_id)
except ValueError:
return ValidationResult(False, error=f"Invalid product ID: {str_id}")
def validate_price(value: Any, min_price: float = 0, max_price: float = 1_000_000) -> ValidationResult:
"""Validate và normalize giá tiền"""
if value is None:
return ValidationResult(False, error="Price is required")
try:
# Parse various formats
price_str = str(value).replace("$", "").replace("¥", "").replace(",", "")
price = float(price_str)
if price < min_price:
return ValidationResult(False, error=f"Price too low: ${price}")
if price > max_price:
return ValidationResult(False, error=f"Price too high: ${price}")
# Round to 2 decimal places
price = round(price, 2)
return ValidationResult(True, value=price)
except ValueError:
return ValidationResult(False, error=f"Invalid price format: {value}")
def validate_email(value: Any) -> ValidationResult:
"""Validate email format"""
if not value:
return ValidationResult(False, error="Email is required")
import re
pattern = r'^[a-zA-Z0-9._%+-]+@[a-zA-Z0-9.-]+\.[a-zA-Z]{2,}$'
if re.match(pattern, str(value).strip()):
return ValidationResult(True, value=str(value).strip().lower())
return ValidationResult(False, error=f"Invalid email: {value}")
Usage in Dify Code Node
def process_user_input(user_data):
"""Validate all user inputs before processing"""
errors = []
validated = {}
# Validate each field
id_result = validate_product_id(user_data.get("product_id"))
if not id_result.success:
errors.append(id_result.error)
else:
validated["product_id"] = id_result.value
price_result = validate_price(user_data.get("price"))
if not price_result.success:
errors.append(price_result.error)
else:
validated["price"] = price_result.value
email_result = validate_email(user_data.get("email"))
if not email_result.success:
errors.append(email_result.error)
else:
validated["email"] = email_result.value
if errors:
return {
"success": False,
"errors": errors,
"data": None
}
return {
"success": True,
"errors": [],
"data": validated
}
6.2. Unified Type Schema
# File: schemas.py
Unified type schema cho toàn bộ hệ thống
from dataclasses import dataclass, field, asdict
from typing import List, Optional, Dict, Any
from enum import Enum
from datetime import datetime
class ProductCategory(Enum):
ELECTRONICS = "electronics"
FASHION = "fashion"
HOME = "home"
BEAUTY = "beauty"
SPORTS = "sports"
OTHER = "other"
@dataclass
class Product:
"""Unified product schema - single source of truth"""
# Required fields
id: str
name: str
price_usd: float
# Optional with defaults
category: ProductCategory = ProductCategory.OTHER
description: str = ""
images: List[str] = field(default_factory=list)
tags: List[str] = field(default_factory=list)
# Inventory
stock_qty: int = 0
in_stock: bool = False
# Metadata
sku: str = ""
created_at: str = ""
updated_at: str = ""
# Computed fields
@property
def can_sell(self) -> bool:
return self.in_stock and self.stock_qty > 0
@property
def price_vnd(self) -> int:
"""Convert USD to VND: 1 USD = 24,500 VND (2026 rate)"""
return int(self.price_usd * 24500)
@property
def price_formatted(self) -> str:
return f"${self.price_usd:.2f}"
def to_dict(self) -> Dict[str, Any]:
"""Convert sang dict cho JSON serialization"""
data = asdict(self)
# Convert enum to string
data['category'] = self.category.value
# Convert datetime nếu có
if self.created_at:
if isinstance(self.created_at, datetime):
data['created_at'] = self.created_at.isoformat()
return data
@classmethod
def from_raw(cls, raw_data: Dict) -> 'Product':
"""Factory method tạo Product từ raw data"""
# Map các field name khác nhau
return cls(
id=str(raw_data.get('id', raw_data.get('product_id', ''))),
name=str(raw_data.get('name', raw_data.get('title', ''))),
price_usd=float(raw_data.get('price_usd', raw_data.get('price', 0))),
category=ProductCategory(
raw_data.get('category', 'other')
),
description=str(raw_data.get('description', '')),
images=raw_data.get('images') or [],
tags=raw_data.get('tags') or [],
stock_qty=int(raw_data.get('stock', raw_data.get('quantity', 0))),
in_stock=bool(raw_data.get('in_stock', raw_data.get('available', False))),
sku=str(raw_data.get('sku', '')),
created_at=str(raw_data.get('created_at', '')),
updated_at=str(raw_data.get('updated_at', ''))
)
Usage
raw_product = {
"product_id": "12345",
"title": "Sony WH-1000XM5 Headphones",
"price": 299.99,
"category": "electronics",
"stock": 50,
"available": True
}
product = Product.from_raw(raw_product)
print(f"Product: {product.name}")
print(f"Price: {product.price_formatted}")
print(f"Price VND: {product.price_vnd:,} VND")
print(f"Can sell: {product.can_sell}")
Performance và Cost Optimization
Khi làm việc với HolySheep AI, việc tối ưu data transformation không chỉ giúp code sạch hơn mà còn tiết kiệm đáng kể chi phí API:
- Cache transformed data — Tránh transform lại nhiều lần cùng một data
- Batch processing — Gửi nhiều items trong một API call thay vì nhiều calls riêng lẻ