Viettel Post webhook: cách khai, dữ liệu gửi về và code PHP mẫu

Chào anh em! Bưu tá giao xong từ hôm qua, khách đã nhận hàng, vậy mà đơn trên website vẫn đứng ở “Đang xử lý”. Nhân viên phải mở viettelpost.vn tra từng mã rồi quay lại WooCommerce đổi tay. Khai Viettel Post webhook thì khỏi tra tay, vì Viettel Post tự báo trạng thái về website mỗi khi đơn đi thêm một chặng.

Viettel Post webhook đồng bộ trạng thái đơn hàng về website WooCommerce

Đọc xong, bạn tự khai được webhook trên Partner và cho đơn WooCommerce đổi trạng thái theo tin Viettel Post gửi về, bằng đoạn PHP trong bài hoặc bằng plugin. Thông số lấy từ tài liệu Webhook chính thức của Viettel Post (đọc ngày 10/10/2026), phần còn lại là kinh nghiệm làm plugin.

Webhook Viettel Post là gì, sao không gọi API tra cứu liên tục?

Theo tài liệu, webhook là cơ chế callback: khi đơn đổi trạng thái (đã lấy hàng, đang giao, giao thành công, giao thất bại, hoàn hàng…), Viettel Post chủ động gửi một request POST chứa thông tin cập nhật tới đường dẫn bạn khai trước. Website nhận tin, lưu hành trình rồi đổi trạng thái đơn WooCommerce theo đó.

Để 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

Không có webhook thì website phải tự hẹn giờ hỏi cho từng đơn chưa xong: 200 đơn, nửa tiếng một lần là gần 10.000 request mỗi ngày mà trạng thái vẫn trễ. Còn mục Tài liệu trên Partner (xem ngày 10/10/2026) liệt kê API địa danh, dịch vụ, tính cước, tạo đơn, cập nhật đơn, không có API tra hành trình để gọi định kỳ. Muốn đơn tự đổi trạng thái thì dùng webhook.

Cách khai webhook trên Partner Viettel Post

Shop tự khai, không phải chờ Viettel Post bật giúp:

  1. Đăng nhập partner2.viettelpost.vn bằng tài khoản Viettel Post của shop.
  2. Vào Bảng điều khiển → Thông tin tài khoản → Cấu hình webhook.
  3. Ô Webhook Endpoints: dán đường dẫn nhận webhook trên website (dạng https://…).
  4. Ô Secret parameter: chuỗi bí mật do bạn đặt. Viettel Post gửi kèm chuỗi này trong mỗi request để website nhận ra tin thật.
  5. Bấm Kiểm tra kết nối.
Trang Cấu hình webhook trên Partner Viettel Post với ô Webhook Endpoints, Secret parameter và nút Kiểm tra kết nối
Cấu hình webhook trên Partner: Bảng điều khiển → Thông tin tài khoản → Cấu hình webhook
  • Mỗi tài khoản chỉ cấu hình được một webhook endpoint. Dùng chung cho hai website thì chỉ một bên nhận được trạng thái.
  • Dùng cơ chế uỷ quyền thì hành trình về webhook của tài khoản uỷ quyền, tài khoản được uỷ quyền không cần khai.
  • Viettel Post hướng dẫn cấu hình thử trên môi trường Development trước.

Viettel Post gửi gì về website?

Request POST, header Content-Type: application/json;charset=UTF-8 (bảng header trong tài liệu có thêm Authorization). Thân là JSON hai phần: TOKEN chính là Secret parameter bạn khai, DATA là thông tin đơn. Mẫu rút gọn theo tài liệu, mình đổi tên và số điện thoại:

{
  "DATA": {
    "ORDER_NUMBER": "TUANPAD3024822076",
    "ORDER_REFERENCE": "TUANPAD3024822076",
    "ORDER_STATUSDATE": "10/11/2025 11:07:16",
    "ORDER_STATUS": 104,
    "STATUS_NAME": "Giao cho Bưu tá đi nhận",
    "LOCATION_CURRENTLY": "HNI, GLM, Bưu cục Gia Lâm - HNI - Hà Nội",
    "NOTE": "Phân công bưu tá nhận hàng",
    "MONEY_COLLECTION": 0,
    "MONEY_FEECOD": 1000,
    "MONEY_TOTALFEE": 236115,
    "MONEY_TOTAL": 266884,
    "PRODUCT_WEIGHT": 7100,
    "ORDER_SERVICE": "VHT",
    "ORDER_PAYMENT": 1,
    "EMPLOYEE_NAME": "Nguyễn Văn A",
    "EMPLOYEE_PHONE": "84900000000",
    "IS_RETURNING": false,
    "REASON_CODE": null
  },
  "TOKEN": "chuoi-bi-mat-cua-ban"
}
  • ORDER_NUMBER: mã vận đơn Viettel Post. ORDER_REFERENCE: mã tham chiếu của đối tác, tức mã đơn bạn gửi lúc tạo đơn.
  • ORDER_STATUS: mã trạng thái (số nguyên), kèm STATUS_NAME, NOTE và ORDER_STATUSDATE dạng ngày/tháng/năm giờ:phút:giây.
  • LOCATION_CURRENTLY: vị trí hiện tại của bưu gửi. ⚠️ Mẫu JSON trong tài liệu có thêm trường viết sai chính tả LOCALION_CURRENTLY, code nên đọc cả hai.
  • Tiền: MONEY_COLLECTION thu hộ, MONEY_FEECOD phí thu hộ, MONEY_TOTALFEE tổng cước chưa VAT, MONEY_TOTALVAT tiền VAT. PRODUCT_WEIGHT tính bằng gram.
  • EMPLOYEE_NAME, EMPLOYEE_PHONE: bưu tá phụ trách. REASON_CODE: mã lý do phát thất bại.
  • MONEY_COLLECTION_ORIGIN: tiền thu hộ ban đầu. Khi ORDER_STATUS nhỏ hơn 200 nó luôn là null, tài liệu dặn đừng dùng.
  • POD: ảnh bằng chứng giao hàng (.jpg, lưu 6 tháng). Gói webhook cơ bản chưa trả link ảnh, muốn nhận phải gửi yêu cầu tới Viettel Post.

Website phải trả lời webhook thế nào?

  • ⏱️ Phản hồi dưới 1 giây, để hành trình được gửi liên tục, không gián đoạn.
  • Viettel Post đẩy hành trình lần lượt theo thứ tự. Mỗi bản ghi chỉ tính là thành công khi nhận HTTP 200; chưa nhận thì gửi lại tối đa 5 lần.
  • Hành trình có thể trùng hoặc thừa: ghi log và vẫn trả 200.
  • Đơn đã ở trạng thái cuối (101, 107, 201, 501, 503, 504) thì dừng cập nhật. Bản ghi nào tới sau đó cũng chỉ ghi log và trả 200.

Code nhận webhook vì thế chỉ nên kiểm secret, ghi nhận rồi trả 200 ngay; việc nặng đẩy sang chạy nền.

Mã trạng thái Viettel Post hay gặp

Bảng trạng thái trong tài liệu webhook, mình gom theo chặng:

  • Chặng lấy hàng gồm 103 điều phối bưu cục lấy hàng, 104 điều phối bưu tá lấy hàng, 102 lấy hàng thất bại, 200 lấy hàng thành công, 202 sửa phiếu gửi.
  • Chặng trung chuyển gồm 300 khai thác đi, 400 khai thác đến và 509 chuyển tiếp bưu cục khác.
  • Chặng giao hàng gồm 500 giao bưu tá đi phát, 501 phát thành công, 506 phát thất bại, 507 khách đến bưu cục nhận, 508 và 550 phát tiếp.
  • Hoàn hàng gồm 505 yêu cầu chuyển hoàn, 515 duyệt hoàn và 504 đã chuyển trả người gửi.
  • Nhóm huỷ gồm 101 Viettel Post huỷ lấy hàng, 107 đối tác yêu cầu huỷ qua API, 201 Viettel Post huỷ đơn hàng và 503 tiêu huỷ theo yêu cầu khách hàng.

Mình chốt đơn theo nhóm trạng thái cuối: 501 là giao xong; 101, 107, 201, 503 là huỷ; 504 là hàng đã hoàn về shop. Mốc giữa đường chỉ nên lưu vào hành trình, đổi trạng thái theo từng mốc làm đơn nhảy liên tục và dễ bắn email nhầm cho khách. Bảng trong tài liệu đánh dấu thêm 104 là trạng thái cuối, phần lưu ý thì không; mình theo phần lưu ý.

Với 505, 506, 507, trường REASON_CODE cho biết lý do: 35–38 hẹn phát lại, không liên lạc được, khách đến bưu cục nhận; 20–26 và 30–32 khách từ chối nhận (sai hàng, chất lượng kém, không cho xem hàng, sai tiền thu hộ, sai địa chỉ…); 27 sai số điện thoại; 43 người gửi yêu cầu hoàn; 46, 47 phát thất bại nhiều lần. Ghi lý do vào ghi chú đơn để nhân viên gọi khách đúng chuyện.

Tự code: nhận webhook trong WordPress

Đoạn code dưới tạo một endpoint REST, kiểm secret, tìm đơn theo mã vận đơn, lưu hành trình, đổi trạng thái theo nhóm trạng thái cuối rồi trả 200. Mã vận đơn đọc ở meta _vtp_ma_van_don, đúng chỗ đoạn tạo đơn lưu lại sau khi gọi createOrder. Phần lấy token và tạo đơn có ở bài tích hợp API Viettel Post bằng PHP. Secret để trong wp-config.php, giống hệt chuỗi ở ô Secret parameter:

define( 'DEVVN_VTP_WEBHOOK_SECRET', 'chuoi-bi-mat-cua-ban' );
/**
 * Đăng ký endpoint POST /wp-json/devvn-vtp/v1/webhook cho Viettel Post.
 *
 * @return void
 */
function devvn_vtp_register_webhook() {
	register_rest_route(
		'devvn-vtp/v1',
		'/webhook',
		array(
			'methods'             => 'POST',
			'callback'            => 'devvn_vtp_webhook',
			'permission_callback' => '__return_true',
		)
	);
}
add_action( 'rest_api_init', 'devvn_vtp_register_webhook' );

/**
 * Tìm đơn theo mã vận đơn Viettel Post, chạy đúng cả khi bật lẫn tắt HPOS.
 *
 * @param string $tracking Mã vận đơn (DATA.ORDER_NUMBER).
 * @return WC_Order|null
 */
function devvn_vtp_find_order( $tracking ) {
	$orders = wc_get_orders(
		array(
			'limit'      => 1,
			'meta_key'   => '_vtp_ma_van_don',
			'meta_value' => $tracking,
		)
	);
	if ( empty( $orders ) || $orders[0]->get_meta( '_vtp_ma_van_don' ) !== $tracking ) {
		return null;
	}
	return $orders[0];
}

/**
 * Nhận một bản ghi hành trình, lưu vào đơn, đổi trạng thái và trả lời ngay.
 *
 * @param WP_REST_Request $request Request Viettel Post gửi tới.
 * @return WP_REST_Response
 */
function devvn_vtp_webhook( WP_REST_Request $request ) {
	$body  = (array) $request->get_json_params();
	$token = isset( $body['TOKEN'] ) ? (string) $body['TOKEN'] : '';
	if ( '' === $token || ! hash_equals( DEVVN_VTP_WEBHOOK_SECRET, $token ) ) {
		return new WP_REST_Response( array( 'message' => 'Sai secret' ), 401 );
	}

	$data  = isset( $body['DATA'] ) ? (array) $body['DATA'] : array();
	$data += array(
		'ORDER_NUMBER'       => '',
		'ORDER_STATUS'       => 0,
		'ORDER_STATUSDATE'   => '',
		'STATUS_NAME'        => '',
		'NOTE'               => '',
		'LOCATION_CURRENTLY' => '',
		'LOCALION_CURRENTLY' => '',
	);
	$ok       = new WP_REST_Response( array( 'message' => 'OK' ), 200 );
	$tracking = sanitize_text_field( (string) $data['ORDER_NUMBER'] );
	$status   = (int) $data['ORDER_STATUS'];
	$order    = ( '' !== $tracking && $status ) ? devvn_vtp_find_order( $tracking ) : null;
	if ( ! $order ) {
		return $ok;
	}

	$final = array( 101, 107, 201, 501, 503, 504 );
	if ( in_array( (int) $order->get_meta( '_vtp_trang_thai' ), $final, true ) ) {
		return $ok;
	}

	$history = $order->get_meta( '_vtp_hanh_trinh' );
	$history = is_array( $history ) ? $history : array();
	$key     = $status . '|' . sanitize_text_field( (string) $data['ORDER_STATUSDATE'] );
	if ( isset( $history[ $key ] ) ) {
		return $ok;
	}
	$history[ $key ] = array(
		'ten'    => sanitize_text_field( (string) $data['STATUS_NAME'] ),
		'ghichu' => sanitize_text_field( (string) $data['NOTE'] ),
		'vitri'  => sanitize_text_field( (string) ( $data['LOCATION_CURRENTLY'] ? $data['LOCATION_CURRENTLY'] : $data['LOCALION_CURRENTLY'] ) ),
	);
	$order->update_meta_data( '_vtp_hanh_trinh', $history );
	$order->update_meta_data( '_vtp_trang_thai', $status );

	$map = array(
		501 => 'completed',
		504 => 'failed',
		101 => 'cancelled',
		107 => 'cancelled',
		201 => 'cancelled',
		503 => 'cancelled',
	);
	if ( isset( $map[ $status ] ) ) {
		$order->set_status( $map[ $status ], 'Viettel Post: ' . $history[ $key ]['ten'] );
	}
	$order->save();
	return $ok;
}
  • URL khai trên Partner: https://ten-mien-cua-ban/wp-json/devvn-vtp/v1/webhook. Đường dẫn tĩnh kiểu mặc định (Plain) thì dùng https://ten-mien-cua-ban/?rest_route=/devvn-vtp/v1/webhook.
  • Sai secret trả 401 để cấu hình sai lộ ra sớm. Tin hợp lệ, kể cả tin trùng, tin không khớp đơn hay tin tới sau trạng thái cuối, đều trả 200 như tài liệu yêu cầu.
  • Tìm theo mã vận đơn (Viettel Post sinh, không trùng) bằng meta_key/meta_value rồi đối chiếu lại meta, vì trên kho đơn cũ (chưa bật HPOS) wc_get_orders() âm thầm bỏ qua meta_query và trả về một đơn mới đặt gần đây.
  • 504 (hàng hoàn về) mình để Thất bại cho tách khỏi đơn huỷ trước khi gửi. Có trạng thái riêng kiểu “Đã hoàn hàng” thì đổi trong $map.

Đổi đơn sang Đã hoàn thành kéo theo email WooCommerce gửi khách ngay trong request, dễ làm phản hồi quá 1 giây. Cho email gửi nền bằng một dòng:

add_filter( 'woocommerce_defer_transactional_emails', '__return_true' );

Bắn thử một bản tin

Chỉ làm trên bản sao website hoặc đơn thử. Mã 200 (lấy hàng thành công) không có trong $map nên chỉ ghi hành trình:

curl -i -X POST 'https://ten-mien-cua-ban/wp-json/devvn-vtp/v1/webhook' \
  -H 'Content-Type: application/json' \
  -d '{"TOKEN":"chuoi-bi-mat-cua-ban","DATA":{"ORDER_NUMBER":"MA_VAN_DON_THU","ORDER_STATUS":200,"STATUS_NAME":"Lấy hàng thành công","ORDER_STATUSDATE":"10/10/2026 08:30:00"}}'

Đúng secret thì dòng đầu là HTTP/1.1 200 OK (hoặc HTTP/2 200), sai secret ra 401. Ra 403, 503 thường là tường lửa chặn; ra 301 là đường dẫn đang bị chuyển hướng.

Dùng plugin: chép đường dẫn, dán secret, chọn trạng thái

Ba bước bên dưới có trong video, bắt đầu ở giây 0:26.

Web dùng plugin Viettel Post cho WooCommerce của mình thì phần nhận webhook có sẵn, bạn chỉ khai cấu hình:

  1. Vào WooCommerce → ViettelPost, tab ViettelPost, mục Cấu hình, kéo xuống khối Cài đặt Webhook.
  2. Chép dòng Đường dẫn API/Webhook (dạng https://ten-mien-cua-ban/wp-admin/admin-ajax.php?action=vtpost_webhook) vào ô Webhook Endpoints trên Partner.
  3. Ô Tham số bí mật: lần đầu mở, plugin điền sẵn một chuỗi ngẫu nhiên nhưng chưa lưu. Bấm Lưu thay đổi, tải lại trang, rồi mới chép chuỗi đó vào ô Secret parameter.
  4. Bấm Kiểm tra kết nối trên Partner.

⚠️ Link Tạo tham số bí mật sinh chuỗi mới. Bấm rồi lưu là chuỗi cũ hết hiệu lực, nhớ cập nhật lại trên Partner.

Ngay dưới là bảng Trạng thái đơn hàng: 36 mã Viettel Post, mỗi dòng chọn một trạng thái WooCommerce hoặc Không đồng bộ trạng thái.

Bảng ánh xạ trạng thái Viettel Post sang trạng thái đơn WooCommerce trong plugin
Mỗi mã trạng thái Viettel Post chọn một trạng thái WooCommerce, hoặc để Không đồng bộ trạng thái
  • Mặc định plugin đổi 501 sang Đã hoàn thành; -15, 101, 107, 503 sang Đã huỷ; 102 sang Đang xử lý; các mã khác chỉ lưu vào hành trình.
  • Khi webhook chuyển đơn sang Đã huỷ, plugin xoá mã vận đơn trên đơn để bạn tạo vận đơn mới được.
  • Bảng mặc định chỉnh lại được theo quy trình shop, ví dụ đưa 505 và 515 sang một trạng thái riêng để kho biết hàng đang quay về.

Mỗi lần nhận tin, plugin lưu trạng thái và lịch sử hành trình vào đơn, bỏ qua tin trùng. Trang tra cứu dùng shortcode [viettel_tracking] đọc chính dữ liệu này để hiện dòng thời gian cho khách. Dev cần can thiệp thì có filter devvn_vtp_to_order_status_{mã}. Chi tiết ở trang webhook và đồng bộ trạng thái trong tài liệu plugin.

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

  1. Website phải gọi được từ Internet, nên site chạy local, đang bảo trì hay khoá mật khẩu thì Viettel Post không gửi vào được.
  2. URL khai sai hoặc bị chuyển hướng (khai http:// trong khi site chuyển sang https://, lệch có/không www, thiếu một ký tự) thì Viettel Post nhận 301 hay 404 thay vì 200; chứng chỉ SSL hết hạn cũng làm request hỏng.
  3. Chế độ chống bot của Cloudflare, tường lửa máy chủ hay plugin bảo mật có thể chặn POST từ máy chủ lạ vào admin-ajax.php hay /wp-json/, khi đó tạo quy tắc cho phép đúng đường dẫn webhook.
  4. Secret trên website và trên Partner phải trùng từng ký tự, không dư khoảng trắng, nên đổi chuỗi trên website thì cập nhật lại Partner.
  5. Với plugin, Tham số bí mật chỉ có hiệu lực sau khi bấm Lưu thay đổi.
  6. Website phải trả 200 trong 1 giây; lỗi PHP, máy chủ quá tải hay plugin khác chạy nặng khi đơn đổi trạng thái đều làm hỏng phản hồi.
  7. Mỗi tài khoản chỉ có một endpoint, nên khi tài khoản đã khai cho website khác thì website khai sau sẽ thay chỗ website khai trước.
  8. Đơn tạo thẳng trên viettelpost.vn, không lưu mã vận đơn vào WooCommerce thì webhook không có đơn nào để cập nhật.
  9. Mã nào trong bảng ánh xạ để Không đồng bộ trạng thái thì hành trình vẫn lưu nhưng trạng thái đơn giữ nguyên, plugin cố ý làm vậy.

Theo mục Câu hỏi thường gặp trên Partner, chức năng Tra cứu đơn hàng ở Bảng điều khiển cho xem trạng thái mới và toàn bộ log webhook của một đơn; thấy thiếu thì dùng Đồng bộ hành trình để đồng bộ lại.

Không muốn tự viết và bảo trì đoạn code trên thì plugin có sẵn endpoint, bảng ánh xạ 36 mã và trang tra cứu hành trình cho khách:

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

Một tài khoản Viettel Post khai webhook cho hai website được không?

Không. Mỗi tài khoản chỉ cấu hình được một webhook endpoint. Hai website thì dùng hai tài khoản, hoặc để một website nhận rồi tự chuyển tiếp cho website kia.

Có phải nhờ Viettel Post bật webhook không?

Không. Shop tự khai ở Cấu hình webhook trên Partner rồi bấm Kiểm tra kết nối. Chỉ khi lỗi không rõ nguyên nhân mới cần liên hệ đội hỗ trợ Viettel Post.

Website lỗi một lúc thì có mất trạng thái không?

Viettel Post gửi lại tối đa 5 lần khi chưa nhận HTTP 200. Vẫn thiếu thì xem log ở Tra cứu đơn hàng trên Partner rồi dùng Đồng bộ hành trình.

Webhook có gửi ảnh giao hàng không?

Có trường POD cho ảnh báo phát (.jpg, lưu 6 tháng), nhưng gói webhook cơ bản chưa trả link ảnh. Muốn nhận thì gửi yêu cầu tới Viettel Post.

Vướng chỗ nào cứ để lại bình luận, nhớ che secret và số điện thoại trước khi dán log nhé. 🚀

Bài viết trong series

Kết nối WooCommerce với Viettel Post

0/3đã đọc
  1. 1 Tích hợp API Viettel Post vào website: token, tính cước, tạo đơn bằng PHP Đang đọc Đã đọc
  2. 2 Kết nối Viettel Post với website: 3 cách cho shop online Đang đọc Đã đọc
  3. 3 Viettel Post webhook: cách khai, dữ liệu gửi về và code PHP mẫu Đang đọc Đã đọc
Sản phẩm nổi bật của Toản
x