Đăng ký
Địa chỉ nhận phải là HTTPS, và hệ thống chỉ gọi về khi job đã vào trạng thái kết thúc, không gọi ở QUEUED hay PROCESSING. Phản hồi khi đăng ký thành công trả về khóa ký một lần duy nhất, hãy lưu lại ngay.
Tạo job tối ưu, theo dõi trạng thái và lấy kết quả. Đây là nhóm trung tâm của Route4Green.
Phần này giải thích cách gọi nhóm endpoint tối ưu tuyến. Danh sách endpoint đầy đủ nằm ngay bên dưới; ở đây là những điều bạn cần biết trước khi gọi lần đầu.
Tối ưu tuyến chạy bất đồng bộ. Bạn gửi job, hệ thống nhận và xử lý, kết quả lấy về sau bằng cách hỏi trạng thái hoặc nhận webhook.
Gửi thành công trả về 202 Accepted. Nghĩa là job đã được ghi nhận, chưa phải đã xong. Phản hồi mang mã job để bạn tra cứu, không mang kết quả tối ưu.
Mỗi request gửi job phải kèm header Idempotency-Key. Khóa này thuộc phạm vi từng client, tối đa 128 ký tự, và hệ thống giữ lại vô thời hạn, nên một giá trị đã dùng thì không dùng lại cho job khác được.
Khi request bị timeout, phía bạn không biết máy chủ đã nhận job hay chưa. Gửi lại mà không có khóa này thì rất dễ tạo ra job thứ hai cho cùng một chuyến. Idempotency-Key là thứ làm cho việc gửi lại trở nên an toàn.
Cùng khóa và nội dung giống hệt lần trước: hệ thống trả về chính job đã tạo, không tạo thêm và không báo lỗi. Đây là điều làm cho việc gửi lại sau timeout an toàn.
Cùng khóa nhưng nội dung khác đi: hệ thống từ chối với 409 và mã IDEMPOTENCY_CONFLICT. Gặp mã này gần như chắc chắn là bạn đã dùng lại khóa cũ cho một job mới.
Nội dung gửi lên nhận trường external_reference, là mã định danh phía bạn đặt cho job. Nó khác Idempotency-Key: khóa bảo vệ một lần gửi cụ thể, còn external_reference gắn job với dữ liệu bên bạn. Mã định danh của từng đơn hàng và từng xe bên trong nội dung gửi lên là chuyện thứ ba nữa, và hai mã lỗi liên quan tới chúng nằm ở phần mã lỗi bên dưới.
const idempotencyKey = crypto.randomUUID();
async function submit(body: unknown) {
const response = await fetch(
"https://<your-host>/api/v1/partners/addons/route-optimization/jobs",
{
method: "POST",
headers: {
"api-key": process.env.ROUTE4GREEN_API_KEY!,
"Idempotency-Key": idempotencyKey,
"content-type": "application/json",
},
body: JSON.stringify(body),
},
);
// 202 Accepted. The job is recorded, not finished.
const envelope = await response.json();
if (!envelope.success) {
throw new Error(`${envelope.error_code}: ${envelope.message}`);
}
return envelope.data;
}Job đi qua sáu trạng thái. Đây là chuỗi ký tự nguyên văn hệ thống trả về, hãy so khớp đúng chúng, đừng dịch hay đổi dạng chữ.
| Trạng thái | Kết thúc | Nghĩa là gì | Bạn nên làm gì |
|---|---|---|---|
| QUEUED | Không | Job đã được nhận, chưa bắt đầu xử lý. | Chờ tiếp. |
| PROCESSING | Không | Đang tính toán. | Chờ tiếp. |
| SUCCEEDED | Có | Đã tính xong toàn bộ. | Lấy kết quả. |
| PARTIAL | Có | Đã tính xong nhưng không phải mọi phần đều thành công. | Lấy kết quả và kiểm tra phần nào chưa được xếp trước khi dùng. |
| FAILED | Có | Job dừng vì lỗi. | Đọc thông tin lỗi trước khi gửi lại, và nhớ dùng khóa mới nếu đây là một lần gửi mới. |
| CANCELLED | Có | Job đã bị hủy. | Không chờ thêm. |
Danh sách này lấy từ mô tả của đội backend. Bản đặc tả đã lưu chưa công bố tập giá trị này, nên nếu bạn thấy một trạng thái khác, hãy báo lại cho chúng tôi.
Nếu chưa dùng webhook, bạn hỏi trạng thái theo chu kỳ và dừng lại khi job vào một trong bốn trạng thái kết thúc. Luôn đặt giới hạn cho vòng lặp: thời gian xử lý tối đa hiện tại là 30 phút, nên một vòng lặp không giới hạn sẽ chạy mãi khi có sự cố.
const TERMINAL = new Set([
"SUCCEEDED",
"PARTIAL",
"FAILED",
"CANCELLED",
]);
async function waitForJob(jobId: string, deadlineMs = 30 * 60 * 1000) {
const startedAt = Date.now();
while (Date.now() - startedAt < deadlineMs) {
const response = await fetch(
`https://<your-host>/api/v1/partners/addons/route-optimization/jobs/${jobId}`,
{ headers: { "api-key": process.env.ROUTE4GREEN_API_KEY! } },
);
const envelope = await response.json();
if (!envelope.success) {
throw new Error(String(envelope.error_code));
}
const status: string = envelope.data.status;
if (TERMINAL.has(status)) return status;
await new Promise((resolve) => setTimeout(resolve, 5_000));
}
throw new Error("Job did not reach a terminal state before the deadline");
}Thay vì hỏi liên tục, bạn đăng ký một địa chỉ để hệ thống gọi về khi job kết thúc. Đây là các endpoint webhook của riêng nhóm tối ưu tuyến, khác với nhóm webhook chung trong tài liệu này.
Đăng ký webhook cũng cần khóa API, và khóa đó hiện chưa tự tạo được. Xem phần xác thực ở trang tài liệu chính.
Địa chỉ nhận phải là HTTPS, và hệ thống chỉ gọi về khi job đã vào trạng thái kết thúc, không gọi ở QUEUED hay PROCESSING. Phản hồi khi đăng ký thành công trả về khóa ký một lần duy nhất, hãy lưu lại ngay.
Danh sách trả về không kèm khóa ký.
Một endpoint dùng cho cả ba việc. Tài liệu gốc chưa nói rõ khi đổi khóa thì khóa mới trả về ở đâu, nên hãy hỏi chúng tôi trước khi dựa vào điều đó.
| Mục | Giá trị |
|---|---|
| Giao thức | Chỉ HTTPS |
| Thời điểm gọi | Khi job vào trạng thái kết thúc |
| Header chữ ký | X-Smartway-Signature: sha256=<hex> |
| Header dấu thời gian | X-Smartway-Timestamp, tính bằng giây |
| Header mã sự kiện | X-Smartway-Event-Id |
| Header loại sự kiện | X-Smartway-Event |
| Chuỗi được ký | timestamp.eventId.eventType.rawBody |
| Thời gian chờ | 5 giây |
| Số webhook | 1 cho mỗi client |
| Địa chỉ nội bộ | Bị từ chối |
Chữ ký là HMAC-SHA256 dạng hex, tính trên bốn phần nối bằng dấu chấm theo đúng thứ tự này: dấu thời gian, mã sự kiện, loại sự kiện, rồi phần thân thô. Bỏ tiền tố sha256= trước khi so sánh, và so sánh bằng hàm chống dò thời gian.
Phải tính trên đúng chuỗi byte thô của request. Nếu bạn parse JSON rồi tạo lại chuỗi, thứ tự khóa sẽ khác đi và mọi chữ ký đều sai. Trong Next.js nghĩa là đọc bằng request.text() chứ không phải request.json().
import { createHmac, timingSafeEqual } from "node:crypto";
const SECRET = process.env.ROUTE4GREEN_WEBHOOK_SECRET!;
/**
* HMAC-SHA256 over the four delivery parts joined by dots, in this order.
* Anything else, including a different separator, produces a digest that
* never matches.
*/
function expectedSignature(
timestamp: string,
eventId: string,
eventType: string,
rawBody: string,
): string {
const preimage = `${timestamp}.${eventId}.${eventType}.${rawBody}`;
return createHmac("sha256", SECRET).update(preimage).digest("hex");
}
function safeEqualHex(a: string, b: string): boolean {
const left = Buffer.from(a, "hex");
const right = Buffer.from(b, "hex");
// Compare lengths first: timingSafeEqual throws on a mismatch.
return left.length === right.length && timingSafeEqual(left, right);
}
export async function POST(request: Request): Promise<Response> {
// Raw bytes, not a parsed object. Re-serialising JSON reorders keys and
// every signature then fails.
const rawBody = await request.text();
const timestamp = request.headers.get("x-smartway-timestamp") ?? "";
const eventId = request.headers.get("x-smartway-event-id") ?? "";
const eventType = request.headers.get("x-smartway-event") ?? "";
const received = (request.headers.get("x-smartway-signature") ?? "").replace(/^sha256=/, "");
if (!timestamp || !eventId || !eventType || !received) {
return new Response("missing signature headers", { status: 400 });
}
if (!safeEqualHex(received, expectedSignature(timestamp, eventId, eventType, rawBody))) {
return new Response("invalid signature", { status: 401 });
}
// Your own freshness window. No server-side replay window is documented,
// so choose one and reject anything older.
const ageSeconds = Math.abs(Date.now() / 1000 - Number(timestamp));
if (!Number.isFinite(ageSeconds) || ageSeconds > 300) {
return new Response("stale delivery", { status: 401 });
}
// Deliveries can repeat, so key your handler on eventId and make it safe to
// run twice. Acknowledge first: the delivery timeout is 5 seconds.
void handleTerminalEvent(eventId, eventType, JSON.parse(rawBody));
return new Response("ok");
}Hãy coi mỗi sự kiện có thể đến nhiều hơn một lần và viết bộ xử lý sao cho chạy lại không gây hậu quả. Trả lời thật nhanh rồi xử lý sau, vì thời gian chờ hiện tại chỉ 5 giây.
Mọi phản hồi đều nằm trong cùng một lớp bọc gồm success, message, error_code và data. Điều quan trọng nhất: mã trạng thái HTTP không phải lúc nào cũng là tín hiệu. Có trường hợp trả về HTTP 200 nhưng success là false. Luôn kiểm tra phần thân, đừng chỉ kiểm tra mã HTTP.
// error_code is a string on some responses and a number on others, so the
// only safe shape is a union. Never compare it numerically.
type Envelope<T> = {
success: boolean;
message: string;
error_code: string | number;
data: T;
};
const response = await fetch(url, options);
const envelope: Envelope<unknown> = await response.json();
// Branch on success first. Do NOT branch on response.ok alone, and do not
// branch on error_code alone: some failures arrive as HTTP 200 with success
// set to false, and a successful response can still carry error_code 0.
if (!envelope.success) {
handleFailure(String(envelope.error_code), envelope.message);
return;
}
use(envelope.data);error_code có thể là chuỗi hoặc số tùy phản hồi: hệ thống thật đã trả về cả error_code là 0 lẫn error_code là một chuỗi. Hãy khai kiểu là string | number, đừng so sánh bằng phép so sánh số, và đừng rẽ nhánh chỉ dựa vào error_code. Rẽ nhánh theo success trước, rồi mới đọc mã.
Đây là lỗi ở phía hệ thống gốc chứ không phải quy ước. Chúng tôi ghi lại để bạn viết mã chịu được cả hai kiểu, và sẽ cập nhật khi phía hệ thống thống nhất lại.
Mã lỗi nằm trong trường error_code của phần thân phản hồi.
| Mã | Khi nào gặp và nên làm gì |
|---|---|
| ROUTE_OPT_API_KEY_REQUIRED | Request không mang khóa API. Thêm header api-key. |
| ROUTE_OPT_NOT_ENABLED | Khóa chưa có Quyền dùng API tối ưu tuyến, hoặc tính năng đang tắt ở phía máy chủ. Liên hệ chúng tôi, đây không phải thứ bạn tự bật được. |
| ROUTE_OPT_VALIDATION_ERROR | Nội dung gửi lên không hợp lệ. Đọc message để biết trường nào. |
| DUPLICATE_EXTERNAL_ID | Trong cùng một request có hai mục mang trùng mã định danh. Đây là lỗi 400, phát hiện lúc kiểm tra nội dung gửi lên. |
| ROUTE_OPT_RATE_LIMITED | Vượt số lần gửi cho phép trong một phút. Chờ rồi thử lại, và dùng lại đúng Idempotency-Key cũ. |
| ROUTE_OPT_PAYLOAD_TOO_LARGE | Nội dung vượt dung lượng tối đa hiện tại. Chia nhỏ rồi gửi lại. |
| ROUTE_OPT_JOB_NOT_FOUND | Không có job nào ứng với mã này. |
| ROUTE_OPT_WEBHOOK_NOT_FOUND | Không có webhook nào ứng với mã này. |
| ROUTE_OPT_PERSISTENCE_ERROR | Lỗi phía hệ thống khi lưu dữ liệu. Liên hệ chúng tôi nếu lặp lại. |
Hai mã này nghe giống nhau nhưng nói về hai chuyện hoàn toàn khác. Đọc kỹ trước khi viết nhánh xử lý.
DUPLICATE_EXTERNAL_ID, lỗi 400: nội dung bạn gửi tự mâu thuẫn. Hai mục trong cùng một request mang trùng mã định danh. Sửa ở phía bạn rồi gửi lại.
ACTIVE_EXTERNAL_ID_CONFLICT, lỗi 409: mã định danh đó đang bận. Một đơn hàng hoặc một xe với mã đó đang gắn với một job khác còn hoạt động. Chờ job kia kết thúc, hoặc dùng mã khác.
Các mã trả về cùng trạng thái 409. Cột nguồn cho biết mã nào đã được đội phát triển xác nhận trong mã nguồn, mã nào mới chỉ có trong bản đặc tả. Với nhóm thứ hai, hãy hỏi lại chúng tôi trước khi viết nhánh xử lý riêng.
| Mã | Endpoint | Nguồn |
|---|---|---|
| IDEMPOTENCY_CONFLICT | POST /jobs | Đã xác nhận |
| ACTIVE_EXTERNAL_ID_CONFLICT | POST /jobs | Đã xác nhận |
| JOB_ALREADY_PROCESSING | POST /jobs | Theo đặc tả |
| RESULT_NOT_READY | GET /jobs/{jobId}/result | Theo đặc tả |
| RESULT_UNAVAILABLE | GET /jobs/{jobId}/result | Theo đặc tả |
| WEBHOOK_LIMIT_REACHED | POST /webhooks | Theo đặc tả |
Đây là các giới hạn áp dụng cho khóa chính thức. Chúng là giới hạn về hạ tầng, không phải trần cứng của sản phẩm.
| Giới hạn | Khóa chính thức |
|---|---|
| Dung lượng nội dung gửi lên | 5 MB |
| Số xe trong một job | 200 |
| Số lần gửi job mỗi phút | 10 cho mỗi client |
| Số điểm giao, chặng đầu và chặng cuối | 320 (ngưỡng mềm) |
| Số điểm giao, chặng giữa | 220 (ngưỡng mềm) |
| Thời gian xử lý tối đa | 30 phút |
| Thời gian chờ webhook | 5 giây |
| Số webhook mỗi client | 1 |
Hai con số về số điểm giao là ngưỡng mềm: vượt qua thì thời gian tính toán tăng lên đáng kể chứ không bị từ chối ngay.
Khóa thử nghiệm chạy dưới các mức dưới đây, và vượt quá là bị từ chối thẳng chứ không phải chạy chậm lại. Mức này đủ để dựng và kiểm thử trọn vẹn một tích hợp, và cố ý không đủ để chạy vận hành thật.
| Giới hạn | Khóa thử nghiệm |
|---|---|
| Số lần gửi job mỗi phút | 5 |
| Số xe trong một job | 10 |
| Số điểm dừng trong một job | 50 |
| Số job mỗi tháng | 200 |
Khóa thử nghiệm chỉ chạy được bài toán chặng cuối. Đây là quyết định về phạm vi dùng thử chứ không phải giới hạn kỹ thuật; các bài toán còn lại mở khi bạn dùng khóa chính thức.
Nếu khối lượng của bạn vượt các con số này, hãy nói với chúng tôi. Đây là việc mở rộng hạ tầng và chúng tôi xử lý theo từng trường hợp, nên cứ mô tả quy mô thật của bạn.
Trao đổi về hạn mứcChúng tôi không đưa ra cam kết về thời gian phản hồi hay tỷ lệ sẵn sàng. Hệ thống đang trong giai đoạn chạy thử với một khách hàng và chờ nghiệm thu, nên phần cam kết dịch vụ sẽ chỉ có sau khi hoàn tất.
7 endpoint
/addons/route-optimization/jobsList visible jobs for the API-key client/addons/route-optimization/jobsSubmit an asynchronous route optimization job/addons/route-optimization/jobs/{jobId}Get public job status and lifecycle metadata/addons/route-optimization/jobs/{jobId}/resultGet a terminal public optimization result/addons/route-optimization/webhooksList webhooks without secret material/addons/route-optimization/webhooksRegister an HTTPS terminal-event webhook/addons/route-optimization/webhooks/{webhookId}Activate, deactivate, or rotate a webhook secret