Chuyển đến nội dung
Afi Planner

Nhà phát triển

Hướng dẫn tích hợp API

Gửi đơn từ cửa hàng, ERP hoặc WMS, lập và gửi tuyến, và nhận lại mọi trạng thái qua webhook. Mỗi bước có một lệnh curl bạn chạy được ngay.

Địa chỉ API

https://app.afi.io/api/v1

Mọi yêu cầu

Authorization: Bearer afi_…

Tài liệu đầy đủ: openapi.json (OpenAPI 3.1, phiên bản 1.10.0), hầu hết công cụ nhập được để tạo client. JSON vào và ra, thời gian theo UTC (ISO 8601), đơn vị mét.

1Trước khi bắt đầu: một API key

Trong Afi Planner, mở Cài đặt → Nhà phát triển, đặt tên cho key (hệ thống sẽ dùng nó) rồi bấm Tạo key. Sao chép ngay: key chỉ hiện một lần. Chủ tài khoản tạo key, hoặc thành viên có vai trò mang quyền Nhà phát triển.

  • Key hành động thay công ty bạn với quyền của người tạo ra nó, không bao giờ nhiều hơn quyền điều phối viên: nó thêm được đơn, lập và gửi tuyến, nhưng không bao giờ sửa đội ngũ, thanh toán hay cài đặt.
  • Chọn Chỉ xem cho hệ thống chỉ đọc (mọi phương thức khác trả về 403 key_read_only), và hạn dùng nếu bạn muốn.
  • Đổi key tạo key mới cùng tên và giữ key cũ hoạt động thêm 1 hoặc 24 giờ, để bạn đổi mà không bị gián đoạn.

Giữ key trên máy chủ của bạn, không bao giờ để trong trình duyệt hay ứng dụng di động. Các ví dụ dưới đây đọc key, địa chỉ API và một ngày giao hàng từ shell của bạn:

Shell
export AFI_API_KEY="afi_…"          # your key, from Settings → Developers
export AFI="https://app.afi.io/api/v1"
export DAY="2026-11-03"              # a delivery day, YYYY-MM-DD: today or later

Kiểm tra key. /me trả về công ty, key và giới hạn gói của bạn:

Yêu cầu
curl "$AFI/me" -H "Authorization: Bearer $AFI_API_KEY"
Phản hồi 200
{
  "company": { "id": 1, "name": "Bayside Fresh Co." },
  "key": { "id": 1, "name": "Shop backend", "prefix": "afi_wzPUFcTM", "scope": "full", "expiresAt": null },
  "subscription": { "state": "trial", "limits": { "maxStopsPerPlan": 500, "optimizationsPerMonth": 2000, … } }
}

Chưa viết code? Cài đặt → Nhà phát triển → Thử key này gửi các yêu cầu mẫu bằng key của bạn và hiện phản hồi cùng lệnh curl. Tải bộ Postman cho bạn mọi yêu cầu trong hướng dẫn này, thiết lập sẵn cho công ty bạn (key là một biến bạn tự điền), và Yêu cầu API liệt kê mọi lệnh gọi trong 3 ngày qua.

2Gửi đơn cho một ngày

ref là mã đơn của chính bạn và day là ngày giao. Gửi address, hoặc lat và lng, hoặc cả hai: có toạ độ thì địa chỉ không phải tra lại. Khung giờ là giờ địa phương tại địa chỉ (HH:MM).

Yêu cầu
curl -X POST "$AFI/orders" \
  -H "Authorization: Bearer $AFI_API_KEY" \
  -H "Content-Type: application/json" \
  -H "Idempotency-Key: SHOP-1042-$DAY" \
  -d @- <<EOF
{
  "ref": "SHOP-1042",
  "day": "$DAY",
  "name": "Olivia Shah",
  "phone": "+14155550142",
  "email": "olivia@example.com",
  "address": "1355 Market St, San Francisco, CA 94103",
  "lat": 37.7768,
  "lng": -122.4166,
  "timeWindows": [{ "start": "09:00", "end": "12:00" }],
  "packages": 2,
  "weightKg": 4.5,
  "customerNote": "Leave with the front desk"
}
EOF
Phản hồi 201
{
  "id": 1,
  "ref": "SHOP-1042",
  "day": "2026-11-03",
  "status": "new",
  "test": false,
  "phoneIssue": null,
  "addressIssue": null,
  "timeZone": "America/Los_Angeles",
  "timeWindows": [{ "start": "09:00", "end": "12:00", "startAt": "2026-11-03T09:00:00-08:00", "endAt": "2026-11-03T12:00:00-08:00" }],
  "route": null,
  "draftRoute": null,
  "trackingUrl": "https://app.afi.io/t/1.bfc92ec4c7c1da1b30b0ccce",
  …
}

Giữ lại id: đó là đơn hàng trong mọi lệnh gọi sau. Một ref chỉ một đơn đang mở trên mọi ngày, không phải một đơn mỗi ngày: gửi một ref đang có đơn mở ở bất kỳ ngày nào sẽ cập nhật đơn đó (200) và giữ nguyên ngày của đơn, kể cả khi đơn đã được chuyển ngày (đổi ngày bằng PATCH /orders/{id}). Một đơn còn mở cho đến khi giao xong, giao thất bại hoặc bị bỏ qua; đơn chưa được ghé trong một ngày đã qua thì không còn mở. Khi tài xế đã xử lý đơn, phản hồi là 409 order_worked và PATCH /orders/{id} sửa các trường của đơn.

Khi không có đơn mở, một ref có đơn giao thất bại trong 14 ngày trước day mới sẽ thành lần giao tiếp theo của đơn đó, cùng link theo dõi; nếu không (thất bại lâu hơn, hoặc đơn đã giao) thì là đơn mới (201). Để luôn tạo đơn mới, gửi "acceptDuplicateRef": true: khi đó ref không còn chỉ một đơn duy nhất, nên hãy giữ id.

TrườngÝ nghĩa
refMã đơn của bạn, duy nhất trong các đơn đang mở trên mọi ngày (không phân biệt hoa thường và khoảng trắng hai đầu)
dayNgày giao, YYYY-MM-DD, hôm nay hoặc sau đó
name, phone, emailKhách hàng. Số điện thoại lưu theo E.164; số không nhận được tin nhắn vẫn được lưu kèm phoneIssue
address, unit, lat, lngNơi giao: bắt buộc có address trừ khi bạn gửi lat và lng
timeWindows1 hoặc 2 khung { "start": "09:00", "end": "12:00" }; giờ kết thúc trước giờ bắt đầu nghĩa là kết thúc vào hôm sau
serviceMinutesThời gian tại cửa, khi khác mặc định của công ty
packages, weightKg, volumeM3Hàng hoá, theo đơn vị mét bất kể ứng dụng hiển thị gì
skills, priorityKỹ năng tài xế cần có; normal hoặc high
pickupAt, pickupNơi lấy hàng trước: tên hoặc id của điểm lấy hàng đã lưu, "depot", một địa chỉ, hoặc "none"
customerNote, leaveAtDoorHướng dẫn của khách cho tài xế, ví dụ từ trang thanh toán của bạn
notifyCustomerfalse: không gửi SMS, WhatsApp hay email cho khách này
notes, barcodes, customFields, access, languageGhi chú cho tài xế, mã vạch kiện hàng, trường riêng của bạn, mã cổng và lối vào, ngôn ngữ của khách (en, sv, vi, es)

Gửi lại một ref sẽ đổi các trường bạn gửi; các trường khác giữ nguyên. Trường API không biết sẽ bị từ chối (400 invalid_request): hãy để dữ liệu riêng của bạn trong customFields, trường này trả về trên đơn và trong mọi webhook.

Nhiều đơn cùng lúc: POST /orders/batch nhận tối đa 1.000 đơn, mỗi đơn có day riêng. Mỗi dòng có kết quả riêng, nên một dòng lỗi không chặn các dòng khác:

Yêu cầu
curl -X POST "$AFI/orders/batch" \
  -H "Authorization: Bearer $AFI_API_KEY" \
  -H "Content-Type: application/json" \
  -d @- <<EOF
{
  "orders": [
    { "ref": "SHOP-1043", "day": "$DAY", "name": "Marcus Lee", "address": "600 Guerrero St, San Francisco, CA 94110", "lat": 37.7612, "lng": -122.4241 },
    { "ref": "SHOP-1044", "day": "$DAY", "name": "Ana Torres", "address": "2101 Irving St, San Francisco, CA 94122", "lat": 37.7637, "lng": -122.4825,
      "timeWindows": [{ "start": "13:00", "end": "17:00" }] },
    { "ref": "SHOP-1045", "day": "$DAY", "name": "Jordan Price", "lat": 120, "lng": -122.4752 }
  ]
}
EOF
Phản hồi 200
{
  "results": [
    { "row": 1, "ref": "SHOP-1043", "id": 2, "result": "created", "addressIssue": null },
    { "row": 2, "ref": "SHOP-1044", "id": 3, "result": "created", "addressIssue": null },
    { "row": 3, "ref": "SHOP-1045", "result": "error", "error": "Latitude can't be more than 90", "code": "invalid_order", "field": "lat" }
  ],
  "created": 2, "updated": 0, "failed": 1
}

Một ref xuất hiện hai lần trong cùng một batch chỉ tính một lần, dù các dòng thuộc ngày nào: các dòng sau nhận ref_repeated. Với acceptDuplicateRef (cạnh orders) điều này chỉ áp dụng trong cùng một ngày.

Thử trên công ty thật của bạn? Cho ref bắt đầu bằng API-TEST- (ví dụ API-TEST-0001): đơn như vậy không bao giờ được xếp tuyến, không nhắn tin cho khách, không được tính ở đâu và tự xoá sau 3 ngày. Webhook của nó vẫn được gửi, với "test": true.

3Lập tuyến

Lấy danh sách tài xế, chọn ai làm ngày đó, rồi bắt đầu lập tuyến. Việc lập tuyến chạy nền: phản hồi là một job để theo dõi.

Tài xế được thêm trong ứng dụng, ở trang Tài xế. API liệt kê họ và chọn ai làm việc mỗi ngày; chưa chọn tài xế nào thì lập tuyến trả về 400 (“Choose at least one driver for this plan.”).

Yêu cầu
curl "$AFI/drivers" -H "Authorization: Bearer $AFI_API_KEY"

curl -X PUT "$AFI/days/$DAY/drivers" \
  -H "Authorization: Bearer $AFI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "driverIds": [1, 2] }'

curl -X POST "$AFI/days/$DAY/optimize" \
  -H "Authorization: Bearer $AFI_API_KEY" \
  -H "Idempotency-Key: plan-$DAY-1"
Phản hồi 202
{ "jobId": "75d50077-6549-449a-bccc-80cc661174c6", "state": "running", "planId": 1, "day": "2026-11-03" }

Hỏi job vài giây một lần cho đến khi state là done (hoặc failed, với lý do trong error). Một ngày 176 điểm giao và 8 tài xế mất khoảng 40 giây; ngày nhỏ hơn thì nhanh hơn.

Yêu cầu
curl "$AFI/jobs/$JOB_ID" -H "Authorization: Bearer $AFI_API_KEY"
Phản hồi 200
{ "id": "75d50077-…", "state": "done", "kind": "optimize", "planId": 1, "day": "2026-11-03", "error": null, … }

GET /days/{day}/plan trả về các tuyến. Khi chưa gửi, chúng nằm trong draft: routes luôn là những gì tài xế đang có, và điều phối viên vẫn có thể sửa bản nháp trong ứng dụng.

Yêu cầu
curl "$AFI/days/$DAY/plan" -H "Authorization: Bearer $AFI_API_KEY"
Phản hồi 200
{
  "day": "2026-11-03", "planId": 1, "plannedAt": null, "dispatchedAt": null,
  "routes": [],
  "notOnRoute": [],
  "draft": {
    "plannedAt": "2026-11-02T22:12:43Z",
    "routes": [
      { "driverId": 1, "driverName": "Daniel Kim", "start": "2026-11-03T16:32:57Z", "end": "2026-11-03T17:12:57Z",
        "distanceMeters": 12623, "travelSeconds": 1233, "breaks": [],
        "visits": [
          { "position": 1, "orderId": 2, "ref": "SHOP-1043", "kind": "delivery", "arrival": "2026-11-03T16:39:51Z", "departure": "2026-11-03T16:44:51Z" },
          { "position": 2, "orderId": 1, "ref": "SHOP-1042", "kind": "delivery", "arrival": "2026-11-03T17:00:00Z", "departure": "2026-11-03T17:05:00Z" }
        ] }
    ],
    "notOnRoute": []
  }
}

4Gửi tuyến cho tài xế

Khi gửi, mỗi tài xế nhận tuyến của mình trong ứng dụng tài xế, hoặc một đường link mở được trên mọi điện thoại (url). Gọi lại sau khi có thay đổi để gửi cập nhật.

Yêu cầu
curl -X POST "$AFI/days/$DAY/dispatch" \
  -H "Authorization: Bearer $AFI_API_KEY" \
  -H "Idempotency-Key: send-$DAY-1"
Phản hồi 200
{
  "day": "2026-11-03",
  "dispatchedAt": "2026-11-03T14:02:57Z",
  "drivers": [
    { "driverId": 1, "url": "https://app.afi.io/d/1.3uu3J7…" },
    { "driverId": 2, "url": "https://app.afi.io/d/2.Qm9xk2…" }
  ]
}

Một ngày có đơn nhưng chưa lập tuyến trả về 400 (“Plan the routes first”), ngày đã qua trả về 409 day_over.

5Xem từng đơn đang ở đâu

Một đơn, với tuyến như đã gửi cho tài xế, ETA, mọi báo cáo của tài xế kèm bằng chứng giao hàng và mọi lần giao:

Yêu cầu
curl "$AFI/orders/$ORDER_ID" -H "Authorization: Bearer $AFI_API_KEY"
Phản hồi 200
{
  "id": 1, "ref": "SHOP-1042", "status": "assigned",
  "route": { "driverId": 1, "driverName": "Daniel Kim", "position": 2, "eta": "2026-11-03T17:00:00Z", "liveEta": null, "pickupEta": null },
  "draftRoute": null,
  "trackingUrl": "https://app.afi.io/t/1.bfc92ec4c7c1da1b30b0ccce",
  "events": [],
  "attempts": [{ "number": 1, "orderId": 1, "day": "2026-11-03", "current": true, "moved": false, "outcome": null }],
  …
}

Mọi đơn của một ngày, hoặc một ref. Với limit (1–1.000) phản hồi chia theo trang: gửi nextCursor của phản hồi làm cursor cho trang tiếp theo; ở trang cuối nó là null.

Yêu cầu
curl "$AFI/orders?day=$DAY&ref=SHOP-1042" -H "Authorization: Bearer $AFI_API_KEY"

curl "$AFI/orders?day=$DAY&status=assigned&limit=100" -H "Authorization: Bearer $AFI_API_KEY"

Mã hoá URL cho ref trong query: #1001 của Shopify là %231001 (encodeURIComponent). Không mã hoá thì mọi thứ sau # bị bỏ và kết quả là mọi đơn của ngày.

Các trạng thái:

statusÝ nghĩa
newChưa nằm trên tuyến nào tài xế đang có
assignedNằm trên tuyến đã gửi cho tài xế
unassignedCác tuyến đã gửi bỏ nó ra
on_holdTạm giữ, không đưa vào lập tuyến (POST /orders/{id}/hold, /release)
needs_fix, locatingKhông dùng được địa chỉ (addressIssue cho biết lý do), hoặc đang tra địa chỉ
to_pickup, at_pickup, picked_up, pickup_failedBáo cáo của tài xế tại điểm lấy hàng
in_transit, at_deliveryĐang đến, đang ở cửa
delivered, failed, skippedĐã xong: events có lý do, ảnh, chữ ký và người nhận
cancelledĐã huỷ: cancellation có reason, note, ai huỷ (by), lúc nào (cancelledAt) và returnToDepot khi kiện hàng đã ở trên xe

Sửa đơn bằng PATCH /orders/{id} (chỉ các trường bạn gửi), huỷ bằng POST /orders/{id}/cancel (một reason và note tuỳ chọn: đơn vẫn còn, với trạng thái cancelled), xoá đơn nhập nhầm bằng DELETE /orders/{id} (đơn đã gửi cho tài xế trả về 409 order_sent: hãy huỷ thay vì xoá), và giao lại một đơn thất bại bằng POST /orders/{id}/redeliver: một lần giao mới vào ngày làm việc kế tiếp (hoặc day bạn gửi), giữ nguyên link theo dõi.

Yêu cầu
curl -X PATCH "$AFI/orders/$ORDER_ID" \
  -H "Authorization: Bearer $AFI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{ "timeWindows": [{ "start": "10:00", "end": "13:00" }], "notes": "Gate code at the side door" }'

6Nhận mọi thay đổi qua webhook

Thay vì hỏi đi hỏi lại, để Afi Planner gửi từng thay đổi tới máy chủ của bạn. Thêm endpoint trong Cài đặt → Nhà phát triển, hoặc qua API. Phản hồi kèm khoá ký secret, chỉ hiện một lần.

Yêu cầu
curl -X POST "$AFI/webhooks" \
  -H "Authorization: Bearer $AFI_API_KEY" \
  -H "Content-Type: application/json" \
  -d '{
    "url": "https://example.com/afi/webhooks",
    "events": ["order.status_changed", "order.eta_changed", "route.dispatched"],
    "description": "Shop backend"
  }'
Phản hồi 201
{
  "id": 1,
  "url": "https://example.com/afi/webhooks",
  "events": ["order.status_changed", "order.eta_changed", "route.dispatched"],
  "enabled": true,
  "secret": "whsec_…",
  …
}

Gửi sự kiện thử bằng POST /webhooks/{id}/test. Mỗi sự kiện là một POST JSON:

Sự kiện
POST /afi/webhooks
Afi-Event: order.status_changed
Afi-Delivery: 3
Afi-Signature: t=1793078018,v1=5f0c…

{
  "id": "evt_EbCy9y6Dq8yoTQXL",
  "type": "order.status_changed",
  "sequence": 3,
  "occurredAt": "2026-11-03T14:02:58Z",
  "createdAt": "2026-11-03T14:02:58Z",
  "companyId": 1,
  "test": false,
  "data": {
    "order": { "id": 1, "ref": "SHOP-1042", "status": "assigned", "route": { "driverId": 1, "eta": "2026-11-03T17:00:00Z", … }, … },
    "previousStatus": "new",
    "event": null
  }
}
Sự kiệnKhi nào
order.created, order.updated, order.deletedMột đơn được thêm, sửa (changed liệt kê các trường) hoặc xoá
order.cancelledĐơn bị huỷ, trong ứng dụng hoặc bằng POST /orders/{id}/cancel, kèm cancellation; không có order.status_changed hay order.deleted cho nó
order.status_changedĐược gửi trên tuyến, đang đến, đã giao, thất bại…, kèm lý do và bằng chứng
order.eta_changedETA thay đổi theo ngưỡng của bạn, mặc định 5 phút (trực tiếp khi tài xế đang chạy)
order.loadedTài xế đã xếp kiện hàng lên xe
order.rescheduled_by_customer, order.instructions_changed, order.ratedKhách chọn ngày khác, sửa hướng dẫn hoặc đánh giá lần giao trên trang theo dõi
order.not_attemptedNgày làm việc của tài xế kết thúc khi đơn vẫn còn mở
order.moved_backViệc chuyển sang ngày khác được hoàn tác: đơn lại là đơn trong movedBackTo
plan.optimized, plan.job_failed, route.dispatchedTuyến mới trên bảng tuyến, một lần lập tuyến thất bại, tuyến hoặc cập nhật đã gửi

Kiểm tra chữ ký trước khi tin một sự kiện: v1 là HMAC-SHA256 của t.body với secret của bạn, dạng hex. Từ chối sự kiện cũ hơn 5 phút, trả 2xx trong 10 giây rồi mới xử lý. Bằng Node:

Node.js
import http from 'node:http'
import crypto from 'node:crypto'

const secret = process.env.AFI_WEBHOOK_SECRET // whsec_…

function verify(header, rawBody) { // rawBody: the bytes as they came, before any JSON parsing
  const parts = header.split(',').map((p) => p.split('='))
  const t = Number(parts.find(([k]) => k === 't')?.[1])
  if (!t || Math.abs(Date.now() / 1000 - t) > 300) return false // older than 5 minutes
  const expected = crypto.createHmac('sha256', secret).update(`${t}.`).update(rawBody).digest()
  return parts.filter(([k]) => k === 'v1').some(([, sig]) => {
    const got = Buffer.from(sig, 'hex')
    return got.length === expected.length && crypto.timingSafeEqual(got, expected)
  })
}

http.createServer((req, res) => {
  const chunks = []
  req.on('data', (c) => chunks.push(c))
  req.on('end', () => {
    const raw = Buffer.concat(chunks)
    if (!verify(req.headers['afi-signature'] ?? '', raw)) { res.writeHead(401).end(); return }
    res.writeHead(200).end() // answer first, then process
    const event = JSON.parse(raw.toString('utf8'))
    const order = event.data.order ?? event.data // order.deleted, order.moved_back: id, ref, day
    console.log(event.sequence, event.type, order.ref, order.status)
  })
}).listen(3000)
  • Thử lại: phản hồi khác 2xx, hoặc không phản hồi trong 10 giây, sẽ được gửi lại sau 1 phút, 5 phút, 30 phút, 2 giờ, 6 giờ và 12 giờ. Endpoint lỗi 3 ngày liền sẽ bị tắt.
  • Thứ tự: sự kiện có thể đến không theo thứ tự. Chỉ áp dụng một sự kiện nếu sequence của nó lớn hơn sự kiện cuối cùng bạn đã áp dụng cho đơn đó; lần gửi lại mang cùng id và sequence.
  • Sau khi đổi khoá ký (POST /webhooks/{id}/rotate-secret), khoá cũ vẫn ký thêm 24 giờ: header khi đó có hai giá trị v1, chấp nhận giá trị nào khớp.
  • Sự kiện trong lúc endpoint bị tắt không được giữ lại cho nó: sau khi bật lại, đọc lại ngày bằng GET /orders?day=.
  • Mọi lần gửi, nội dung và phản hồi của máy chủ bạn được giữ 3 ngày trong Cài đặt → Nhà phát triển và trong GET /webhooks/{id}/deliveries, với Thử lại và Gửi lại.
  • Không có events, endpoint nhận mọi loại sự kiện (*).
  • order.deleted và order.moved_back mang id, ref và day trong data, không có order.
  • Liên kết bằng chứng giao hàng (ảnh, chữ ký) được ký và dùng được 1 giờ: tải về những gì bạn cần giữ, hoặc đọc lại đơn để có liên kết mới.

Khoá bí mật là cả chuỗi whsec_…. Tính chữ ký trên body đúng như khi nhận: framework nào parse JSON trước thì phải đưa bạn các byte gốc (trong Express: express.raw({ type: "application/json" }) cho route này).

7Kết nối cửa hàng: ví dụ với Shopify

Afi Planner không nhận webhook của cửa hàng trực tiếp: một dịch vụ nhỏ của bạn, gọi là cầu nối, đứng ở giữa. Nó biến mỗi đơn của cửa hàng thành một đơn Afi Planner, và mỗi sự kiện của Afi Planner thành một cập nhật trong cửa hàng. Các bước tương tự dùng được cho WooCommerce, ERP hay WMS.

Luồng
orders/create, orders/updated  →  bridge  →  POST /orders
orders/cancelled               →  bridge  →  DELETE /orders/{id}
fulfillment, tracking link     ←  bridge  ←  webhooks (signed)
Đơn ShopifyĐơn Afi Planner
nameref: số đơn mà khách và nhân viên của bạn thấy (#1001)
shipping_addressname, phone, address trên một dòng, unit từ address2, và lat, lng khi Shopify có
email, noteemail, customerNote
idcustomFields: { "shopifyOrderId": "…" }, để tìm lại đơn Shopify từ bất kỳ sự kiện nào
(delivery date)day: ngày khách chọn lúc thanh toán nếu bạn có hỏi, nếu không là ngày giao kế tiếp của bạn
Node.js
// Shopify orders/create and orders/updated → Afi Planner (check Shopify's own signature first)
async function sendToAfi(o, day) {
  const a = o.shipping_address
  const res = await fetch(`${process.env.AFI}/orders`, {
    method: 'POST',
    headers: {
      Authorization: `Bearer ${process.env.AFI_API_KEY}`,
      'Content-Type': 'application/json',
      'Idempotency-Key': `shopify-${o.id}-${o.updated_at}`, // Shopify may send a webhook twice
    },
    body: JSON.stringify({
      ref: o.name,
      day,
      name: a.name,
      phone: a.phone ?? o.phone,
      email: o.email,
      address: [a.address1, a.city, a.province_code, a.zip].filter(Boolean).join(', '),
      unit: a.address2 || undefined,
      ...(a.latitude != null ? { lat: a.latitude, lng: a.longitude } : {}),
      customerNote: o.note || undefined,
      customFields: { shopifyOrderId: String(o.id) },
    }),
  })
  const body = await res.json()
  if (!res.ok) throw new Error(`Afi Planner ${res.status} ${body.code}: ${body.error}`)
  return body // keep body.id with the Shopify order
}

Lưu id mà Afi Planner trả về cạnh đơn Shopify: huỷ đơn (POST /orders/{id}/cancel) và các thay đổi sau cần nó; không có nó thì GET /orders?ref=%231001 tìm được đơn ở bất kỳ ngày nào. Cập nhật sau khi tài xế đã xử lý đơn sẽ trả về 409 order_worked: sửa bằng PATCH /orders/{id}, hoặc để nguyên.

Sự kiện Afi PlannerTrong Shopify
order.status_changed → assignedTạo fulfillment với trackingUrl của đơn làm liên kết theo dõi: khách theo dõi tài xế trực tiếp
order.status_changed → in_transitSự kiện fulfillment out_for_delivery
order.status_changed → deliveredSự kiện fulfillment delivered; data.event.proof có ảnh, chữ ký và người nhận
order.status_changed → failedSự kiện fulfillment attempted_delivery; POST /orders/{id}/redeliver gửi giao lại
order.eta_changedThông báo “sẽ đến lúc” của riêng bạn, nếu bạn có hiển thị

Thử phía đơn hàng trước với ref bắt đầu bằng API-TEST- (ví dụ API-TEST-1001): các đơn này không bao giờ được lập tuyến hay nhắn tin và bị xoá sau 3 ngày; webhook của chúng có "test": true.

8Lỗi

Mọi lỗi có cùng một dạng: một câu bạn có thể hiển thị hoặc ghi log, một code để chương trình xử lý, và field khi một trường bị sai.

Phản hồi 400
{ "error": "Latitude can't be more than 90", "code": "invalid_request", "field": "lat" }
HTTPcodeCần làm gì
400invalid_requestMột trường bị sai (field), hoặc nội dung không phải JSON gửi kèm Content-Type: application/json
400day_in_pastĐơn chỉ dành cho hôm nay hoặc sau đó
400invalid_cursorGửi nextCursor của trang trước đúng như đã nhận
401unauthorizedKey bị thiếu, sai, đã thu hồi hoặc hết hạn
402subscription_endedGói đã hết hạn: vẫn đọc được dữ liệu
403company_pausedCông ty đang bị tạm dừng
403key_read_onlyKey chỉ xem đã cố thay đổi dữ liệu
403creator_forbiddenNgười tạo key không được làm việc này: dùng key của người có quyền
403webhook_needs_viewThêm webhook cần key mà người tạo xem được đơn hàng
403plan_fullKế hoạch trong ngày không còn chỗ cho ngần ấy đơn mới trong gói của bạn (planFull); không có gì được lưu
404not_found, no_planKhông có đơn, job hoặc webhook đó, hoặc ngày đó chưa có kế hoạch
409order_workedTài xế đã xử lý đơn này: PATCH /orders/{id} sửa các trường của nó
409order_sentTài xế đang có đơn này: huỷ bằng POST /orders/{id}/cancel thay vì xoá
409order_cancelledĐơn của ref này trong ngày đó đã bị huỷ: gửi acceptDuplicateRef: true để tạo đơn mới
409order_movedĐơn đã được chuyển sang ngày khác: movedTo cho biết đơn hiện tại
409ref_takenMột đơn đang mở khác đã có ref này, ở bất kỳ ngày nào
409day_overNgày đó đã qua: không lập tuyến hay gửi được nữa
409idempotency_conflictIdempotency-Key này đã dùng cho một yêu cầu khác
409idempotency_in_progressYêu cầu đầu tiên với key này vẫn đang chạy: hỏi lại sau giây lát
422cannot_planKhông lập tuyến được cho ngày này: câu thông báo cho biết lý do
429rate_limitedQuá nhiều yêu cầu: đợi số giây trong Retry-After

Thử lại an toàn: gửi header Idempotency-Key với mỗi POST (một chuỗi duy nhất bất kỳ, như mã đơn và số lần thử). Cùng key và cùng nội dung trong 24 giờ trả lại phản hồi đầu tiên thay vì làm hai lần.

9Giới hạn tần suất và giới hạn gói

  • 600 yêu cầu mỗi phút cho mỗi key, đếm theo phút lịch. Mọi phản hồi cho biết bạn đang ở đâu: RateLimit-Limit, RateLimit-Remaining và RateLimit-Reset (số giây đến phút kế tiếp). Vượt giới hạn, phản hồi là 429 kèm Retry-After.
  • Tối đa 1.000 đơn mỗi yêu cầu batch.
  • Giới hạn gói của bạn áp dụng như trong ứng dụng: số điểm giao mỗi kế hoạch (500 khi dùng thử miễn phí), số lần lập tuyến và tra địa chỉ mỗi tháng. /me hiển thị chúng.
  • Tối đa 10 endpoint webhook mỗi công ty.
Header phản hồi
HTTP/1.1 200 OK
RateLimit-Limit: 600
RateLimit-Remaining: 599
RateLimit-Reset: 4

Mọi ví dụ trên trang này đã được chạy thử với API ngày 27 thg 10, 2026 (OpenAPI 1.10.0). Thiết lập trong ứng dụng: hướng dẫn API & webhook

Có API key trong một phút

Dùng thử miễn phí 3 tháng: API và webhook có trong mọi gói. Không cần thẻ tín dụng.