Skip to content

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 ​

json
{
  "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 ​

json
{
  "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ùng product_id hoặc sku (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ập name + 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_amount khô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ác shipping.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ự; null hoặ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ỗi 400 missing_field). Sửa đơn đang pending có 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 (total không âm); tổng trước khi trừ = total + prepaid_amount. Sửa đơn đang pending có thể đổi prepaid_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-options trả vat_percent hiệ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 định false): 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ại delivery_fee_before_vat khi 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 } (null nế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 trong values với loại select); 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ửi order_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_name hoặ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 /orders trả 400 missing_field kèm danh sách trường thiếu trong message. Có thể đọc cấu hình bằng GET /order-settings (web/app). Trường động bắt buộc: xem required ở GET /order-options.

  • Chống tạo trùng: gửi external_id (hoặc header Idempotency-Key, ≤100 ký tự, không khoảng trắng). Gọi lại với cùng giá trị trả đơn cũ (200, header Idempotent-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ái pending, 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.

bash
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.

bash
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.

  1. 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.
  2. POST /orders/{ref}/shipping — { "quote_id": "…" }: tạo đơn giao thật. Đơn chuyển sang trạng thái «Đang giao», shipping của đơn có mã vận đơn, link theo dõi, tài xế; phát webhook order.shipping_created.
  3. 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ùng label làm delivery_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ặc address/lat/lng, branch_id, provider, channel_id, cod_amount, weight_kg). Trả quotes theo 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ụ ​

bash
# 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"}'

Hỗ trợ: contact@sieutroly.ai