API Đơn hàng
API để hệ thống bên ngoài (website bán hàng, ERP, sàn, POS…) tạo, xem, sửa và đổi trạng thái đơn hàng, tạo mã QR thanh toán, ghi nợ và gọi giao hàng cho đơn.
Base URL, cách lấy API key, xác thực, mã lỗi và giới hạn tần suất: xem Tổng quan. Nhận thông báo khi đơn thay đổi: Webhook sự kiện.
Đối tượng đơn hàng
{
"id": "5f0c…", // id nội bộ (UUID)
"order_number": "DH20261010-003", // mã đơn hiển thị trong ứng dụng
"external_id": "WEB-10025", // mã đơn bên bạn (nếu có gửi)
"status": "pending", // khóa trạng thái do cửa hàng tự thiết lập (mặc định: pending | paid | confirmed | preparing | completed | cancelled); paid và cancelled cố định. Danh sách: GET /order-options → statuses
"origin": "grabfood", // nguồn đến của đơn: chuỗi tự do ≤40 ký tự (website, grabfood, tai_quan…) hoặc null; gửi khi tạo/sửa đơn, lọc bằng ?origin=
"branch": { "id": "b3f1…", "name": "Chi nhánh Cầu Giấy" }, // chi nhánh lên đơn (xem GET /branches) hoặc null; gửi `branch_id` khi tạo/sửa đơn, lọc bằng ?branch_id=
"order_type": "delivery", // dine_in (Tại bàn) | pickup (Đến lấy) | delivery (Giao hàng) | null
"options": [ { "key": "dia_chi_giao_hang", "name": "Địa chỉ giao hàng", "type": "text", "value": "12 Lê Lợi, Q1" } ], // trường động (tuỳ chọn tự định nghĩa của cửa hàng) kèm giá trị; luôn có trong GET /orders, GET /orders/{ref} và mọi kết quả ghi. Sửa giá trị ở mọi trạng thái: PATCH /orders/{ref}/options. Lọc: ?opt=
"source": "api", // api | app
"customer": { "id": "…", "name": "Nguyễn Văn A", "phone": "0901234567", "email": "a@example.com" },
"items": [
{ "product_id": "…", "name": "Cà phê đen", "quantity": 2, "unit_price": 55000,
"toppings": [{ "id": "11", "name": "Thêm trứng", "price": 5000 }],
"note": "ít đá", "line_total": 110000 },
{ "product_id": null, "name": "Phí giao hàng", "quantity": 1, "unit_price": 15000, "toppings": [], "note": "", "line_total": 15000 }
],
"subtotal": 125000, "discount_amount": 5000, "delivery_fee": 30000, "delivery_fee_before_vat": false, "vat_percent": 0, "vat_amount": 0, "prepaid_amount": 0, "delivery_address": "12 Lê Lợi, Q1, TP.HCM", "total": 150000, "note": "Giao giờ hành chính",
"payment": {
"code": "SK48213", // mã thanh toán SKxxxxx — khách ghi vào nội dung chuyển khoản
"transfer_content": "SK48213",
"qr": null, // sau khi tạo QR: { url, amount, bank_name, account_no, account_name, content, created_at }
"paid": false, "paid_at": null, "paid_amount": null, "paid_via": null, // đã nhận tiền? (xem mục "Đã thanh toán")
"debt": false, "debt_paid": 0, "debt_due": 0 // ghi nợ công nợ (xem PATCH /orders/{ref}/debt)
},
"created_at": "2026-10-10T08:42:11.000Z", "updated_at": "2026-10-10T08:42:11.000Z"
}Giá và tên sản phẩm luôn do máy chủ tính từ danh mục (kể cả món phụ); chỉ dòng "tự nhập" (không có sku/product_id) dùng giá bạn gửi.
Endpoints
{ref} là id, order_number, payment.code hoặc external_id của đơn.
GET /products — products:read
Tham số: q (tên/sku), page, limit (≤100), include_inactive=true. Trả { data: [{ id, sku, name, price, unit, category_id, is_active, toppings:[{id,name,price}] }], pagination }.
GET /order-options — orders:read
Phân loại đơn và các tuỳ chọn tự định nghĩa của cửa hàng (ví dụ Số bàn, Địa chỉ giao hàng, Mức đường). Trả { "order_types": [{ value, label }], "options": [{ key, name, type, values?, required, applies_to }] } — type là text | number | select (chọn một trong values) | boolean | date (chuỗi YYYY-MM-DD; cũng nhận DD/MM/YYYY, lưu và trả về YYYY-MM-DD) | datetime (giờ địa phương; nhận YYYY-MM-DDTHH:mm, YYYY-MM-DD HH:mm, DD/MM/YYYY HH:mm, lưu và trả về YYYY-MM-DDTHH:mm); applies_to là các loại đơn áp dụng. Cửa hàng quản lý danh sách này ở web → Cài đặt → Tuỳ chọn đơn hàng.
GET /branches — orders:read
Danh sách chi nhánh của cửa hàng (quản lý ở web: Hàng Hóa → Chi Nhánh). Tham số: include_inactive=true để lấy cả chi nhánh đã ngừng hoạt động (mặc định chỉ chi nhánh đang hoạt động). Trả { "data": [{ "id", "name", "code", "phone", "address", "lat", "lng", "is_active" }] } (trường chưa nhập là null). Dùng id làm branch_id khi tạo/sửa đơn.
GET /orders — orders:read
Tham số (đều tùy chọn): status (nhiều giá trị cách nhau dấu phẩy), order_type, q (mã đơn/tên/SĐT/mã thanh toán/external_id, tìm không dấu), external_id, customer_phone, customer_email, branch_id (chi nhánh), opt (lọc theo trường động, xem bên dưới), total_min, total_max (khoảng tổng tiền), created_from, created_to, updated_since (đồng bộ thay đổi — sắp xếp tăng dần theo updated_at), page, limit (≤100, mặc định 30). Lọc theo trường động: opt là JSON { "<key>": điều kiện }, các điều kiện kết hợp AND. Điều kiện: eq (chuỗi/số/boolean/giá trị select) · contains (chữ, không phân biệt hoa thường) · min / max (số) · from / to (ngày YYYY-MM-DD hoặc ngày giờ YYYY-MM-DDTHH:mm). Ví dụ: ?opt={"so_ban":{"contains":"VIP"},"so_khach":{"min":4},"mang_di":{"eq":true}}. Sai kiểu → 400 invalid_request. Trả { "data": [order…], "pagination": { "page", "limit", "total", "total_pages" } }.
GET /orders/{ref} — orders:read
Chi tiết một đơn.
POST /orders — orders:write
{
"external_id": "WEB-10025",
"customer": { "name": "Nguyễn Văn A", "phone": "0901234567", "email": "a@example.com" },
"items": [
{ "sku": "CF-DEN", "quantity": 2, "toppings": ["11"], "note": "ít đá" },
{ "name": "Phí giao hàng", "unit_price": 15000, "quantity": 1 }
],
"order_type": "delivery",
"options": { "dia_chi_giao_hang": "12 Lê Lợi, Q1", "muc_duong": "50%" },
"discount_amount": 5000,
"note": "Giao giờ hành chính"
}items(1–100 dòng): mỗi dòng dùngproduct_idhoặcsku(mã sản phẩm) +quantity(1–9999) +toppings(mảng id món phụ, tùy chọn) +note; hoặc dòng tự nhậpname+unit_price+quantity.customer(tùy chọn):{ "id": "…" }khách có sẵn, hoặc{ name, phone | email }— cần tên và ít nhất SĐT hoặc email. Trùng SĐT/email với khách đã có thì dùng lại khách đó. Bỏ trống = khách vãng lai.discount_amountkhông vượt tổng tiền hàng.delivery_fee(tùy chọn, số ≥ 0, mặc định 0): phí giao hàng thu của khách, thuộc tính cố định của đơn. Tổng tiền = tạm tính − giảm giá +delivery_fee; mã QR thanh toán, nội dung đối chiếu chuyển khoản (số tiền phải ≥ tổng) và tin gửi khách đều theo tổng này. Khácshipping.fee(cước do hãng vận chuyển báo, không cộng vào tổng). Sai kiểu / âm →400 invalid_request.delivery_address(tùy chọn, chuỗi ≤ 300 ký tự;nullhoặc""= xóa): địa chỉ giao hàng của đơn, riêng từng đơn (khác địa chỉ trong hồ sơ khách). Hiện trong tin gửi khách và là địa chỉ gợi ý khi tạo đơn giao hàng qua đối tác vận chuyển; có thể đặt "bắt buộc" trong cấu hình đơn hàng (lỗi400 missing_field). Sửa đơn đangpendingcó thể đổi; không gửi thì giữ nguyên.prepaid_amount(tùy chọn, số ≥ 0, mặc định 0;null= 0): tiền khách đã trả trước, thuộc tính cố định của đơn và được trừ thẳng vào tổng:total= (tạm tính − giảm giá + phí giao hàng + VAT) −prepaid_amount= số còn phải thu (mã QR thanh toán, đối chiếu chuyển khoản và tin gửi khách theo số này). Số vượt tổng đơn bị cắt về tổng đơn (totalkhông âm); tổng trước khi trừ =total + prepaid_amount. Sửa đơn đangpendingcó thể đổiprepaid_amount; không gửi thì giữ nguyên. Sai kiểu / âm →400 invalid_request.VAT: cửa hàng cấu hình VAT mặc định (%, mặc định 0) ở web → Đơn hàng → Cấu hình VAT;
GET /order-optionstrảvat_percenthiện tại. Mỗi đơn lưu VAT lúc tạo (vat_percent,vat_amount) nên đổi cấu hình không làm đổi đơn cũ; API không đặt VAT cho từng đơn.delivery_fee_before_vat(tùy chọn,true/false, mặc địnhfalse):false= phí giao hàng cộng sau VAT (VAT chỉ tính trên tạm tính − giảm giá; tổng = đó + VAT + phí);true= phí giao hàng tính VAT (VAT trên tạm tính − giảm giá + phí; tổng = đó + VAT). VAT làm tròn đồng. Gửi lạidelivery_fee_before_vatkhi sửa đơn để đổi cách tính (VAT % của đơn giữ nguyên).branch_id(tùy chọn): id chi nhánh lấy từGET /branches. Id không tồn tại →404 branch_not_found; chi nhánh đã ngừng hoạt động →409 branch_inactive; không phải chuỗi →400. Đơn trả về cóbranch: { id, name }(nullnếu chưa chọn). Khi giao hàng qua Ahamove / AllinGo, địa chỉ chi nhánh là điểm lấy hàng.order_type(tùy chọn):dine_in|pickup|delivery.options(tùy chọn): object{ "<key>": giá trị }theo danh sách ởGET /order-options. Giá trị phải đúng kiểu (số, Có/Không, hoặc một trongvaluesvới loạiselect); khóa không tồn tại hoặc không áp dụng cho loại đơn →400 unknown_option/invalid_request. Khi gửiorder_type, các tuỳ chọn bắt buộc của loại đơn đó phải có, thiếu →400 missing_option.Trường bắt buộc do workspace cấu hình (Cài đặt đơn hàng → Trường bắt buộc khi tạo đơn): tên khách (
customer_namehoặc khách có sẵn), số điện thoại, địa chỉ khách (customer_address),order_type,branch_id,note. Thiếu trường nào đã được đặt bắt buộc thìPOST /orderstrả 400missing_fieldkèm danh sách trường thiếu trongmessage. Có thể đọc cấu hình bằngGET /order-settings(web/app). Trường động bắt buộc: xemrequiredởGET /order-options.Chống tạo trùng: gửi
external_id(hoặc headerIdempotency-Key, ≤100 ký tự, không khoảng trắng). Gọi lại với cùng giá trị trả đơn cũ (200, headerIdempotent-Replayed: true) thay vì tạo thêm — an toàn để thử lại khi mất kết nối.Thành công:
201+ đơn. Đơn mới ở trạng tháipending, cópayment.code.
PATCH /orders/{ref} — orders:write
Sửa đơn đang pending: items (thay toàn bộ), discount_amount, delivery_fee (đổi phí giao hàng; null = 0), prepaid_amount (đổi tiền trả trước; null = 0), delivery_address (đổi địa chỉ giao hàng; null = xóa), note, order_type, options (thay toàn bộ tuỳ chọn), branch_id (đổi chi nhánh; null = bỏ chi nhánh) — gửi trường nào đổi trường đó. Đã xử lý → 409 order_not_editable. Tổng tiền đổi thì QR thanh toán cũ bị xóa (cần tạo lại).
PATCH /orders/{ref}/options — orders:write
Cập nhật giá trị các trường động (tuỳ chọn đơn hàng) ở mọi trạng thái của đơn (khác PATCH /orders/{ref} chỉ sửa được đơn pending, và options ở đó thay toàn bộ). Body { "options": { "so_ban": "12", "muc_duong": "50%", "ghi_chu_bep": null } } — chỉ gửi các khóa cần đổi, khóa khác giữ nguyên; giá trị null hoặc "" = xóa tuỳ chọn đó. Giá trị phải đúng kiểu như khi tạo đơn (số, Có/Không, một trong values với loại select, ngày…). Lỗi: 400 unknown_option (khóa không tồn tại / không áp dụng cho loại đơn của đơn này), 400 invalid_request (sai kiểu / giá trị không nằm trong danh sách), 400 missing_option (xóa tuỳ chọn bắt buộc). Trả đơn đã cập nhật (có options mới) và phát webhook order.updated.
PATCH /orders/{ref}/status — orders:write
{ "status": "confirmed" } — một khóa trong danh sách trạng thái của cửa hàng (GET /order-options → statuses; mặc định pending, paid, confirmed, preparing, completed, cancelled). Đặt paid = đánh dấu thủ công đã thanh toán (paid_via: "manual"); đưa về trạng thái đầu (mặc định pending) thì bỏ ghi nhận thanh toán.
POST /orders/{ref}/payment-qr — orders:payment
Tạo mã QR VietQR: số tiền = tổng đơn, nội dung = payment.code. Body tùy chọn { "account_id": "…" } (mặc định dùng tài khoản nhận tiền mặc định của workspace, cấu hình trong ứng dụng). Trả đơn với payment.qr.url là ảnh PNG để hiển thị/gửi cho khách. Đơn đã hủy hoặc 0đ → 400; chưa cấu hình tài khoản nhận tiền → 400 NO_BANK_ACCOUNT.
PATCH /orders/{ref}/payment — orders:payment
Đánh dấu đã / chưa nhận tiền ở mọi trạng thái của đơn (không như PATCH /orders/{ref} chỉ sửa được đơn pending), trừ đơn đã cancelled (400). Body { "autoPaybanking": true } hoặc false (cũng nhận auto_paybanking, và chuỗi "true"/"false"). true → payment.paid = true, payment.paid_via = "manual" (nếu chưa thanh toán), payment.paid_at, và phát sự kiện webhook order.paid; false → bỏ dấu đã thanh toán. Không đổi trạng thái đơn. Phản hồi là đơn mới nhất, payment.autoPaybanking cho biết cờ hiện tại. Dùng khi nhận tiền ngoài hệ thống hoặc chuyển khoản tự động không khớp.
curl -X PATCH https://api.sieutroly.ai/api/public/v1/orders/WEB-10025/payment \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{"autoPaybanking":true}'PATCH /orders/{ref}/debt — orders:payment
Ghi nợ / bỏ ghi nợ đơn (công nợ khách hàng, xem ở Hàng hóa → Công nợ). Body { "debt": true } hoặc false (cũng nhận chuỗi "true"/"false"). Điều kiện: đơn chưa thanh toán, chưa hủy và đã gắn khách hàng (customer.id khác null) — nếu không 400 customer_required / 409 already_paid / 400. Đơn đã được thu một phần nợ thì không bỏ ghi nợ được (409 partially_paid). Phản hồi là đơn mới nhất; payment.debt cho biết đơn đang ghi nợ, payment.debt_paid là số đã thu, payment.debt_due là số còn nợ. Thu nợ làm ở ứng dụng (tiền thu phân bổ vào đơn cũ nhất trước, đủ tiền thì đơn chuyển sang đã thanh toán). Không đổi trạng thái đơn.
curl -X PATCH https://api.sieutroly.ai/api/public/v1/orders/WEB-10025/debt \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{"debt":true}'Đã thanh toán (tự động)
Workspace có thể bật "Xác thực mã chuyển khoản" ở kênh Casso/SePay. Khi tiền về mà nội dung chứa mã payment.code (SKxxxxx) và số tiền ≥ tổng đơn, đơn tự chuyển sang paid (nếu đang ở trạng thái đầu, mặc định pending; đơn đã confirmed/preparing/completed giữ nguyên trạng thái, chỉ ghi nhận đã nhận tiền). Khi đó payment.paid = true, kèm paid_at, paid_amount, paid_via (casso | sepay | manual). Chuyển thiếu tiền không đổi gì. Đơn đã paid không sửa được nội dung và không tạo lại QR (409 already_paid).
Gọi giao hàng cho đơn — shipping:write
Dùng chính kênh Ahamove / AllinGo đã kết nối của cửa hàng (giống nút «Giao hàng» trên web). Tạo đơn giao phát sinh phí thật với hãng.
POST /orders/{ref}/shipping/quote— báo giá từ kênh giao hàng phục vụ chi nhánh của đơn. Body (đều tùy chọn):channel_id(bắt buộc khi có nhiều kênh phục vụ chi nhánh),address,lat,lng,name,phone(trống = lấy từ đơn),weight_kg. Trảquotes: [{ id, carrier, service, fee, eta }]theo giá tăng dần; báo giá được giữ 15 phút trong đơn.POST /orders/{ref}/shipping—{ "quote_id": "…" }: tạo đơn giao thật. Đơn chuyển sang trạng thái «Đang giao»,shippingcủa đơn có mã vận đơn, link theo dõi, tài xế; phát webhookorder.shipping_created.POST /orders/{ref}/shipping/cancel—{ "reason": "…" }: huỷ đơn giao đang chạy (trạng thái đơn hàng giữ nguyên, có thể gọi giao lại).
Lỗi: 409 already_shipping (đơn đã có đơn giao đang chạy), 409 quote_expired (báo giá quá 15 phút), 400 channel_required, 409 shipping_disabled (chưa có kênh giao hàng), 409 branch_not_served.
Phí ship và giao độc lập
GET /shipping/options—orders:read: các kênh giao hàng đang hoạt động và chi nhánh mỗi kênh phục vụ.GET /places?q=—orders:read: gợi ý địa chỉ đầy đủ kèm toạ độ (bản đồ Ahamove, cần kênh Ahamove). Dùnglabellàmdelivery_address.POST /shipping/quote—orders:read: chỉ báo giá từ mọi kênh phục vụ chi nhánh (order= mã đơn để lấy địa chỉ/SĐT/COD từ đơn, hoặcaddress/lat/lng,branch_id,provider,channel_id,cod_amount,weight_kg). Trảquotestheo giá tăng dần +cheapest; không ghi gì vào đơn.POST /shipping/shipments—shipping:write: tạo đơn giao không gắn đơn hàng (gửi đồ, giao hộ…). Body:channel_id,dropoff: { phone (bắt buộc), address | lat+lng, name, note },pickup: { address, phone, name }(tuỳ chọn, mặc định điểm lấy hàng của kênh),service(id báo giá hoặc từ khoá hãng/dịch vụ; trống = rẻ nhất),cod_amount,weight_kg,reference,note. Trảexternal_id, hãng, phí, link theo dõi.GET /shipping/shipments/{external_id}?channel_id=—orders:read: trạng thái mới nhất từ hãng (state: created | delivering | delivered | failed | cancelled).POST /shipping/shipments/{external_id}/cancel—shipping:write: huỷ đơn giao độc lập.
Ví dụ
# Tạo đơn (an toàn khi gọi lại nhờ external_id)
curl -X POST https://api.sieutroly.ai/api/public/v1/orders \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" \
-d '{"external_id":"WEB-10025","customer":{"name":"Nguyễn Văn A","phone":"0901234567"},
"items":[{"sku":"CF-DEN","quantity":2}]}'
# Tạo QR thanh toán rồi hiển thị payment.qr.url cho khách
curl -X POST https://api.sieutroly.ai/api/public/v1/orders/WEB-10025/payment-qr -H "Authorization: Bearer $API_KEY"
# Đồng bộ đơn thay đổi từ một mốc
curl "https://api.sieutroly.ai/api/public/v1/orders?updated_since=2026-10-10T00:00:00Z&limit=100" -H "Authorization: Bearer $API_KEY"
# Hủy đơn
curl -X PATCH https://api.sieutroly.ai/api/public/v1/orders/WEB-10025/status \
-H "Authorization: Bearer $API_KEY" -H "Content-Type: application/json" -d '{"status":"cancelled"}'