Tích hợp Nhanh.vn Open API v3 bằng PHP cho WordPress, WooCommerce

Chào anh em! Khách dùng Nhanh.vn POS cho chuỗi cửa hàng, web chạy WooCommerce, nhờ bạn “nối API để tồn kho trên web khớp với Nhanh”. Đọc tài liệu Open API thì gặp ngay mấy chỗ dễ vấp: tài liệu vừa đổi tên miền, có hai phiên bản v2 và v3, token sống một năm mà không có refresh token, mỗi URL chỉ được 150 request trong 30 giây. Đọc hết bài, anh em tự tích hợp được API Nhanh.vn bằng PHP cho WordPress, từ tạo app tới code kéo sản phẩm chạy nền theo con trỏ next.

Tích hợp Nhanh.vn Open API v3 bằng PHP cho website WordPress, WooCommerce

Endpoint, tham số và mã lỗi trong bài lấy từ tài liệu chính thức tại developers.nhanh.group (bản cũ apidocs.nhanh.vn vẫn mở), mình đọc ngày 11/10/2026; giao diện open.nhanh.vn kiểm cùng ngày và có thể đổi sau này. Plugin nhắc ở cuối bài là sản phẩm mình viết, dùng Open API của Nhanh.vn như mọi ứng dụng bên ngoài khác. Code bên dưới là mẫu tối giản để anh em nắm luồng, chạy thử trên bản sao website trước khi đưa lên thật. Trong các trang mình đọc, tài liệu không nhắc môi trường thử (sandbox) riêng, nên API ghi dữ liệu (thêm đơn, sửa sản phẩm) sẽ đụng vào dữ liệu thật của doanh nghiệp.

Nhanh.vn Open API là gì, tài liệu nằm ở đâu?

Open API là bộ API để hệ thống khác đồng bộ sản phẩm, đơn hàng, tồn kho, khách hàng, hoá đơn… với Nhanh.vn. Theo trang giới thiệu tài liệu, Open API hiện miễn phí và dùng được khi doanh nghiệp có gói dịch vụ của Nhanh. Mấy địa chỉ cần nhớ:

Để duy trì blog nên mình có làm aff cho 1 số bên hosting. Nhưng dù aff mình cũng chọn 1 số nhà cung cấp uy tín về chất lượng và support nên các bạn cứ yên tâm nhé.

Nếu có mua hosting mà có trong list dưới đây các bạn click vào link trước khi mua để ủng hộ mình nhé. Mình cảm ơn nhiều

  • Tài liệu cho developer từ 23/09/2026 chuyển sang developers.nhanh.group. Trang cũ apidocs.nhanh.vn vẫn mở, có bản Markdown của từng trang (thêm .md vào cuối đường dẫn) và mục lục llms.txt, tiện để tra hoặc đưa cho công cụ AI đọc.
  • App được tạo và quản lý ở open.nhanh.vn; tài khoản developer ở đây chỉ để tạo, sửa app, không cần quyền gì trên dữ liệu của shop.
  • Máy chủ API POS là https://pos.open.nhanh.vn, còn Vpage có tên miền riêng vpage.open.nhanh.vn.
  • Nhanh có Postman collection v3.0 công khai, và mỗi trang tài liệu đều kèm lệnh curl mẫu để import thử.

Về phiên bản: Nhanh.vn API v3 ra mắt từ 31/08/2025; tài liệu ghi v2.0 đã ngừng phát triển tính năng mới và ngừng hỗ trợ sau 30/11/2026. Riêng phần webhook, form tạo app trên open.nhanh.vn (mình xem ngày 11/10/2026) ghi “version 2.0 (Ngừng hỗ trợ sau 31-08-2026)”. Việc mới thì viết thẳng theo v3, hệ thống đang chạy v2 thì lên lịch chuyển.

Các nhóm API v3 hay dùng khi nối website: product (list, detail, inventory, category, add, edit…), order (list, add, edit…), customer, business/depot (danh sách kho), shipping (địa chỉ, phí, hãng vận chuyển) và webhooks.

Bước 1: tạo app trên open.nhanh.vn

Đăng nhập open.nhanh.vn bằng tài khoản developer, vào Danh sách app, bấm Thêm mới. Các trường theo tài liệu và form hiện tại:

  • Tên App hiện cho người dùng thấy lúc cấp quyền.
  • Trạng thái chuyển sang Ngừng hoạt động thì app không gọi được API và không nhận webhook nữa.
  • Redirect URL là đường dẫn trên hệ thống của bạn; người dùng cấp quyền xong, Nhanh chuyển về đây kèm accessCode. Bắt buộc chạy https.
  • Nhóm ô Bật webhooks, Webhooks version, Webhooks callback URL và Webhooks verify token (16–128 ký tự gồm chữ và số) là phần nhận dữ liệu Nhanh gửi về website.
Form Thêm mới app trên open.nhanh.vn gồm Tên App, Trạng thái, Redirect URL, Bật webhooks, Webhooks version 3.0 và danh sách sự kiện
Form Thêm mới app, giá trị mẫu tenmien.com. Ảnh: giao diện open.nhanh.vn, 10/2026.

Phía doanh nghiệp, tài khoản Giám đốc phải bật “Cho phép kết nối Open API” ở Cài đặt chung → Cài đặt Open API trên Nhanh.vn; chưa bật thì API trả ERR_BUSINESS_NOT_ENABLE_API. Từng bước bấm, chỗ lấy App ID và Secret Key có ở bài lấy access token Nhanh.vn. Bật webhook, các sự kiện v3 và code nhận trong WordPress có ở bài webhook Nhanh.vn.

Bước 2: đổi accessCode lấy accessToken (v3)

Nhanh dùng luồng ba bước giống OAuth:

  1. Chuyển người dùng tới https://nhanh.vn/oauth?appId=…&returnLink=…. returnLink phải chạy https và nằm trong Redirect URL của app.
  2. Người dùng đăng nhập Nhanh.vn, chọn quyền (và kho) cấp cho app, bấm Đồng ý. Nhanh chuyển về returnLink?accessCode=….
  3. Máy chủ của bạn gọi POST https://pos.open.nhanh.vn/v3.0/app/getaccesstoken?appId=… với body JSON gồm accessCode và secretKey. Phản hồi có accessToken, expiredAt (Unix timestamp), businessId, depotIds, pageIds, permissions.
  • accessCode sống 10 phút và hết hiệu lực ngay khi đổi token thành công.
  • ⚠️ accessToken hạn 1 năm kể từ lúc tạo, Nhanh chưa hỗ trợ refresh token. Hệ thống phải tự nhắc người dùng cấp quyền lại trước hạn.
  • v3 chỉ giữ một token cho mỗi cặp appId + businessId: tạo token mới thì token cũ còn hiệu lực thêm 15 phút rồi thôi.
  • Quyền và kho của token cố định lúc cấp. Tài khoản nhân viên chỉ chọn được những quyền tài khoản đó đang có; sau này được phân thêm quyền trên Nhanh thì token cũ vẫn không có, phải cấp quyền lại.
  • Kiểm token bằng /v3.0/app/checkaccesstoken: trả hạn, quyền, kho và page được thao tác.

Cấu hình để trong wp-config.php. DEVVN_NHANH_BUSINESS_ID để trống lần đầu cũng được; có ID doanh nghiệp rồi thì điền vào để code từ chối token của doanh nghiệp khác:

define( 'DEVVN_NHANH_APP_ID', 'YOUR_APP_ID' );
define( 'DEVVN_NHANH_SECRET_KEY', 'YOUR_SECRET_KEY' );
define( 'DEVVN_NHANH_BUSINESS_ID', '' );

Đoạn dưới gồm nút kết nối trong trang quản trị (kiểm quyền, nonce rồi đặt cờ chờ 10 phút) và route nhận accessCode, đổi token, lưu vào option không autoload:

/**
 * Đường dẫn Nhanh.vn chuyển về sau khi cấp quyền; khai đúng chuỗi này ở Redirect URL của app.
 *
 * @return string
 */
function devvn_nhanh_return_link() {
	return rest_url( 'devvn-nhanh/v1/oauth' );
}

/**
 * Nút "Kết nối Nhanh.vn": kiểm quyền và nonce, đặt cờ chờ rồi chuyển sang trang cấp quyền của Nhanh.
 *
 * @return void
 */
function devvn_nhanh_connect() {
	if ( ! current_user_can( 'manage_options' ) ) {
		wp_die( 'Bạn không có quyền thực hiện thao tác này.' );
	}
	check_admin_referer( 'devvn_nhanh_connect' );
	set_transient( 'devvn_nhanh_oauth_pending', get_current_user_id(), 10 * MINUTE_IN_SECONDS );
	$url = 'https://nhanh.vn/oauth?' . http_build_query(
		array(
			'appId'      => DEVVN_NHANH_APP_ID,
			'returnLink' => devvn_nhanh_return_link(),
		)
	);
	wp_redirect( $url );
	exit;
}
add_action( 'admin_post_devvn_nhanh_connect', 'devvn_nhanh_connect' );

/**
 * Đăng ký route GET nhận accessCode Nhanh.vn chuyển về.
 *
 * @return void
 */
function devvn_nhanh_register_oauth_route() {
	register_rest_route(
		'devvn-nhanh/v1',
		'/oauth',
		array(
			'methods'             => 'GET',
			'callback'            => 'devvn_nhanh_oauth_return',
			'permission_callback' => '__return_true',
		)
	);
}
add_action( 'rest_api_init', 'devvn_nhanh_register_oauth_route' );

/**
 * Đổi accessCode lấy accessToken v3, lưu token rồi chuyển về trang quản trị kèm kết quả.
 *
 * @param WP_REST_Request $request Request trình duyệt bị Nhanh.vn chuyển về.
 * @return WP_REST_Response
 */
function devvn_nhanh_oauth_return( WP_REST_Request $request ) {
	$code   = sanitize_text_field( (string) $request->get_param( 'accessCode' ) );
	$result = 'error';
	if ( '' !== $code && get_transient( 'devvn_nhanh_oauth_pending' ) ) {
		delete_transient( 'devvn_nhanh_oauth_pending' );
		$res  = wp_remote_post(
			add_query_arg( 'appId', DEVVN_NHANH_APP_ID, 'https://pos.open.nhanh.vn/v3.0/app/getaccesstoken' ),
			array(
				'timeout' => 15,
				'headers' => array( 'Content-Type' => 'application/json' ),
				'body'    => wp_json_encode(
					array(
						'accessCode' => $code,
						'secretKey'  => DEVVN_NHANH_SECRET_KEY,
					)
				),
			)
		);
		$json = is_wp_error( $res ) ? null : json_decode( wp_remote_retrieve_body( $res ), true );
		if ( isset( $json['code'], $json['data']['accessToken'] ) && 1 === (int) $json['code'] ) {
			$data = wp_parse_args(
				$json['data'],
				array(
					'expiredAt'   => 0,
					'businessId'  => 0,
					'depotIds'    => array(),
					'permissions' => array(),
				)
			);
			$want = (string) DEVVN_NHANH_BUSINESS_ID;
			if ( '' === $want || (string) $data['businessId'] === $want ) {
				update_option(
					'devvn_nhanh_token',
					array(
						'token'       => $data['accessToken'],
						'expired_at'  => (int) $data['expiredAt'],
						'business_id' => (int) $data['businessId'],
						'depot_ids'   => $data['depotIds'],
						'permissions' => $data['permissions'],
					),
					false
				);
				$result = 'ok';
			}
		}
	}
	return new WP_REST_Response( null, 302, array( 'Location' => add_query_arg( 'devvn_nhanh', $result, admin_url() ) ) );
}
  • Nút kết nối là một link tới wp_nonce_url( admin_url( 'admin-post.php?action=devvn_nhanh_connect' ), 'devvn_nhanh_connect' ), đặt ở trang cài đặt của bạn.
  • Redirect URL khai trên app phải đúng chuỗi devvn_nhanh_return_link() trả về, ví dụ https://ten-mien-cua-ban/wp-json/devvn-nhanh/v1/oauth.
  • Route phải công khai vì trình duyệt bị Nhanh chuyển về, không mang nonce REST. Mình chặn bằng hai lớp: cờ chờ chỉ admin bấm nút mới đặt được (sống 10 phút, bằng tuổi accessCode), và so businessId trả về với ID doanh nghiệp đã điền. Tài liệu không nhắc tham số state như OAuth chuẩn, nên đừng dựa vào nó.
  • secretKey và accessToken chỉ nằm ở máy chủ: không đưa vào JavaScript, không ghi ra log, không commit lên Git.
  • Lưu expired_at để nhắc người dùng cấp quyền lại trước hạn, đừng đợi tới lúc API báo lỗi token.

Bước 3: quy ước gọi Nhanh.vn API v3

Mọi API v3 dùng POST với body JSON thô; appId và businessId gắn trên đường dẫn, accessToken nằm ở header Authorization (ví dụ trong tài liệu gửi nguyên token, không có tiền tố Bearer). Phản hồi luôn có trường code:

  • "code": 1: thành công, kèm data; tuỳ API có thêm paginator và warning (ví dụ báo trường sắp bỏ).
  • "code": 0: lỗi, kèm errorCode, messages (chuỗi, hoặc mảng theo từng trường) và có thể có data.
errorCodeÝ nghĩa, cách xử lý
ERR_INVALID_APP_IDappId sai.
ERR_INVALID_ACCESS_TOKENToken sai, hết hạn hoặc đã bị token mới thay.
ERR_INVALID_VERSIONLấy token v2 gọi API v3. Cấp quyền lại để có token v3.
ERR_INVALID_BUSINESS_IDbusinessId sai.
ERR_INVALID_DEPOT_IDLúc cấp quyền không chọn kho này. Cấp lại, chọn đủ kho.
ERR_BUSINESS_NOT_ENABLE_APIDoanh nghiệp chưa bật Cho phép kết nối Open API.
ERR_INVALID_FORM_FIELDSTrường dữ liệu sai; messages chỉ rõ trường nào.
ERR_INVALID_DATABody không phải JSON hợp lệ, thường do tự ghép chuỗi thay vì dùng hàm encode.
ERR_403Token không có quyền với dữ liệu này. Cấp lại và chọn quyền đó.
ERR_429, ERR_EXCEEDED_RATE_LIMITVượt rate limit, xem mục dưới.

Rẽ nhánh theo errorCode, đừng so câu chữ trong messages. Cần kỹ thuật Nhanh hỗ trợ API thì theo trang Hỗ trợ kỹ thuật: gửi email dev@nhanh.vn kèm tên doanh nghiệp, businessId và Code Snippet dạng cURL xuất từ Postman (nhớ che token).

Rate limit 150 request/30 giây và lỗi ERR_429

  • Mức mặc định 150 request trong 30 giây, tính theo appId + businessId + URL API. Bị khoá ở URL này vẫn gọi được URL khác; app dùng cho nhiều doanh nghiệp thì khoá theo từng doanh nghiệp. API nào có mức riêng thì trang của API đó ghi rõ.
  • Vượt mức thì nhận errorCode: ERR_429 kèm data.lockedSeconds (số giây bị khoá) và data.unlockedAt (thời điểm mở khoá, Unix timestamp).
  • 🚨 Gọi tiếp khi đang bị khoá làm lockedSeconds và unlockedAt tăng thêm. Nhanh không mở khoá theo yêu cầu, và cố tình dùng nhiều app để gọi dồn thì toàn bộ các app bị khoá.
  • Khuyến cáo của tài liệu: lưu dữ liệu ở hệ thống của bạn, nhận thay đổi qua webhook, khi gọi API chỉ lấy phần thay đổi bằng updatedAtFrom / updatedAtTo.
{
  "code": 0,
  "errorCode": "ERR_429",
  "message": "Your app exceeded the API Rate Limit",
  "data": {
    "lockedSeconds": 10,
    "unlockedAt": 1733387520
  }
}

Để ý mẫu lỗi này dùng message (số ít) chứ không phải messages. Tài liệu cũng không ghi mã HTTP đi kèm, nên hàm gọi chung dưới đây bắt cả errorCode lẫn HTTP 429. Hàm đọc token đã lưu ở bước 2, trả mảng phản hồi khi code bằng 1, còn lại trả WP_Error có mã là errorCode:

define( 'DEVVN_NHANH_API_HOST', 'https://pos.open.nhanh.vn/' );
define( 'DEVVN_NHANH_TIMEOUT', 15 );
define( 'DEVVN_NHANH_DEFAULT_LOCK', 10 );

/**
 * Gọi một API v3 của Nhanh.vn; URL đang bị khoá rate limit thì trả lỗi ngay, không gọi sang Nhanh.
 *
 * @param string $path Đường dẫn API, ví dụ 'v3.0/product/list'.
 * @param array  $body Body JSON gửi đi.
 * @return array|WP_Error Phản hồi khi code = 1, hoặc lỗi mang mã errorCode (lỗi 429 có retry_after).
 */
function devvn_nhanh_request( $path, array $body = array() ) {
	$auth = get_option( 'devvn_nhanh_token', array() );
	if ( empty( $auth['token'] ) ) {
		return new WP_Error( 'nhanh_no_token', 'Chưa có accessToken, cần kết nối lại Nhanh.vn.' );
	}
	$lock_key = 'devvn_nhanh_lock_' . md5( $path );
	$unlock   = (int) get_transient( $lock_key );
	if ( $unlock > time() ) {
		return new WP_Error( 'ERR_429', 'URL đang bị khoá rate limit.', array( 'retry_after' => $unlock - time() ) );
	}
	$url = add_query_arg(
		array(
			'appId'      => DEVVN_NHANH_APP_ID,
			'businessId' => $auth['business_id'],
		),
		DEVVN_NHANH_API_HOST . ltrim( $path, '/' )
	);
	$res = wp_remote_post(
		$url,
		array(
			'timeout' => DEVVN_NHANH_TIMEOUT,
			'headers' => array(
				'Authorization' => $auth['token'],
				'Content-Type'  => 'application/json',
			),
			'body'    => wp_json_encode( empty( $body ) ? new stdClass() : $body ),
		)
	);
	if ( is_wp_error( $res ) ) {
		return $res;
	}
	$http = (int) wp_remote_retrieve_response_code( $res );
	$json = json_decode( wp_remote_retrieve_body( $res ), true );
	if ( ! is_array( $json ) ) {
		return new WP_Error( 'nhanh_http_' . $http, 'Phản hồi không phải JSON.', array( 'status' => $http ) );
	}
	if ( isset( $json['code'] ) && 1 === (int) $json['code'] ) {
		return $json;
	}
	$error = isset( $json['errorCode'] ) ? (string) $json['errorCode'] : 'nhanh_http_' . $http;
	if ( 429 === $http || in_array( $error, array( 'ERR_429', 'ERR_EXCEEDED_RATE_LIMIT' ), true ) ) {
		$wait  = isset( $json['data']['lockedSeconds'] ) ? (int) $json['data']['lockedSeconds'] : DEVVN_NHANH_DEFAULT_LOCK;
		$until = isset( $json['data']['unlockedAt'] ) ? (int) $json['data']['unlockedAt'] : time() + $wait;
		$until = max( $until, time() + 1 );
		set_transient( $lock_key, $until, $until - time() );
		return new WP_Error( 'ERR_429', 'Nhanh.vn đang khoá rate limit.', array( 'retry_after' => $until - time() ) );
	}
	$msg = isset( $json['messages'] ) ? $json['messages'] : ( isset( $json['message'] ) ? $json['message'] : '' );
	return new WP_Error( $error, is_array( $msg ) ? wp_json_encode( $msg ) : (string) $msg, array( 'status' => $http ) );
}
  • Đang bị khoá thì hàm không gọi sang Nhanh mà trả lỗi ngay, để khoá không bị kéo dài. Lỗi mang retry_after (giây) để job chạy nền hẹn lại đúng lúc.
  • Request từ trang sản phẩm, giỏ hàng gặp ERR_429 thì dùng dữ liệu đã lưu để khách khỏi phải chờ; chỉ job nền mới hẹn gọi lại.
  • Body rỗng gửi {}, không gửi []. Body là mảng danh sách (như product/add nhận mảng sản phẩm) thì giữ nguyên dạng mảng.
  • Timeout đặt rõ 15 giây vì mặc định của wp_remote_post() chỉ 5 giây, dễ cắt ngang các API trả nhiều dữ liệu.
  • Muốn chủ động hơn thì tự đếm request theo URL và giữ dưới mức 150/30 giây, thay vì đợi tới lúc bị khoá.

Phân trang bằng paginator.next: kéo hết sản phẩm không sót

  • Request gửi paginator gồm size (mặc định tối đa 100, API nào cho lớn hơn thì ghi riêng), sort (với product/list là sắp theo id, asc hoặc desc) và next.
  • Trang đầu không gửi next. Các trang sau gửi lại đúng cấu trúc next vừa nhận: nhận {"next":{"id":"xxx"}} thì gửi "paginator":{"next":{"id":"xxx"}}, không rút gọn thành "next":"xxx". Mỗi API, thậm chí mỗi trang, có thể trả next khác kiểu.
  • Hết dữ liệu khi không có next hoặc next là null. Trang trả ít bản ghi hơn size mà vẫn có next thì vẫn còn trang sau (dữ liệu bị xoá hoặc bị lọc ẩn).
  • product/list tối đa 100 sản phẩm mỗi trang, lọc được theo ids (tối đa 100), name, parentId, gpid, categoryIds, status, price, brandIds, imei, updatedAtFrom, updatedAtTo.
  • ⚠️ updatedAt chỉ đổi khi thông tin sản phẩm (tên, giá…) đổi, không tính thay đổi tồn kho. Tồn kho phải nhận qua webhook inventoryChange. Tồn trả về cũng chỉ gồm các kho token được cấp, nên thấy thiếu tồn so với giao diện Nhanh thì kiểm lại kho đã chọn lúc cấp quyền.

Đoạn dưới kéo sản phẩm có thay đổi theo kiểu chạy nền: Action Scheduler (đi kèm WooCommerce) gọi 15 phút một lần, mỗi lượt chạy tối đa 20 giây, con trỏ next và mốc thời gian được nhớ trong option để lượt sau chạy tiếp. Quét xong một vòng thì mốc cuối thành updatedAtFrom của vòng sau, nên chỉ vòng khởi đầu mới đi hết danh sách. Sản phẩm trên web khớp với Nhanh qua meta _devvn_nhanh_id:

define( 'DEVVN_NHANH_META', '_devvn_nhanh_id' );
define( 'DEVVN_NHANH_SYNC_HOOK', 'devvn_nhanh_sync_products' );
define( 'DEVVN_NHANH_SYNC_INTERVAL', 15 * MINUTE_IN_SECONDS );
define( 'DEVVN_NHANH_SYNC_BUDGET', 20 );
define( 'DEVVN_NHANH_PAGE_SIZE', 100 );

/**
 * Hẹn lịch kéo sản phẩm định kỳ bằng Action Scheduler; chỉ kiểm ở trang quản trị cho nhẹ trang khách.
 *
 * @return void
 */
function devvn_nhanh_schedule_sync() {
	if ( function_exists( 'as_has_scheduled_action' ) && ! as_has_scheduled_action( DEVVN_NHANH_SYNC_HOOK ) ) {
		as_schedule_recurring_action( time(), DEVVN_NHANH_SYNC_INTERVAL, DEVVN_NHANH_SYNC_HOOK );
	}
}
add_action( 'admin_init', 'devvn_nhanh_schedule_sync' );

/**
 * Kéo các trang sản phẩm có thay đổi trong giới hạn thời gian, nhớ con trỏ next để lượt sau chạy tiếp.
 *
 * @return void
 */
function devvn_nhanh_sync_products() {
	$state = wp_parse_args(
		get_option( 'devvn_nhanh_product_sync', array() ),
		array(
			'from' => 0,
			'to'   => 0,
			'next' => null,
		)
	);
	$state['to'] = $state['to'] ? (int) $state['to'] : time();
	$deadline    = time() + DEVVN_NHANH_SYNC_BUDGET;
	do {
		$filters = array( 'updatedAtTo' => (int) $state['to'] );
		if ( $state['from'] ) {
			$filters['updatedAtFrom'] = (int) $state['from'];
		}
		$paginator = array(
			'size' => DEVVN_NHANH_PAGE_SIZE,
			'sort' => array( 'id' => 'asc' ),
		);
		if ( ! empty( $state['next'] ) ) {
			$paginator['next'] = $state['next'];
		}
		$res = devvn_nhanh_request(
			'v3.0/product/list',
			array(
				'filters'   => $filters,
				'paginator' => $paginator,
			)
		);
		if ( is_wp_error( $res ) ) {
			wc_get_logger()->warning( $res->get_error_code() . ': ' . $res->get_error_message(), array( 'source' => 'devvn-nhanh' ) );
			break;
		}
		devvn_nhanh_save_products( isset( $res['data'] ) ? (array) $res['data'] : array() );
		$state['next'] = isset( $res['paginator']['next'] ) ? $res['paginator']['next'] : null;
		if ( empty( $state['next'] ) ) {
			$state = array(
				'from' => $state['to'],
				'to'   => 0,
				'next' => null,
			);
			break;
		}
	} while ( time() < $deadline );
	update_option( 'devvn_nhanh_product_sync', $state, false );
}
add_action( DEVVN_NHANH_SYNC_HOOK, 'devvn_nhanh_sync_products' );

/**
 * Tìm ID sản phẩm, biến thể WooCommerce theo ID Nhanh.vn bằng một truy vấn.
 *
 * @param array $nhanh_ids Danh sách ID sản phẩm trên Nhanh.vn.
 * @return array Mảng ID Nhanh => ID WooCommerce.
 */
function devvn_nhanh_map_products( array $nhanh_ids ) {
	if ( empty( $nhanh_ids ) ) {
		return array();
	}
	$post_ids = get_posts(
		array(
			'post_type'      => array( 'product', 'product_variation' ),
			'post_status'    => 'any',
			'fields'         => 'ids',
			'posts_per_page' => -1,
			'no_found_rows'  => true,
			'meta_query'     => array(
				array(
					'key'     => DEVVN_NHANH_META,
					'value'   => array_map( 'strval', $nhanh_ids ),
					'compare' => 'IN',
				),
			),
		)
	);
	update_meta_cache( 'post', $post_ids );
	$map = array();
	foreach ( $post_ids as $post_id ) {
		$map[ (string) get_post_meta( $post_id, DEVVN_NHANH_META, true ) ] = (int) $post_id;
	}
	return $map;
}

/**
 * Ghi giá bán lẻ của một trang sản phẩm Nhanh.vn vào các sản phẩm WooCommerce đã khớp ID.
 *
 * @param array $rows Phần data của /v3.0/product/list.
 * @return void
 */
function devvn_nhanh_save_products( array $rows ) {
	$rows = array_column( $rows, null, 'id' );
	$map  = devvn_nhanh_map_products( array_keys( $rows ) );
	_prime_post_caches( array_values( $map ), false, true );
	foreach ( $map as $nhanh_id => $post_id ) {
		$product = wc_get_product( $post_id );
		$price   = isset( $rows[ $nhanh_id ]['prices']['retail'] ) ? (float) $rows[ $nhanh_id ]['prices']['retail'] : 0;
		if ( ! $product || $product->is_type( 'variable' ) || $price <= 0 || (float) $product->get_regular_price() === $price ) {
			continue;
		}
		$product->set_regular_price( wc_format_decimal( $price ) );
		$product->save();
	}
}
  • Lần khớp sản phẩm ban đầu anh em có thể ghép theo SKU (trường code của Nhanh) rồi lưu ID Nhanh vào meta _devvn_nhanh_id; từ đó mọi lượt đều tra theo ID.
  • Cả trang 100 sản phẩm tra bằng một truy vấn meta, nạp sẵn cache rồi mới đọc, nên không có truy vấn nào trong vòng lặp. Chỉ sản phẩm có giá thay đổi mới bị ghi.
  • Với biến thể, code bỏ qua dòng cha (Nhanh dùng parentId -2 cho sản phẩm cha) và ghi giá vào từng biến thể.
  • Gặp lỗi (kể cả ERR_429) thì job chỉ ghi log ở WooCommerce → Trạng thái → Nhật ký, nguồn devvn-nhanh, rồi dừng lượt; lượt sau chạy tiếp từ con trỏ cũ. Xử lý lại một trang đã làm cũng không sinh dữ liệu sai, nên chạy lại là vô hại.
  • Muốn kéo thêm ảnh, mô tả, danh mục thì viết tiếp trong devvn_nhanh_save_products(). Tồn kho để webhook lo.

Chuyển từ API v2 sang v3: những gì phải sửa

Mụcv2.0v3.0
Gửi requestBody form-data, token nằm trong bodyBody JSON thô, token ở header Authorization
Đổi token/api/oauth/access_token/v3.0/app/getaccesstoken
Verify token webhookTrong body (webhooksVerifyToken)Header Authorization
Gọi từ JavaScriptBị chặnĐã mở CORS
Thêm đơnNhận ID sản phẩm của web và idNhanhChỉ dùng ID sản phẩm trên Nhanh
  • Bảng mã lỗi hiện tại ghi lấy token v2 gọi API v3 sẽ ra ERR_INVALID_VERSION, nên chuyển sang v3 là phải cấp quyền lại để có token v3. Trang “Nâng cấp lên v3.0” viết từ thời v3 Alpha còn nói token v2 dùng được cho v3; mình theo bảng mã lỗi.
  • Cấu trúc dữ liệu trả về (response) đổi hoàn toàn, phải đọc lại tài liệu từng API. Paginator mới hỗ trợ nhiều kiểu sắp xếp hơn.
  • Khoảng thời gian lọc ở một số API trước giới hạn 10 ngày, v3 nâng lên 31 ngày.
  • Với webhook, đổi Webhooks version trong app sang 3.0 và sửa code đọc verify token từ header. Payload v3 có cấu trúc giống dữ liệu của API danh sách nên đỡ phải gọi thêm.

Tự viết hay dùng plugin cho WooCommerce?

Gọi được API mới là phần dễ. Phần tốn công là mọi thứ xung quanh: giao diện kết nối và nhắc hạn token, khớp sản phẩm, biến thể, ảnh, hàng đợi webhook, điều tiết request, đăng đơn, phí ship ở trang thanh toán…

  • Tự viết hợp khi hệ thống không phải WooCommerce, hoặc chỉ cần một luồng lẻ (ví dụ đẩy đơn từ landing page sang Nhanh).
  • Dùng plugin hợp khi web là WooCommerce và cần chạy ngay.

Với plugin kết nối Nhanh.vn với WooCommerce của mình, những phần trong bài đã có sẵn:

  • Để kết nối, tab “Nhanh.vn” cho chọn Version 2 hoặc Version 3 và đưa sẵn Redirect URL, Webhooks callback URL để dán vào app; anh em nhập Business ID, App ID, Secret Key rồi bấm “Get accessCode” ở bước 9. Khung kết nối hiện dòng “Token hết hiệu lực vào” để biết lúc nào cần cấp lại; plugin không tự gia hạn token.
  • Plugin tự điều tiết API, giữ khoảng 120 trên 150 request mỗi 30 giây cho mỗi endpoint; gặp ERR_429 thì chờ hết khoá rồi gọi lại một lần.
  • Plugin nhận webhook, lưu vào hàng đợi, trả 200 rồi mới xử lý trong nền; trang Webhook Queue cho xem Chờ xử lý, Hoàn thành, Thất bại và bấm Retry.
  • Phần đồng bộ sản phẩm v3 lấy danh mục, sản phẩm, biến thể, ảnh (nhận theo phiên bản) và có chế độ đồng bộ trong nền. Đồng bộ một chiều từ Nhanh về web, không xoá sản phẩm trên web.
  • Dev có các hook như pre_data_create_order_v3, nhanhvn_webhook_default để tuỳ biến mà không sửa lõi. Danh sách ở trang hooks của plugin Nhanh.vn.
Tab Nhanh.vn của plugin WooCommerce: Redirect URL, chọn Version 3, Webhooks callback URL, ô Business ID, App ID, Secret Key, link Get accessCode và hạn token
Với plugin, phần OAuth là nhập ba ô rồi bấm Get accessCode. Dữ liệu mẫu, token đã che.

Phần kết nối trong plugin có hướng dẫn chi tiết ở trang kết nối API Nhanh.vn của tài liệu plugin. So sánh ba cách nối Nhanh.vn với website (website của Nhanh, tự code, plugin) có ở bài kết nối Nhanh.vn với website.

Câu hỏi thường gặp

Open API Nhanh.vn có tính phí không?

Theo trang giới thiệu tài liệu (đọc 10/2026), Open API hiện miễn phí; doanh nghiệp mua gói dịch vụ của Nhanh thì được dùng Open API.

Có môi trường sandbox để test không?

Trong các trang tài liệu mình đọc không thấy môi trường thử riêng. Nên thử các API đọc trước (product/list, business/depot), dùng Postman collection của Nhanh để xem tham số, và chỉ chạy API ghi khi đã chắc dữ liệu gửi đi.

Gọi API Nhanh.vn bằng JavaScript ở trình duyệt được không?

Tài liệu ghi v3 đã mở CORS. Nhưng gọi từ trình duyệt là lộ accessToken cho bất kỳ ai xem mã trang, và token này sống tới một năm. Với website, hãy gọi từ máy chủ như các đoạn PHP ở trên.

Token v2 có gọi được API v3 không?

Không. Theo bảng mã lỗi, lấy token v2 gọi API v3 sẽ nhận ERR_INVALID_VERSION. Cấp quyền lại để lấy token v3; với plugin là chọn Version 3 rồi bấm Get accessCode lần nữa.

Anh em tích hợp vướng chỗ nào cứ để lại bình luận, nhớ che accessToken, Secret Key và businessId trước khi dán log nhé. Chúc vòng quét khởi đầu đi hết danh sách sản phẩm mà không dính lần ERR_429 nào! 🚀

Bài viết trong series

Kết nối WooCommerce với Nhanh.vn

0/8đã đọc
  1. 1 Lấy access token Nhanh.vn: tạo app, App ID, Secret Key và gia hạn Đang đọc Đã đọc
  2. 2 Kết nối Nhanh.vn với website: 3 cách và 5 bước nối WooCommerce Đang đọc Đã đọc
  3. 3 Đồng bộ tồn kho Nhanh.vn với website WooCommerce qua webhook Đang đọc Đã đọc
  4. 4 Đồng bộ sản phẩm Nhanh.vn về website WooCommerce trong 3 bước Đang đọc Đã đọc
  5. 5 Đăng đơn lên Nhanh.vn từ WooCommerce: thủ công, hàng loạt, tự động Đang đọc Đã đọc
  6. 6 Hiển thị tồn kho theo cửa hàng WooCommerce bằng dữ liệu Nhanh.vn Đang đọc Đã đọc
  7. 7 Webhook Nhanh.vn v3: cài đặt, xác thực và code nhận trong WordPress Đang đọc Đã đọc
  8. 8 Tích hợp Nhanh.vn Open API v3 bằng PHP cho WordPress, WooCommerce Đang đọc Đã đọc
Sản phẩm nổi bật của Toản
x