Webhook Nhanh.vn v3: cài đặt, xác thực và code nhận trong WordPress

Chào anh em! Cửa hàng vừa bán chiếc áo cuối cùng trên máy POS Nhanh.vn, vậy mà trang sản phẩm trên web WooCommerce vẫn báo còn hàng và khách vẫn đặt được. Hẹn giờ gọi API liên tục để dò thay đổi thì đụng ngay giới hạn 150 request mỗi 30 giây. Nhanh.vn có sẵn webhook cho việc này: Nhanh tự gửi request về website mỗi khi sản phẩm, tồn kho, đơn hàng có thay đổi, website chỉ việc nhận và cập nhật. Bài này đi từ bật webhook Nhanh.vn trong app tới code PHP nhận và hàng đợi chạy nền cho WordPress.

Webhook Nhanh.vn v3 gửi tồn kho, sản phẩm, đơn hàng về website WordPress, WooCommerce

Thông số lấy từ trang Webhooks và trang Sự kiện trong tài liệu chính thức của Nhanh.vn (đọc ngày 11/10/2026), cùng giao diện open.nhanh.vn mình kiểm cùng ngày; giao diện có thể đổi sau này. Plugin nhắc trong bài là sản phẩm mình viết, dùng Open API của Nhanh.vn. Code là mẫu tối giản, chạy thử trên bản sao website trước khi dùng thật.

Webhook Nhanh.vn là gì, khác gì gọi API?

Gọi API là website hỏi Nhanh; webhook là chiều ngược lại: Nhanh gửi một request POST chứa JSON tới đường dẫn bạn khai, mỗi khi có sự kiện. Tài liệu Nhanh khuyên chính cách này để tránh vượt rate limit: lưu dữ liệu ở hệ thống của bạn, nhận thay đổi qua webhook, chỉ gọi API để lấy phần thiếu.

Để 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ồn kho gần như bắt buộc đi qua webhook, vì API danh sách sản phẩm lọc theo updatedAt, mà updatedAt không đổi khi chỉ tồn kho đổi. Tài liệu khuyên đăng ký webhook tồn kho để cập nhật số tồn.
  • Webhook chỉ gửi dữ liệu mới, tức những gì phát sinh sau khi app được cấp quyền hoặc bật webhook; dữ liệu cũ không được gửi lại. Lần nối ban đầu vẫn phải kéo dữ liệu bằng API, sau đó webhook lo phần thay đổi.

Bật webhook trong app trên open.nhanh.vn

  1. Vào open.nhanh.vn → Danh sách app, mở app cần sửa (hoặc bấm Thêm mới), bật công tắc Bật webhooks.
  2. Ở ô Webhooks version, chọn version 3.0. Form hiện ghi version 2.0 “Ngừng hỗ trợ sau 31-08-2026”.
  3. Ô Webhooks callback URL điền đường dẫn nhận trên website. Form ghi URL cần chạy https, hỗ trợ POST và trả về HTTP 200.
  4. Ô Webhooks verify token điền chuỗi bạn tự đặt, 16–128 ký tự gồm chữ và số. Nhanh gửi kèm chuỗi này để bạn nhận ra request thật.
  5. Tick các sự kiện muốn nhận rồi bấm Lưu.
Form app trên open.nhanh.vn: Bật webhooks, Webhooks version 3.0, Webhooks callback URL, Webhooks verify token và các ô sự kiện
Bật webhooks, chọn version 3.0, nhập callback URL và verify token, tick sự kiện. Giá trị mẫu tenmien.com. Ảnh: giao diện open.nhanh.vn, 10/2026.

⚠️ Khung đỏ trên form nói rõ: app phải được đăng nhập cấp quyền, lấy được accessToken thì mới có sự kiện bắn về. Bật webhook xong mà chỉ nhận webhooksEnabled là bình thường, đó là webhook thử của hệ thống. Vào Nhanh.vn → Cài đặt → Cài đặt Open API, thấy appId nằm trong danh sách đang kết nối là xong phần cấp quyền. Cách lấy token từng bước có ở bài lấy access token Nhanh.vn.

🚨 Tài liệu dặn chỉ bật webhook khi đường dẫn nhận đã chạy được. Để URL lỗi liên tục (301, 302, 403, 404, 500…) thì app có thể bị tắt webhook hoặc bị khoá.

Các sự kiện v3

Bảng sự kiện ở trang cài đặt webhook liệt kê 9 sự kiện; trang Sự kiện mới chia theo nhóm App, POS và thêm orderPartialReturn:

  • webhooksEnabled: kiểm URL khi bạn bật webhook, hoặc khi Nhanh kiểm lại URL còn hoạt động (thường lúc tỉ lệ phản hồi thành công thấp). Mẫu trong tài liệu không có businessId.
  • appUninstalled: doanh nghiệp gỡ app; app không gọi API lấy dữ liệu của doanh nghiệp được nữa cho tới khi cấp quyền lại.
  • productAdd, productUpdate: data là một sản phẩm, cấu trúc giống một phần tử của API danh sách sản phẩm.
  • productDelete: data là mảng ID sản phẩm đã xoá.
  • inventoryChange: data là mảng sản phẩm kèm số tồn tổng và theo từng kho. Hoá đơn, đơn hàng xuất nhập kho sát nhau, hoặc app xử lý lỗi, thì Nhanh có thể gom nhiều sản phẩm vào một request.
  • orderAdd, orderUpdate: data là một đơn, cấu trúc giống một đơn trong API danh sách đơn hàng.
  • orderDelete: data là mảng ID đơn đã xoá.
  • orderPartialReturn: trả hàng một phần, hoặc nhập hoàn kho với đơn sàn; đơn trả một phần có thêm originalOrder (đơn gốc).

Form trên open.nhanh.vn có 8 ô tick: Thêm sản phẩm, Sửa sản phẩm, Xóa sản phẩm, Cập nhật tồn kho, Thêm đơn hàng, Sửa đơn hàng, Xóa đơn hàng, Trả hàng 1 phần; hai sự kiện của app không có ô tick. Quyền của token cũng quyết định: webhook sản phẩm và tồn kho cần quyền danh sách sản phẩm, webhook đơn hàng cần quyền danh sách đơn hàng.

Dữ liệu gửi về

Mỗi sự kiện là một request POST có header Content-Type: application/json và Authorization mang đúng chuỗi verify token, body JSON gồm event, businessId và data. Mẫu webhook tồn kho Nhanh.vn (inventoryChange) rút gọn theo tài liệu, số là minh hoạ:

POST /wp-json/devvn-nhanh/v1/webhook
Content-Type: application/json
Authorization: VERIFY_TOKEN_CUA_BAN

{
  "event": "inventoryChange",
  "businessId": 123456,
  "data": [
    {
      "id": 5001,
      "code": "AO-THUN-M",
      "remain": 12,
      "holding": 2,
      "available": 10,
      "depots": [
        { "id": 11, "remain": 7, "available": 6 },
        { "id": 12, "remain": 5, "available": 4 }
      ]
    }
  ]
}
  • available: số có thể bán; tài liệu ghi trường này dùng để chặn đặt sản phẩm hết hàng, nên đây là số hợp để ghi vào tồn kho WooCommerce. Ngoài ra còn remain (tồn), shipping (đang giao), damaged (lỗi), holding (tạm giữ), transfering (đang chuyển kho).
  • depots: số theo từng kho. Shop chỉ muốn bán online từ vài kho thì cộng available của các kho đó thay vì lấy số tổng.
  • Với sản phẩm combo, tài liệu ghi trả về cả sản phẩm combo lẫn các sản phẩm trong combo.
  • Tài liệu mô tả data là “json string”, nhưng mọi ví dụ đều là object hoặc mảng JSON. Code bên dưới đỡ cả hai kiểu.
  • Cấu trúc chung không có mã sự kiện riêng hay giờ gửi, tài liệu cũng không nói về thứ tự gửi. Muốn khử trùng thì tự băm nội dung.

Xác thực bằng verify token ở header Authorization

  • Với version 3.0, verify token đi trong header Authorization (trang Webhooks). Trang Khởi tạo app còn câu cũ nói verify token nằm trong body: đó là cách của version 2.0, trường webhooksVerifyToken. Code phải theo đúng version đang chọn trong app.
  • So bằng hash_equals() để thời gian so sánh không để lộ thông tin, rồi đối chiếu businessId với doanh nghiệp của bạn.
  • Đặt chuỗi dài, ngẫu nhiên, để trong wp-config.php; không commit, không in ra log.
  • ⚠️ Một số máy chủ Apache chạy PHP qua CGI/FPM làm rơi header Authorization. File .htaccess do WordPress tạo có dòng RewriteRule .* - [E=HTTP_AUTHORIZATION:%{HTTP:Authorization}] để chuyển header này vào PHP; ai tự viết lại .htaccess thì kiểm còn dòng đó không.

Trả lời webhook thế nào để Nhanh không tắt?

  • ✅ Trả HTTP 200 là Nhanh tính đã xử lý thành công.
  • Phản hồi quá chậm hoặc trả mã khác 200 thì Nhanh gửi lại tối đa 3 lần. Tài liệu không ghi con số thời gian chờ cụ thể.
  • App xử lý chậm hoặc hay lỗi thì bị hạ độ ưu tiên, các webhook sau có thể tới trễ.
  • 🚨 Tỉ lệ thành công thấp thì tuỳ mức, Nhanh gọi lại webhooksEnabled để kiểm URL, tắt webhook của app (bạn phải bật lại như trên), hoặc khoá app (liên hệ dev@nhanh.vn để mở).
  • Tài liệu khuyến cáo lưu webhook và trả lời Nhanh trước, xử lý sau bằng cronjob.

Suy ra cách viết endpoint: kiểm token, lưu nguyên payload vào hàng đợi, trả 200 ngay; việc nặng như ghi tồn kho, cập nhật sản phẩm để tiến trình nền làm. Tin hợp lệ mà không cần xử lý cũng trả 200. Chỉ trả mã lỗi khi website hỏng thật (ví dụ không ghi được hàng đợi) và bạn muốn Nhanh gửi lại.

Code PHP nhận webhook trong WordPress

Code gồm bốn phần: bảng hàng đợi, REST route nhận webhook, tiến trình nền xử lý, và hàm xử lý từng sự kiện. Tiến trình nền dùng Action Scheduler đi kèm WooCommerce, không có thì lùi về WP-Cron. Sản phẩm trên web khớp với Nhanh qua meta _devvn_nhanh_id. Cấu hình trong wp-config.php, chuỗi verify token giống hệt chuỗi khai trong app:

define( 'DEVVN_NHANH_WEBHOOK_TOKEN', 'YOUR_VERIFY_TOKEN' );
define( 'DEVVN_NHANH_BUSINESS_ID', 'YOUR_BUSINESS_ID' );

Phần 1: bảng hàng đợi

define( 'DEVVN_NHANH_META', '_devvn_nhanh_id' );
define( 'DEVVN_NHANH_WH_TABLE', 'devvn_nhanh_webhooks' );
define( 'DEVVN_NHANH_WH_DB_VERSION', '1' );
define( 'DEVVN_NHANH_WH_HOOK', 'devvn_nhanh_process_webhooks' );
define( 'DEVVN_NHANH_WH_BATCH', 25 );
define( 'DEVVN_NHANH_WH_MAX_ATTEMPTS', 3 );
define( 'DEVVN_NHANH_WH_STUCK', 5 * MINUTE_IN_SECONDS );
define( 'DEVVN_NHANH_WH_KEEP', 7 * DAY_IN_SECONDS );

/**
 * Tên bảng hàng đợi webhook kèm tiền tố.
 *
 * @return string
 */
function devvn_nhanh_wh_table() {
	global $wpdb;
	return $wpdb->prefix . DEVVN_NHANH_WH_TABLE;
}

/**
 * Tạo hoặc nâng cấp bảng hàng đợi khi phiên bản cấu trúc thay đổi.
 *
 * @return void
 */
function devvn_nhanh_wh_install() {
	global $wpdb;
	if ( get_option( 'devvn_nhanh_wh_db' ) === DEVVN_NHANH_WH_DB_VERSION ) {
		return;
	}
	require_once ABSPATH . 'wp-admin/includes/upgrade.php';
	$table   = devvn_nhanh_wh_table();
	$collate = $wpdb->get_charset_collate();
	dbDelta(
		"CREATE TABLE {$table} (
		id bigint(20) unsigned NOT NULL AUTO_INCREMENT,
		event varchar(40) NOT NULL,
		body_hash char(32) NOT NULL,
		payload longtext NOT NULL,
		status varchar(20) NOT NULL DEFAULT 'pending',
		attempts tinyint(3) unsigned NOT NULL DEFAULT 0,
		last_error text NULL,
		created_at datetime NOT NULL,
		updated_at datetime NOT NULL,
		PRIMARY KEY  (id),
		KEY status_id (status,id),
		KEY body_hash (body_hash)
		) {$collate};"
	);
	update_option( 'devvn_nhanh_wh_db', DEVVN_NHANH_WH_DB_VERSION );
}
add_action( 'admin_init', 'devvn_nhanh_wh_install' );

Phần 2: route nhận, kiểm token, lưu hàng đợi, trả 200

/**
 * Đăng ký endpoint POST /wp-json/devvn-nhanh/v1/webhook.
 *
 * @return void
 */
function devvn_nhanh_wh_register_route() {
	register_rest_route(
		'devvn-nhanh/v1',
		'/webhook',
		array(
			'methods'             => 'POST',
			'callback'            => 'devvn_nhanh_wh_receive',
			'permission_callback' => '__return_true',
		)
	);
}
add_action( 'rest_api_init', 'devvn_nhanh_wh_register_route' );

/**
 * Các sự kiện được đưa vào hàng đợi; webhooksEnabled chỉ cần trả 200.
 *
 * @return string[]
 */
function devvn_nhanh_wh_events() {
	return array( 'appUninstalled', 'productAdd', 'productUpdate', 'productDelete', 'inventoryChange', 'orderAdd', 'orderUpdate', 'orderDelete', 'orderPartialReturn' );
}

/**
 * Nhận webhook: kiểm verify token và businessId, lưu hàng đợi, trả 200 ngay.
 *
 * @param WP_REST_Request $request Request Nhanh.vn gửi tới.
 * @return WP_REST_Response
 */
function devvn_nhanh_wh_receive( WP_REST_Request $request ) {
	$token = (string) $request->get_header( 'authorization' );
	if ( '' === $token || ! hash_equals( (string) DEVVN_NHANH_WEBHOOK_TOKEN, $token ) ) {
		return new WP_REST_Response( array( 'message' => 'Unauthorized' ), 401 );
	}
	$ok    = new WP_REST_Response( array( 'message' => 'OK' ), 200 );
	$body  = (string) $request->get_body();
	$json  = json_decode( $body, true );
	$event = isset( $json['event'] ) ? preg_replace( '/[^A-Za-z]/', '', (string) $json['event'] ) : '';
	if ( ! in_array( $event, devvn_nhanh_wh_events(), true ) ) {
		return $ok;
	}
	if ( ! isset( $json['businessId'] ) || (string) $json['businessId'] !== (string) DEVVN_NHANH_BUSINESS_ID ) {
		return $ok;
	}
	if ( ! devvn_nhanh_wh_enqueue( $event, $body ) ) {
		return new WP_REST_Response( array( 'message' => 'Queue error' ), 500 );
	}
	devvn_nhanh_wh_schedule();
	return $ok;
}

/**
 * Lưu một webhook vào hàng đợi; bản giống hệt đang chờ xử lý thì bỏ qua vì đó là Nhanh gửi lại.
 *
 * @param string $event Tên sự kiện.
 * @param string $body  Nguyên văn body JSON.
 * @return bool
 */
function devvn_nhanh_wh_enqueue( $event, $body ) {
	global $wpdb;
	$table = devvn_nhanh_wh_table();
	$hash  = md5( $body );
	$found = $wpdb->get_var( $wpdb->prepare( "SELECT id FROM {$table} WHERE body_hash = %s AND status = %s LIMIT 1", $hash, 'pending' ) );
	if ( $found ) {
		return true;
	}
	$now = current_time( 'mysql', true );
	return (bool) $wpdb->insert(
		$table,
		array(
			'event'      => $event,
			'body_hash'  => $hash,
			'payload'    => $body,
			'status'     => 'pending',
			'created_at' => $now,
			'updated_at' => $now,
		)
	);
}

/**
 * Hẹn một lượt xử lý hàng đợi nếu chưa có lượt nào đang chờ chạy.
 *
 * @param int $delay Số giây chờ trước khi chạy.
 * @return void
 */
function devvn_nhanh_wh_schedule( $delay = 0 ) {
	if ( function_exists( 'as_get_scheduled_actions' ) ) {
		$pending = as_get_scheduled_actions(
			array(
				'hook'     => DEVVN_NHANH_WH_HOOK,
				'status'   => 'pending',
				'per_page' => 1,
			),
			'ids'
		);
		if ( empty( $pending ) ) {
			as_schedule_single_action( time() + $delay, DEVVN_NHANH_WH_HOOK );
		}
		return;
	}
	if ( ! wp_next_scheduled( DEVVN_NHANH_WH_HOOK ) ) {
		wp_schedule_single_event( time() + $delay, DEVVN_NHANH_WH_HOOK );
	}
}

Phần 3: tiến trình nền xử lý hàng đợi

/**
 * Xử lý một lô webhook: nhận việc từng dòng, lỗi thì thử lại tối đa 3 lần rồi đánh dấu failed.
 *
 * @return void
 */
function devvn_nhanh_wh_process() {
	global $wpdb;
	$table = devvn_nhanh_wh_table();
	$wpdb->query( $wpdb->prepare( "UPDATE {$table} SET status = %s WHERE status = %s AND updated_at < %s", 'pending', 'processing', gmdate( 'Y-m-d H:i:s', time() - DEVVN_NHANH_WH_STUCK ) ) );
	$wpdb->query( $wpdb->prepare( "DELETE FROM {$table} WHERE status = %s AND updated_at < %s LIMIT 500", 'done', gmdate( 'Y-m-d H:i:s', time() - DEVVN_NHANH_WH_KEEP ) ) );
	$rows = $wpdb->get_results( $wpdb->prepare( "SELECT id, event, payload, attempts FROM {$table} WHERE status = %s ORDER BY id ASC LIMIT %d", 'pending', DEVVN_NHANH_WH_BATCH ) );
	foreach ( $rows as $row ) {
		$claimed = $wpdb->update(
			$table,
			array(
				'status'     => 'processing',
				'updated_at' => current_time( 'mysql', true ),
			),
			array(
				'id'     => $row->id,
				'status' => 'pending',
			)
		);
		if ( 1 !== $claimed ) {
			continue;
		}
		$json = json_decode( $row->payload, true );
		$data = isset( $json['data'] ) ? $json['data'] : array();
		$data = is_string( $data ) ? json_decode( $data, true ) : $data;
		try {
			$result = devvn_nhanh_wh_handle( $row->event, is_array( $data ) ? $data : array() );
		} catch ( Throwable $e ) {
			$result = new WP_Error( 'nhanh_wh_exception', $e->getMessage() );
		}
		$fields = array(
			'status'     => 'done',
			'updated_at' => current_time( 'mysql', true ),
		);
		if ( is_wp_error( $result ) ) {
			$attempts             = (int) $row->attempts + 1;
			$fields['status']     = $attempts >= DEVVN_NHANH_WH_MAX_ATTEMPTS ? 'failed' : 'pending';
			$fields['attempts']   = $attempts;
			$fields['last_error'] = $result->get_error_message();
		}
		$wpdb->update( $table, $fields, array( 'id' => $row->id ) );
	}
	$left = (int) $wpdb->get_var( $wpdb->prepare( "SELECT COUNT(*) FROM {$table} WHERE status = %s", 'pending' ) );
	if ( $left > 0 ) {
		devvn_nhanh_wh_schedule( count( $rows ) < DEVVN_NHANH_WH_BATCH ? MINUTE_IN_SECONDS : 0 );
	}
}
add_action( DEVVN_NHANH_WH_HOOK, 'devvn_nhanh_wh_process' );

Phần 4: xử lý từng sự kiện

/**
 * Xử lý một sự kiện; sự kiện chưa viết riêng thì phát action devvn_nhanh_webhook_{event}.
 *
 * @param string $event Tên sự kiện.
 * @param array  $data  Phần data của webhook.
 * @return true|WP_Error
 */
function devvn_nhanh_wh_handle( $event, array $data ) {
	if ( 'inventoryChange' === $event ) {
		return devvn_nhanh_wh_inventory( $data );
	}
	if ( 'productDelete' === $event ) {
		foreach ( devvn_nhanh_map_products( $data ) as $post_id ) {
			delete_post_meta( $post_id, DEVVN_NHANH_META );
		}
		return true;
	}
	do_action( 'devvn_nhanh_webhook_' . $event, $data );
	return true;
}

/**
 * Ghi số có thể bán (available) vào tồn kho các sản phẩm, biến thể đã khớp ID Nhanh.vn.
 *
 * @param array $items Mảng sản phẩm trong webhook inventoryChange.
 * @return true
 */
function devvn_nhanh_wh_inventory( array $items ) {
	$items = array_column( $items, null, 'id' );
	$map   = devvn_nhanh_map_products( array_keys( $items ) );
	_prime_post_caches( array_values( $map ), false, true );
	foreach ( $map as $nhanh_id => $post_id ) {
		$product = wc_get_product( $post_id );
		if ( ! $product || $product->is_type( 'variable' ) || ! isset( $items[ $nhanh_id ]['available'] ) ) {
			continue;
		}
		$qty = max( 0, (int) floor( (float) $items[ $nhanh_id ]['available'] ) );
		if ( $product->get_manage_stock() && (int) $product->get_stock_quantity() === $qty ) {
			continue;
		}
		$product->set_manage_stock( true );
		$product->set_stock_quantity( $qty );
		$product->save();
	}
	return true;
}

/**
 * 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;
}
  • URL khai ở ô Webhooks callback URL là https://ten-mien-cua-ban/wp-json/devvn-nhanh/v1/webhook. Đường dẫn tĩnh kiểu mặc định (Plain) thì dùng https://ten-mien-cua-ban/?rest_route=/devvn-nhanh/v1/webhook.
  • Sai token thì trả 401 để lỗi cấu hình lộ ra sớm. Đổi verify token thì đổi cả hai nơi cùng lúc: lệch lâu là tỉ lệ thành công tụt và Nhanh có thể tắt webhook.
  • Gặp webhooksEnabled, sự kiện lạ hoặc businessId khác thì đều trả 200 và không lưu. Tên sự kiện giữ nguyên hoa thường, đừng lọc bằng sanitize_key() vì hàm này đổi inventoryChange thành chữ thường.
  • Code khử trùng bằng cách bỏ qua bản giống hệt đang chờ. Bản đã xử lý xong mà cùng nội dung tới lại vẫn được nhận, vì tồn có thể quay về đúng số cũ sau vài lần bán, nhập. Ghi tồn theo giá trị (không cộng trừ) nên xử lý lặp lại cũng vô hại.
  • Hàng đợi xử lý mỗi lượt 25 dòng, nhận việc từng dòng bằng UPDATE có điều kiện để hai tiến trình không làm trùng; dòng kẹt ở processing quá 5 phút được trả về pending; lỗi thử lại tối đa 3 lần rồi thành failed kèm last_error; dòng xong giữ 7 ngày để tra.
  • Cả lô sản phẩm được tra theo meta _devvn_nhanh_id bằng một truy vấn (không truy vấn trong vòng lặp), chỉ ghi khi số tồn thực sự đổi. Sản phẩm cha của biến thể được bỏ qua, tồn ghi vào từng biến thể. Ghép chung với code kéo sản phẩm (cũng có devvn_nhanh_map_products() và DEVVN_NHANH_META) thì chỉ giữ một bản, PHP không cho khai báo trùng tên hàm.
  • Muốn mở rộng thì móc vào action mà sự kiện chưa xử lý riêng phát ra, ví dụ add_action( 'devvn_nhanh_webhook_productUpdate', 'ham_cua_ban' ) để cập nhật giá, ảnh; hay devvn_nhanh_webhook_appUninstalled để email báo admin cấp quyền lại. Hàm gọi API có xử lý ERR_429 (khi cần lấy thêm chi tiết sản phẩm) có ở bài tích hợp Nhanh.vn Open API.

Bắn thử bằng curl

Chỉ làm trên bản sao website hoặc sản phẩm thử có meta _devvn_nhanh_id bằng 5001; đổi 123456 thành ID doanh nghiệp đã khai trong wp-config.php. Lệnh đầu giả lập webhooksEnabled, lệnh sau giả lập đổi tồn:

curl -i -X POST 'https://ten-mien-cua-ban/wp-json/devvn-nhanh/v1/webhook' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_VERIFY_TOKEN' \
  -d '{"event":"webhooksEnabled"}'

curl -i -X POST 'https://ten-mien-cua-ban/wp-json/devvn-nhanh/v1/webhook' \
  -H 'Content-Type: application/json' \
  -H 'Authorization: YOUR_VERIFY_TOKEN' \
  -d '{"event":"inventoryChange","businessId":123456,"data":[{"id":5001,"available":7}]}'

Đúng token thì dòng đầu là HTTP/1.1 200 OK (hoặc HTTP/2 200), sai token ra 401. Ra 301, 302 là đường dẫn đang bị chuyển hướng, phải khai lại đúng đường dẫn cuối. Tài liệu Nhanh nhắc riêng: Postman mặc định tự đi theo chuyển hướng (Automatically follow redirects) nên dễ thấy 200 giả; curl không có -L thì không đi theo, nhìn được mã thật.

Dùng plugin: phần nhận webhook có sẵn

Web dùng plugin Nhanh.vn cho WooCommerce của mình thì phần nhận webhook có sẵn, bạn chỉ khai vào app những gì plugin đưa ra:

  1. Vào WooCommerce → Nhanh.vn, tab Nhanh.vn, chọn Version 3 ở dòng “Bật webhooks > Chọn version”.
  2. Chép Webhooks callback URL plugin đưa sẵn, dạng https://ten-mien-cua-ban/wp-json/nhanhvn/v1/webhook/, dán vào app.
  3. Plugin tự sinh sẵn Webhooks verify token là chuỗi 24 ký tự ngẫu nhiên. Cần đổi thì bấm Thay đổi verify token; token chỉ có hiệu lực sau khi bấm Lưu thay đổi, rồi dán đúng chuỗi đó vào app.
  4. Tick các sự kiện trang cài đặt ghi plugin hỗ trợ: Xóa sản phẩm, Xóa đơn hàng, Cập nhật tồn kho, Sửa đơn hàng, Sửa sản phẩm.

Đoạn lấy đường dẫn trong plugin và khai vào app trên open.nhanh.vn có trong video dưới, bắt đầu ở giây 0:15.

Tab Nhanh.vn của plugin: chọn Version 3, Webhooks callback URL dạng wp-json/nhanhvn/v1/webhook, ô verify token và link Thay đổi verify token
Bước 4 tới 7 trong tab Nhanh.vn: chọn Version 3, chép callback URL và verify token sang app. Dữ liệu mẫu, token đã che.

Với API v3, plugin dùng webhook cho những việc sau:

  • inventoryChange: cập nhật tồn kho, tính theo các kho đã đồng bộ về web và bỏ các kho tick “Không tính tồn kho”.
  • productUpdate: cập nhật giá, tồn, ảnh, mô tả, cân nặng, kích thước theo các ô đồng bộ đang bật.
  • productDelete: chỉ gỡ liên kết ID Nhanh, không xoá sản phẩm trên web.
  • orderDelete: đơn WooCommerce tương ứng chuyển sang Đã huỷ. Trạng thái đơn ở API v3 thì anh em theo dõi trên Nhanh.vn.

⚠️ Trước khi trông vào webhook tồn kho, vào tab Kho hàng bấm Đồng bộ kho từ Nhanh.vn về web. Plugin chỉ cộng tồn của các kho có trong danh sách đã đồng bộ: chưa bấm thì tồn về 0, Nhanh thêm kho mới thì bấm đồng bộ lại. Cách tính tồn và cách xử lý khi lệch có ở bài đồng bộ tồn kho Nhanh.vn.

Ở tab Webhook, ô Chạy nền (Background Queue) bật sẵn: plugin nhận request, lưu hàng đợi, trả 200 ngay rồi xử lý bằng Action Scheduler (không có thì WP-Cron), đúng cách tài liệu Nhanh khuyên.

Mục Xử lý Webhook trong plugin: ô Chạy nền (Background Queue) đã bật, giải thích nhận request, lưu queue, trả 200 ngay, xử lý sau bằng Action Scheduler
Ô Chạy nền (Background Queue) bật sẵn trong tab Webhook của plugin.

Trang Webhook Queue đếm Tất cả, Chờ xử lý, Đang xử lý, Hoàn thành, Đã gộp, Thất bại; có ô tìm trong data và các nút Data, Retry, Xoá, “Retry tất cả failed”, “Xoá log đã xử lý”. inventoryChange và productUpdate của cùng một sản phẩm đang chờ thì bản cũ được đánh dấu Đã gộp. Lỗi được thử lại tối đa 3 lần rồi mới thành Thất bại. Lời gọi API đi kèm (ví dụ lấy chi tiết sản phẩm) được plugin giữ khoảng 120 trên 150 request mỗi 30 giây, gặp ERR_429 thì chờ hết khoá rồi gọi lại một lần.

Trang Webhook Queue của plugin với các ô đếm trạng thái, nút Retry tất cả failed và bảng sự kiện inventoryChange, productUpdate, orderUpdate
Trang Webhook Queue: dòng Thất bại (lỗi ERR_429 mẫu) bấm Retry để chạy lại. Dữ liệu mẫu.

Chi tiết từng ô có ở trang webhook trong tài liệu plugin.

Webhook không về: kiểm theo thứ tự này

  1. Kiểm app đã được cấp quyền chưa. Chỉ có webhooksEnabled mà không có sự kiện thật thì xem Cài đặt Open API trên Nhanh đã có appId chưa. Token hết hạn (sau 1 năm) hoặc doanh nghiệp gỡ app thì webhook cũng dừng.
  2. Mở app trên open.nhanh.vn xem webhook còn bật không, vì Nhanh có thể tự tắt khi URL lỗi hoặc tỉ lệ thành công thấp.
  3. Xem tab API trên Nhanh.vn. Trang chi tiết đơn hàng, chi tiết sản phẩm có tab API lưu lịch sử request và webhook đã gửi cho app. Tài liệu khuyên kiểm ở đây trước khi báo lỗi.
  4. Thử bằng curl ở trên xem URL có trả 200 không. Xem lại tên miền, chứng chỉ SSL, chuyển hướng 301, 302 (http sang https, có hay không www), trang bắt đăng nhập.
  5. Kiểm tường lửa có chặn không. Cloudflare, tường lửa máy chủ, plugin bảo mật có thể chặn POST vào /wp-json/. Tài liệu Nhanh nêu riêng Wordfence có thể trả 503, làm không bật được webhook hoặc nhận chập chờn, và khuyên gỡ plugin này.
  6. So verify token hai bên có khớp không. Token lệch khi đổi một bên mà quên bên kia, hoặc header Authorization không tới được PHP (xem mục xác thực ở trên).
  7. Xem hàng đợi có chạy không. Dòng nằm mãi ở Chờ xử lý thường do cron không chạy: tắt WP-Cron mà chưa đặt cron hệ thống, hoặc site quá ít truy cập. Xem danh sách Scheduled Actions của WooCommerce.
  8. Dữ liệu cũ thì đừng chờ, vì webhook chỉ gửi thay đổi phát sinh sau khi cấp quyền, bật webhook; dữ liệu trước đó phải kéo bằng API.

Không muốn tự viết và bảo trì endpoint, hàng đợi, trang theo dõi thì plugin có sẵn cả, kèm đồng bộ sản phẩm, đăng đơn và phí ship từ Nhanh.vn:

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

Nhanh.vn gửi lại webhook mấy lần?

Tối đa 3 lần khi website phản hồi quá chậm hoặc trả mã khác 200. Lỗi kéo dài thì app bị hạ độ ưu tiên, có thể bị tắt webhook hoặc khoá app.

Verify token nằm ở header hay body?

Version 3.0 gửi ở header Authorization. Version 2.0 gửi trong body, trường webhooksVerifyToken. Đổi app sang 3.0 thì nhớ sửa code đọc token theo.

Tồn kho trên web cập nhật sau bao lâu?

Tài liệu không cam kết thời gian. Tin tới khi Nhanh gửi, có thể trễ khi app bị hạ độ ưu tiên; phía website còn tuỳ hàng đợi chạy nền chạy lúc nào. Vì vậy đừng coi webhook là thời gian thực, và nên để cron chạy đều.

Đã hẹn giờ gọi API thì có cần webhook không?

Với tồn kho thì cần: API danh sách sản phẩm không bắt được thay đổi tồn qua updatedAt, còn quét toàn bộ liên tục thì dễ chạm rate limit 150 request/30 giây.

Vướng chỗ nào cứ để lại bình luận, nhớ che verify token, businessId và dữ liệu khách trước khi dán payload nhé. 🚀

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