Skip to main content

Đơn hàng

Tổng quan​

Method + pathScopeDùng để
GET /ordersorders_listLiệt kê đơn để đồng bộ (keyset), hoặc tra một đơn theo orderCode
GET /orders/:idorders_detailMột đơn kèm dòng hàng và địa chỉ giao
GET /order-statusesorders_listDanh mục trạng thái đơn của cửa hàng
PUT /ordersorders_create + orders_updateTạo hoặc cập nhật tối đa 50 đơn mỗi lần
GET /orders/:id/paymentsorders_detailKỳ thanh toán và lịch sử thanh toán của một đơn
GET /paymentsorders_listLiệt kê khoản thanh toán của cửa hàng để đồng bộ (keyset), gồm cả khoản đã xoá
POST /orders/:id/paymentsorders_updateGhi một khoản thanh toán cho đơn
DELETE /orders/:id/payments/:paymentIdorders_updateXoá một khoản thanh toán
GET /payment-methodsorders_listDanh mục phương thức thanh toán

Chuẩn bị dữ liệu để tạo đơn​

CầnLấy ở
Khách của đơnGET /customers, hoặc tạo trước bằng POST /customers
variantId / sku của dòng hàngGET /products
shippingProvinceCode / shippingWardCodeGET /address/provinces, GET /address/wards
statusIdGET /order-statuses

Đơn tạo qua Open API không đi qua bước duyệt và không bắn thông báo nội bộ như đơn tạo trên giao diện; nguồn đơn ghi là api:<tên key>.


Đối tượng đơn hàng​

{
"id": "b7c2…",
"orderCode": "DH-1001",
"customerId": "1f0c6a7e-5d1b-4a8e-9a63-2c1f0f6b9d10",
"statusId": "5a21…",
"paymentStatus": "unpaid",
"subtotal": 380000,
"taxAmount": 6400,
"shippingFee": 30000,
"discountAmount": 0,
"totalAmount": 416400,
"paidAmount": 0,
"debtAmount": 416400,
"currency": "VND",
"shipping": {
"name": "Nguyễn Thị Lan",
"phone": "0901234567",
"address": "12 Lý Thường Kiệt",
"provinceCode": "01",
"province": "Thành phố Hà Nội",
"wardCode": "00070",
"ward": "Phường Hoàn Kiếm"
},
"assignedStaffId": "7a2d…",
"source": "api:ERP Kho",
"items": [
{
"id": "e1f0…", "productId": "c3a9…", "variantId": "3e8b…",
"productName": "Áo thun basic", "variantName": "Trắng / M", "sku": "AO-THUN-M",
"quantity": 2, "unitPrice": 150000, "discountAmount": 0, "vatRate": 0,
"taxAmount": 0, "totalPrice": 300000, "notes": null
}
],
"createdAt": "2026-10-08T03:00:00.000Z",
"updatedAt": "2026-10-08T03:12:45.123Z"
}

source là nguồn tạo đơn (ui hoặc api:<tên key>). Tên tỉnh / phường do hệ thống tra từ mã, không lấy từ dữ liệu gửi lên. paidAmount, debtAmount và paymentStatus (unpaid · partially_paid · paid) do hệ thống tính từ lịch sử thanh toán.

Đọc đơn​

GET /api/open/v1/orders?updatedSince=2026-10-01T00:00:00Z&updatedUntil=2026-10-08T00:00:00Z&limit=50&excludeSource=api:ERP%20Kho
GET /api/open/v1/orders?orderCode=DH-1001
GET /api/open/v1/orders/b7c2…
  • Liệt kê theo keyset, trả { "data": [đơn…], "nextCursor": …, "window": {…} } — tối đa 50 đơn / lần, cửa sổ tối đa 30 ngày. excludeSource bỏ các đơn được tạo từ nguồn đó.
  • ?orderCode= trả { "data": đơn | null }.
  • GET /orders/:id trả { "data": đơn }, hoặc 404 OPEN_API_ORDER_NOT_FOUND.

Key chỉ thấy những đơn mà người tạo key được thấy (theo phạm vi phân quyền đơn hàng).


Danh mục trạng thái​

GET /api/open/v1/order-statuses
{
"data": [
{ "id": "0d6f…", "label": "Mới", "isInitial": true, "isCompleted": false, "isCancelled": false },
{ "id": "5a21…", "label": "Đang giao", "isInitial": false, "isCompleted": false, "isCancelled": false },
{ "id": "9c40…", "label": "Hoàn thành", "isInitial": false, "isCompleted": true, "isCancelled": false },
{ "id": "e117…", "label": "Đã huỷ", "isInitial": false, "isCompleted": false, "isCancelled": true }
]
}

Gửi statusId lấy từ danh mục này. Đơn mới luôn bắt đầu ở trạng thái isInitial.


Tạo hoặc cập nhật đơn​

PUT /api/open/v1/orders
{
"orders": [
{
"orderCode": "DH-1001",
"customer": { "matchField": "customerCode", "value": "KH-001" },
"items": [
{ "sku": "AO-THUN-M", "quantity": 2, "unitPrice": 150000 },
{ "variantId": "3e8b…", "quantity": 1, "unitPrice": 90000, "discountAmount": 10000, "vatRate": 8 }
],
"shippingFee": 30000,
"shippingName": "Nguyễn Thị Lan",
"shippingPhone": "0901234567",
"shippingAddress": "12 Lý Thường Kiệt",
"shippingProvinceCode": "01",
"shippingWardCode": "00070",
"statusId": "5a21…"
},
{
"customer": { "id": "1f0c6a7e-5d1b-4a8e-9a63-2c1f0f6b9d10" },
"items": [{ "sku": "AO-THUN-L", "quantity": 1, "unitPrice": 150000 }]
}
]
}

Nhận diện đơn bằng orderCode​

GửiKết quả
orderCode trùng một đơn đang cóCập nhật đơn đó
orderCode chưa cóTạo đơn mới với đúng mã này
Không gửi orderCodeLuôn tạo mới, mã tự sinh ORD-YYYYMMDD-XXXXXX

Mỗi dòng kết quả trả orderCode — lưu lại để lần sau cập nhật đúng đơn.

Nên luôn gửi orderCode

Không gửi orderCode thì gửi lại (retry) sẽ tạo đơn trùng. Đặt orderCode = mã đơn bên hệ thống của bạn để mọi lần gửi đều idempotent. Mã đã dùng cho một đơn — kể cả đơn đã xoá — không dùng lại được (ORDER_CODE_DUPLICATE).

Trường của một đơn​

TrườngKiểuGhi chú
orderCodechuỗi ≤ 50Mã đơn, duy nhất trong cửa hàng
customerobjectBắt buộc khi tạo: { "id": "<id khách>" } hoặc { "matchField": "phone" | "email" | "customerCode" | "<mã trường duy nhất>", "value": "…" }. Bỏ qua khi cập nhật
itemsmảng ≤ 200Bắt buộc khi tạo. Khi cập nhật: gửi ⇒ thay toàn bộ dòng hàng; không gửi ⇒ giữ nguyên
statusIduuidChuyển sang trạng thái này (đã đúng trạng thái thì bỏ qua)
shippingFeesố ≥ 0
shippingName, shippingPhone, shippingAddresschuỗishippingAddress là số nhà, tên đường
shippingProvinceCodechuỗiMã tỉnh / thành — xem danh mục
shippingWardCodechuỗiMã phường / xã, phải thuộc tỉnh ở trên. Khi cập nhật: gửi một trong hai mã là thay cả cặp (mã vắng ⇒ xoá)

Một dòng hàng:

TrườngKiểuGhi chú
variantId hoặc skuuuid / chuỗiPhải có trong danh mục sản phẩm của cửa hàng, nếu không ⇒ OPEN_API_ITEM_NOT_FOUND
quantitysố nguyên ≥ 1
unitPricesố ≥ 0
discountAmountsố ≥ 0Giảm giá trên dòng
vatRatesố ≥ 0% VAT
productNamechuỗiMặc định lấy tên sản phẩm trong danh mục
noteschuỗi ≤ 1000Ghi chú của dòng hàng

Tổng tiền, thuế và công nợ do hệ thống tính lại từ dòng hàng và phí giao hàng.

Đơn đã kết thúc

Đơn ở trạng thái hoàn thành hoặc đã huỷ không cập nhật được nữa — dòng đó trả status: "error". Chuyển trạng thái cũng tuân theo quy tắc của cửa hàng (trạng thái kết thúc bị khoá, bước cần duyệt).


Thanh toán​

Mỗi đơn có lịch sử thanh toán (từng khoản khách đã trả) và có thể có kỳ thanh toán (trả góp theo đợt). Khi một khoản được ghi hoặc xoá, hệ thống tự tính lại paidAmount, debtAmount, paymentStatus của đơn và số đã trả của kỳ, rồi gửi webhook payment.created / payment.deleted kèm order.updated.

Đối tượng thanh toán​

{
"id": "2a4c6e8b-0d1f-4b3a-9c5e-7f9b1d3a5c7e",
"externalId": "PT-2026-0001",
"orderId": "b7c2…",
"orderCode": "DH-1001",
"paymentTermId": null,
"amount": 300000,
"paymentMethod": "bank_transfer",
"reference": "FT26277123456",
"note": "Khách chuyển khoản đợt 1",
"paidAt": "2026-10-20T02:30:00.000Z",
"documents": [],
"createdById": "9e90…",
"source": "api:ERP Kho",
"createdAt": "2026-10-20T02:31:00.000Z",
"updatedAt": "2026-10-20T02:31:00.000Z",
"deletedAt": null
}
TrườngGhi chú
externalIdMã bên hệ thống của bạn (nếu bạn gửi khi tạo)
paymentMethodMã trong danh mục phương thức
referenceMã giao dịch ngân hàng / ví
documentsChứng từ đính kèm trên giao diện (name, url)
sourceNguồn lần ghi cuối (tạo hoặc xoá): ui hoặc api:<tên key>
deletedAtCó giá trị ⇒ khoản đã bị xoá

Đọc thanh toán của một đơn​

GET /api/open/v1/orders/b7c2…/payments
{
"data": {
"terms": [
{ "id": "4d1e…", "termNumber": 1, "name": "Đợt 1", "dueAmount": 300000, "dueDate": "2026-10-20", "paidAmount": 300000, "status": "paid", "notes": null },
{ "id": "8f2a…", "termNumber": 2, "name": "Đợt 2", "dueAmount": 116400, "dueDate": "2026-11-20", "paidAmount": 0, "status": "pending", "notes": null }
],
"payments": [ { "id": "2a4c…", "amount": 300000, "paymentTermId": "4d1e…", "…": "…" } ]
}
}

terms[].status: pending · partial · paid · overdue. Khoản đã xoá không có trong payments ở đây — dùng GET /payments để thấy cả khoản đã xoá.

Đồng bộ thanh toán của cả cửa hàng​

GET /api/open/v1/payments?updatedSince=2026-10-01T00:00:00Z&limit=50&excludeSource=api:ERP%20Kho

Theo keyset như GET /orders: tối đa 50 khoản / lần, cửa sổ tối đa 30 ngày. Trả cả khoản đã xoá (deletedAt có giá trị) để bạn đối soát — xoá bên bạn khi gặp. excludeSource bỏ các khoản mà lần ghi cuối đến từ nguồn đó. Chỉ gồm khoản của các đơn người tạo key được thấy.

Ghi một khoản thanh toán​

POST /api/open/v1/orders/b7c2…/payments
{
"externalId": "PT-2026-0001",
"amount": 300000,
"paymentMethod": "bank_transfer",
"paymentTermId": "4d1e…",
"reference": "FT26277123456",
"note": "Khách chuyển khoản đợt 1",
"paidAt": "2026-10-20T02:30:00Z"
}
TrườngKiểuGhi chú
amountsố > 0Bắt buộc, tối đa 2 chữ số thập phân
paymentMethodchuỗiBắt buộc, mã trong GET /payment-methods
paidAtISO 8601Bắt buộc, thời điểm khách trả
externalIdchuỗi ≤ 100Nên gửi — xem bên dưới
paymentTermIduuidKỳ thanh toán của chính đơn này (lấy ở terms), sai ⇒ ORDER_PAYMENT_TERM_NOT_FOUND
referencechuỗi ≤ 255Mã giao dịch
notechuỗi ≤ 2000

Tạo mới trả 201. Gửi lại cùng externalId cho cùng đơn ⇒ trả khoản đã có (200, "created": false), không ghi trùng.

Nên luôn gửi externalId

Không có externalId thì gửi lại (retry khi mất mạng) sẽ ghi trùng khoản tiền và làm sai công nợ của đơn. Đặt externalId = mã phiếu thu bên hệ thống của bạn.

Khoản ghi qua API vẫn gửi thông báo nội bộ "đơn đã thanh toán" / "thanh toán một phần" cho nhân viên như khi ghi trên giao diện.

Xoá một khoản​

DELETE /api/open/v1/orders/b7c2…/payments/2a4c…

Trả { "data": <khoản đã xoá, có deletedAt> }. Khoản không thuộc đơn ⇒ 404 OPEN_API_PAYMENT_NOT_FOUND. Không có API sửa một khoản: ghi sai thì xoá rồi ghi lại.

Phương thức thanh toán​

GET /api/open/v1/payment-methods
{
"data": [
{ "code": "cash", "label": "Tiền mặt" },
{ "code": "bank_transfer", "label": "Chuyển khoản" },
{ "code": "momo", "label": "MoMo" },
{ "code": "zalopay", "label": "ZaloPay" },
{ "code": "other", "label": "Khác" }
]
}