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/v1Mọ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:
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 laterKiểm tra key. /me trả về công ty, key và giới hạn gói của bạn:
curl "$AFI/me" -H "Authorization: Bearer $AFI_API_KEY"{
"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).
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": "[email protected]",
"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{
"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 |
|---|---|
| ref | Mã đơ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) |
| day | Ngày giao, YYYY-MM-DD, hôm nay hoặc sau đó |
| name, phone, email | Khá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, lng | Nơi giao: bắt buộc có address trừ khi bạn gửi lat và lng |
| timeWindows | 1 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 |
| serviceMinutes | Thời gian tại cửa, khi khác mặc định của công ty |
| packages, weightKg, volumeM3 | Hàng hoá, theo đơn vị mét bất kể ứng dụng hiển thị gì |
| skills, priority | Kỹ năng tài xế cần có; normal hoặc high |
| pickupAt, pickup | Nơ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, leaveAtDoor | Hướng dẫn của khách cho tài xế, ví dụ từ trang thanh toán của bạn |
| notifyCustomer | false: không gửi SMS, WhatsApp hay email cho khách này |
| notes, barcodes, customFields, access, language | Ghi 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:
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{
"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.”).
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"{ "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.
curl "$AFI/jobs/$JOB_ID" -H "Authorization: Bearer $AFI_API_KEY"{ "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.
curl "$AFI/days/$DAY/plan" -H "Authorization: Bearer $AFI_API_KEY"{
"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.
curl -X POST "$AFI/days/$DAY/dispatch" \
-H "Authorization: Bearer $AFI_API_KEY" \
-H "Idempotency-Key: send-$DAY-1"{
"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:
curl "$AFI/orders/$ORDER_ID" -H "Authorization: Bearer $AFI_API_KEY"{
"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.
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 |
|---|---|
| new | Chưa nằm trên tuyến nào tài xế đang có |
| assigned | Nằm trên tuyến đã gửi cho tài xế |
| unassigned | Các tuyến đã gửi bỏ nó ra |
| on_hold | Tạm giữ, không đưa vào lập tuyến (POST /orders/{id}/hold, /release) |
| needs_fix, locating | Khô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_failed | Bá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.
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.
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"
}'{
"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:
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ện | Khi nào |
|---|---|
| order.created, order.updated, order.deleted | Mộ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_changed | ETA 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.loaded | Tài xế đã xếp kiện hàng lên xe |
| order.rescheduled_by_customer, order.instructions_changed, order.rated | Khá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_attempted | Ngày làm việc của tài xế kết thúc khi đơn vẫn còn mở |
| order.moved_back | Việ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.dispatched | Tuyế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:
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
sequencecủ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ùngidvà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.deletedvàorder.moved_backmangid,refvàdaytrongdata, 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.
orders/create, orders/updated → bridge → POST /orders
orders/cancelled → bridge → DELETE /orders/{id}
fulfillment, tracking link ← bridge ← webhooks (signed)| Đơn Shopify | Đơn Afi Planner |
|---|---|
| name | ref: số đơn mà khách và nhân viên của bạn thấy (#1001) |
| shipping_address | name, phone, address trên một dòng, unit từ address2, và lat, lng khi Shopify có |
| email, note | email, customerNote |
| id | customFields: { "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 |
// 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 Planner | Trong Shopify |
|---|---|
| order.status_changed → assigned | Tạ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_transit | Sự kiện fulfillment out_for_delivery |
| order.status_changed → delivered | Sự kiện fulfillment delivered; data.event.proof có ảnh, chữ ký và người nhận |
| order.status_changed → failed | Sự kiện fulfillment attempted_delivery; POST /orders/{id}/redeliver gửi giao lại |
| order.eta_changed | Thô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.
{ "error": "Latitude can't be more than 90", "code": "invalid_request", "field": "lat" }| HTTP | code | Cần làm gì |
|---|---|---|
| 400 | invalid_request | Một trường bị sai (field), hoặc nội dung không phải JSON gửi kèm Content-Type: application/json |
| 400 | day_in_past | Đơn chỉ dành cho hôm nay hoặc sau đó |
| 400 | invalid_cursor | Gửi nextCursor của trang trước đúng như đã nhận |
| 401 | unauthorized | Key bị thiếu, sai, đã thu hồi hoặc hết hạn |
| 402 | subscription_ended | Gói đã hết hạn: vẫn đọc được dữ liệu |
| 403 | company_paused | Công ty đang bị tạm dừng |
| 403 | key_read_only | Key chỉ xem đã cố thay đổi dữ liệu |
| 403 | creator_forbidden | Người tạo key không được làm việc này: dùng key của người có quyền |
| 403 | webhook_needs_view | Thêm webhook cần key mà người tạo xem được đơn hàng |
| 403 | plan_full | Kế 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 |
| 404 | not_found, no_plan | Không có đơn, job hoặc webhook đó, hoặc ngày đó chưa có kế hoạch |
| 409 | order_worked | Tài xế đã xử lý đơn này: PATCH /orders/{id} sửa các trường của nó |
| 409 | order_sent | Tài xế đang có đơn này: huỷ bằng POST /orders/{id}/cancel thay vì xoá |
| 409 | order_cancelled | Đơn của ref này trong ngày đó đã bị huỷ: gửi acceptDuplicateRef: true để tạo đơn mới |
| 409 | order_moved | Đơn đã được chuyển sang ngày khác: movedTo cho biết đơn hiện tại |
| 409 | ref_taken | Một đơn đang mở khác đã có ref này, ở bất kỳ ngày nào |
| 409 | day_over | Ngày đó đã qua: không lập tuyến hay gửi được nữa |
| 409 | idempotency_conflict | Idempotency-Key này đã dùng cho một yêu cầu khác |
| 409 | idempotency_in_progress | Yê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 |
| 422 | cannot_plan | Không lập tuyến được cho ngày này: câu thông báo cho biết lý do |
| 429 | rate_limited | Quá 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-RemainingvàRateLimit-Reset(số giây đến phút kế tiếp). Vượt giới hạn, phản hồi là 429 kèmRetry-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.
/mehiển thị chúng. - Tối đa 10 endpoint webhook mỗi công ty.
HTTP/1.1 200 OK
RateLimit-Limit: 600
RateLimit-Remaining: 599
RateLimit-Reset: 4Mọ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.