Tích hợp API GHN bằng PHP cho WordPress: tính phí, tạo đơn, in, huỷ

Chào anh em! Nhận dự án website tự tính phí và đẩy đơn sang Giao Hàng Nhanh (GHN), việc trước tiên là đi tìm tài liệu, mà bấm vào link api.ghn.vn quen thuộc thì giờ gặp trang 404. Tài liệu GHN đã dời sang chỗ mới, cấu trúc cũng khác: hai bộ danh mục địa chỉ cũ và mới, tính phí dùng mã quận còn tạo đơn nhận tên phường. Theo hết bài, anh em có bộ hàm PHP kiểu WordPress để tích hợp API GHN, từ tính phí ở trang thanh toán tới tạo, in và huỷ vận đơn.

Tích hợp API GHN 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 developer.ghn.vn (đọc ngày 10/10/2026). Plugin nhắc ở cuối bài là sản phẩm mình viết; mấy nhận xét rút từ việc làm plugin thì mình nói rõ là kinh nghiệm. Code bên dưới là mẫu tối giản để anh em nắm luồng, chưa có giao diện, chưa xử lý hết trường hợp: chạy thử ở môi trường staging và trên bản sao website trước khi đưa lên thật.

Tích hợp API GHN: tài liệu ở đâu, hai môi trường gồm gì?

Tài liệu hiện tại nằm ở developer.ghn.vn, có bản tiếng Việt dưới đường dẫn /vi/docs/. Đường dẫn cũ api.ghn.vn/home/docs/detail giờ chuyển sang developer.ghn.vn và trả 404. Trang hỏi đáp “đồng bộ GHN với web bán hàng” trên ghn.vn vẫn chỉ về api.ghn.vn, anh em bỏ qua, cứ theo developer.ghn.vn.

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

  • Môi trường Production ở https://online-gateway.ghn.vn, đơn tạo ra là đơn thật.
  • Môi trường Staging ở https://dev-online-gateway.ghn.vn, tài khoản thử đăng ký ở 5sao.ghn.dev, và theo trang chủ tài liệu thì gọi thử ở đây “không phát sinh đơn thật”.

Mọi đường dẫn có tiền tố /shiip/public-api/, ví dụ /shiip/public-api/v2/shipping-order/create. Staging và Production là hai tài khoản riêng, token riêng: muốn chạy thử thì lấy token của tài khoản staging, token thật không dùng được ở đó và ngược lại. Quy ước dữ liệu: gửi, nhận JSON; tiền tính bằng đồng, khối lượng bằng gram, kích thước bằng cm; thời gian theo ISO 8601.

Token, ShopId và danh sách IP

  • Header Token bắt buộc với mọi API. Shop tự lấy trong tài khoản: khachhang.ghn.vn → bấm tên tài khoản → Thông tin tài khoản → tab Bảo mật → Token API & IP tin cậy → Quản lý (mình kiểm ngày 11/10/2026), hoặc Developer Portal → Quản lý token theo tài liệu GHN.
  • Header ShopId (số nguyên) cần cho đa số API. Lấy bằng API v2/shop/all ở bước 1 bên dưới.
  • Ô Danh sách IP được phép sử dụng để trống thì mọi IP gọi được, đã có IP thì chỉ các IP đó gọi được, nên bật danh sách mà quên IP máy chủ website là ăn lỗi 401.
Mục Token API & IP tin cậy trong tab Bảo mật của tài khoản GHN
Token API và danh sách IP nằm ở mục Token API & IP tin cậy. Ảnh: giao diện khachhang.ghn.vn, 10/2026.

Token phải nằm ở phía máy chủ: để trong wp-config.php, không đưa vào JavaScript chạy ở trình duyệt, không commit lên Git, không ghi ra log. Từng bước lấy token, OTP và cách xử lý lỗi 401 có ở bài lấy token GHN.

Hàm gọi API GHN dùng chung

Cấu hình để trong wp-config.php, đổi môi trường bằng một hằng số. ShopId điền sau khi chạy bước 1:

define( 'DEVVN_GHN_STAGING', true );
define( 'DEVVN_GHN_TOKEN', 'YOUR_TOKEN' );
define( 'DEVVN_GHN_SHOP_ID', 0 );

Hàm dưới phục vụ mọi lời gọi trong bài. GHN báo lỗi qua trường code trong JSON (kèm code_message), nên hàm kiểm cả mã HTTP lẫn code. Tài liệu GHN cũng khuyên xử lý theo code / code_message, đừng dựa vào câu chữ trong message:

/**
 * Tên miền API GHN theo môi trường đang chọn.
 *
 * @return string
 */
function devvn_ghn_host() {
	return DEVVN_GHN_STAGING ? 'https://dev-online-gateway.ghn.vn' : 'https://online-gateway.ghn.vn';
}

/**
 * Gọi API GHN, trả về phần data hoặc WP_Error.
 *
 * @param string   $method  'GET' hoặc 'POST'.
 * @param string   $path    Đường dẫn sau /shiip/public-api/, ví dụ 'v2/shipping-order/fee'.
 * @param array    $data    Body JSON (POST) hoặc tham số query (GET).
 * @param int|null $shop_id ShopId gửi trong header; null = DEVVN_GHN_SHOP_ID, 0 = không gửi.
 * @return mixed|WP_Error
 */
function devvn_ghn_request( $method, $path, $data = array(), $shop_id = null ) {
	$url     = devvn_ghn_host() . '/shiip/public-api/' . ltrim( $path, '/' );
	$shop_id = null === $shop_id ? (int) DEVVN_GHN_SHOP_ID : (int) $shop_id;
	$args    = array(
		'timeout' => 10,
		'headers' => array(
			'Token'        => DEVVN_GHN_TOKEN,
			'Content-Type' => 'application/json',
		),
	);
	if ( $shop_id > 0 ) {
		$args['headers']['ShopId'] = (string) $shop_id;
	}
	if ( 'GET' === $method ) {
		$res = wp_remote_get( add_query_arg( $data, $url ), $args );
	} else {
		$args['body'] = wp_json_encode( $data );
		$res          = wp_remote_post( $url, $args );
	}
	if ( is_wp_error( $res ) ) {
		return $res;
	}
	$http = (int) wp_remote_retrieve_response_code( $res );
	$body = wp_remote_retrieve_body( $res );
	$json = json_decode( $body, true );
	if ( ! is_array( $json ) ) {
		return 200 === $http ? null : new WP_Error( 'ghn_http_' . $http, $body, array( 'status' => $http ) );
	}
	$code = isset( $json['code'] ) ? (int) $json['code'] : $http;
	if ( 200 !== $http || 200 !== $code ) {
		$key = empty( $json['code_message'] ) ? 'ghn_http_' . $http : $json['code_message'];
		$msg = isset( $json['message'] ) ? $json['message'] : $body;
		return new WP_Error( $key, $msg, array( 'status' => $http ) );
	}
	return isset( $json['data'] ) ? $json['data'] : null;
}
  • timeout đặt rõ 10 giây: API chậm thì request tự bỏ, không kéo cả trang thanh toán treo theo.
  • Lỗi trả về WP_Error có mã là code_message (ví dụ USER_ERR_COMMON) khi GHN gửi kèm, để code gọi rẽ nhánh theo mã.
  • HTTP 200 mà body rỗng thì hàm trả null. Có API của GHN báo “không tìm thấy” theo đúng kiểu này (xem bước 3).

Bước 1: lấy danh sách shop (ShopId)

GET v2/shop/all chỉ cần Token, nên gọi với tham số cuối là 0 để không gửi ShopId:

$data = devvn_ghn_request( 'GET', 'v2/shop/all', array(), 0 );
if ( ! is_wp_error( $data ) && ! empty( $data['shops'] ) ) {
	foreach ( $data['shops'] as $shop ) {
		error_log( sprintf( 'ShopId %d | %s | district_id %d | ward_code %s', $shop['_id'], $shop['name'], $shop['district_id'], $shop['ward_code'] ) );
	}
}

Mỗi shop có _id (chính là ShopId), tên, cùng district_id và ward_code của địa chỉ lấy hàng. Hai giá trị sau dùng làm điểm gửi khi lấy dịch vụ và tính phí. Đừng nhầm với “ID khách hàng” trên web GHN: đó là mã tài khoản, không phải ShopId.

Bước 2: danh mục địa chỉ, bộ cũ 3 cấp và bộ mới 2 cấp

Từ 01/07/2025, GHN chạy song song đơn vị hành chính cũ (Tỉnh → Quận/Huyện → Phường/Xã) và mới (Tỉnh → Phường/Xã) cho địa chỉ lấy, giao, trả hàng. Phía API có hai bộ danh mục:

  • Bộ cũ, tài liệu gọi là “mô hình địa chỉ cũ”, gồm GET master-data/province, master-data/district?province_id=, master-data/ward?district_id=. DistrictID là số, WardCode là chuỗi (có thể có số 0 ở đầu, đừng ép sang số). Quận và phường có SupportType: 0 khoá, 1 chỉ lấy, 2 chỉ giao, 3 lấy và giao.
  • Bộ mới 2 cấp gồm GET v3/master-data/province/all (hiện 34 tỉnh/thành) và GET v3/master-data/ward/all-by-province-id?province_id=, limit tối đa 200. Trường name dùng nguyên văn cho to_province_name / to_ward_name khi tạo đơn với is_new_to_address: true; extension_names chứa các biến thể không dấu, viết tắt để so khớp.

⚠️ Bẫy của bộ mới: sai hoặc thiếu province_id vẫn trả HTTP 200, chỉ có data là null. Hàm dưới bắt trường hợp đó và cache kết quả, vì danh mục hiếm khi đổi mà trang thanh toán thì gọi liên tục:

/**
 * Lấy phường/xã bộ 2 cấp của một tỉnh, cache 1 ngày.
 *
 * @param int $province_id Giá trị _id lấy từ v3/master-data/province/all.
 * @return array|WP_Error
 */
function devvn_ghn_wards_v3( $province_id ) {
	$key   = 'devvn_ghn_wards_' . (int) $province_id;
	$wards = get_transient( $key );
	if ( false !== $wards ) {
		return $wards;
	}
	$wards = devvn_ghn_request(
		'GET',
		'v3/master-data/ward/all-by-province-id',
		array(
			'province_id' => (int) $province_id,
			'limit'       => 200,
		)
	);
	if ( is_wp_error( $wards ) ) {
		return $wards;
	}
	if ( empty( $wards ) ) {
		return new WP_Error( 'ghn_bad_province', 'GHN trả 200 nhưng data rỗng: kiểm lại province_id' );
	}
	set_transient( $key, $wards, DAY_IN_SECONDS );
	return $wards;
}

Mỗi phần tử có _id và name; danh sách tỉnh từ v3/master-data/province/all cũng có _id để truyền vào province_id.

Bước 3: lấy dịch vụ và tính phí ship

Hai API đi cùng nhau: available-services cho biết tuyến này đi được gói nào, fee tính tiền cho gói đó. Cả hai vẫn dùng mã quận/huyện và mã phường/xã của bộ cũ.

  • POST v2/shipping-order/available-services, body shop_id, from_district, to_district. Trả mảng gồm service_id, short_name (“Hàng nhẹ”, “Hàng nặng”), service_type_id. HTTP 200 mà body rỗng nghĩa là không tìm thấy shop.
  • service_type_id: 2 là hàng nhẹ (tổng dưới 20 kg), 5 là hàng nặng (từ 20 kg hoặc nhiều kiện). Tính phí và tạo đơn dùng service_type_id; service_id chỉ để đối chiếu.
  • POST v2/shipping-order/fee: bắt buộc weight (khác 0), to_district_id, to_ward_code, service_type_id; tuỳ chọn from_district_id, from_ward_code (mặc định địa chỉ shop), kích thước, insurance_value, cod_value, coupon, items. Trả total, service_fee, insurance_fee, cod_fee, pick_station_fee, r2s_fee cùng các khoản phí khác.
/**
 * Lấy phí GHN của từng gói khả dụng tới một địa chỉ (mã bộ cũ).
 *
 * @param array  $shop        Một phần tử của v2/shop/all (cần _id, district_id, ward_code).
 * @param int    $to_district DistrictID nơi nhận.
 * @param string $to_ward     WardCode nơi nhận (chuỗi).
 * @param array  $parcel      weight (gram), length, width, height (cm), insurance_value (đồng).
 * @return array|WP_Error Mảng short_name => tổng phí.
 */
function devvn_ghn_rates( array $shop, $to_district, $to_ward, array $parcel ) {
	$shop_id  = (int) $shop['_id'];
	$services = devvn_ghn_request(
		'POST',
		'v2/shipping-order/available-services',
		array(
			'shop_id'       => $shop_id,
			'from_district' => (int) $shop['district_id'],
			'to_district'   => (int) $to_district,
		),
		$shop_id
	);
	if ( is_wp_error( $services ) ) {
		return $services;
	}
	if ( empty( $services ) ) {
		return new WP_Error( 'ghn_no_service', 'Không có gói nào cho tuyến này hoặc không tìm thấy shop' );
	}
	$rates = array();
	foreach ( $services as $service ) {
		$fee = devvn_ghn_request(
			'POST',
			'v2/shipping-order/fee',
			array_merge(
				$parcel,
				array(
					'service_type_id'  => (int) $service['service_type_id'],
					'from_district_id' => (int) $shop['district_id'],
					'from_ward_code'   => (string) $shop['ward_code'],
					'to_district_id'   => (int) $to_district,
					'to_ward_code'     => (string) $to_ward,
				)
			),
			$shop_id
		);
		if ( ! is_wp_error( $fee ) ) {
			$rates[ $service['short_name'] ] = (int) $fee['total'];
		}
	}
	return $rates;
}

Hàm gọi fee cho từng gói, cũng là cách plugin của mình làm ở trang thanh toán. Vài kinh nghiệm khi đưa phần này lên trang thanh toán:

  • Cache theo đúng bộ tham số gửi đi (ví dụ md5 của payload). Plugin của mình giữ danh sách dịch vụ và từng báo giá 2 ngày, chỉ lưu khi GHN trả thành công.
  • Sản phẩm thiếu cân nặng là weight bằng 0, GHN trả USER_ERR_COMMON. Luôn có đường lùi như dòng “phí báo sau” để khách vẫn đặt được hàng.
  • Ngày giao dự kiến lấy bằng POST v2/shipping-order/leadtime, trả về leadtime dạng Unix timestamp (giây) cùng khoảng ngày from_estimate_date / to_estimate_date; API này cũng dùng service_type_id.
  • Xem trước đơn bằng POST v2/shipping-order/preview: API nhận cùng body và kiểm tra y như tạo đơn nhưng không tạo (order_code rỗng), trả phí và expected_delivery_time, hợp để nhân viên xem phí, ngày giao trước khi bấm tạo.

Trang thanh toán dùng địa chỉ 2 cấp thì tính phí thế nào?

Đây là chỗ tài liệu GHN chưa rõ. API fee, leadtime và available-services vẫn đòi mã quận/huyện và mã phường/xã kiểu cũ; chỉ có tạo đơn và xem trước đơn nhận địa chỉ theo tên kèm is_new_to_address. Tài liệu không nói cách tính phí trực tiếp bằng địa chỉ 2 cấp. Hướng mình thấy hợp lý là gọi preview với địa chỉ mới để lấy phí, nhưng đó là suy luận từ tài liệu: anh em thử kỹ ở staging với nhiều địa chỉ thật trước khi dùng. Công cụ ước tính cước công khai của GHN hiện cũng ghi “Vui lòng sử dụng địa chỉ cũ để tra cứu”.

Bước 4: tạo đơn GHN, dùng client_order_code để gọi lại an toàn

POST v2/shipping-order/create tạo vận đơn thật trên Production. Trường bắt buộc:

  • Người nhận cần to_name, to_phone, to_address, to_ward_name, to_province_name; riêng to_district_name bắt buộc khi is_new_to_address là false (mặc định), để trống khi true.
  • Kiện hàng cần weight, length, width, height, với weight tối đa 50.000 g và mỗi chiều tối đa 200 cm.
  • Dịch vụ và thanh toán cần service_type_id (2 hoặc 5), payment_type_id (1 shop trả phí, 2 người nhận trả) và required_note là một trong KHONGCHOXEMHANG (không cho xem), CHOXEMHANGKHONGTHU (cho xem, không thử), CHOTHUHANG (cho xem và thử).
  • Hàng hoá cần content (tối đa 2.000 ký tự) khi không gửi items[]; items[] bắt buộc khi service_type_id là 5, mỗi item phải có length, width, height, weight.

Điểm lấy hàng mặc định là địa chỉ của shop trong header ShopId (pick_station_id bằng 0 nghĩa là GHN tới lấy tại địa chỉ shop). Đoạn dưới tạo đơn với địa chỉ 2 cấp; tên phường/xã, tỉnh lấy đúng chữ trong trường name của danh mục v3 ở bước 2. Khối lượng, kích thước để số mẫu, thực tế anh em cộng từ sản phẩm trong đơn:

/**
 * Tạo vận đơn GHN cho đơn WooCommerce với địa chỉ 2 cấp, lưu mã vận đơn vào đơn.
 *
 * @param WC_Order $order    Đơn hàng.
 * @param string   $ward     Tên phường/xã đúng nguyên văn trường name của danh mục v3.
 * @param string   $province Tên tỉnh/thành đúng nguyên văn trường name của danh mục v3.
 * @return string|WP_Error Mã vận đơn GHN.
 */
function devvn_ghn_create_order( WC_Order $order, $ward, $province ) {
	$existing = $order->get_meta( '_devvn_ghn_order_code' );
	if ( $existing ) {
		return $existing;
	}
	$is_cod = 'cod' === $order->get_payment_method();
	$data   = devvn_ghn_request(
		'POST',
		'v2/shipping-order/create',
		array(
			'client_order_code' => 'WC-' . $order->get_id(),
			'to_name'           => $order->get_formatted_billing_full_name(),
			'to_phone'          => $order->get_billing_phone(),
			'to_address'        => implode( ', ', array( $order->get_billing_address_1(), $ward, $province ) ),
			'to_ward_name'      => $ward,
			'to_province_name'  => $province,
			'is_new_to_address' => true,
			'weight'            => 1200,
			'length'            => 30,
			'width'             => 20,
			'height'            => 10,
			'service_type_id'   => 2,
			'payment_type_id'   => 1,
			'required_note'     => 'CHOXEMHANGKHONGTHU',
			'cod_amount'        => $is_cod ? (int) $order->get_total() : 0,
			'insurance_value'   => min( 5000000, (int) $order->get_subtotal() ),
			'content'           => 'Đơn hàng #' . $order->get_order_number(),
		)
	);
	if ( is_wp_error( $data ) ) {
		return $data;
	}
	$order->update_meta_data( '_devvn_ghn_order_code', $data['order_code'] );
	$order->save();
	return $data['order_code'];
}
  • Gửi lại cùng client_order_code (tối đa 50 ký tự, không trùng trong một shop) thì GHN trả order_code đã tạo thay vì tạo đơn mới. Nhờ vậy request bị timeout giữa chừng có thể gửi lại an toàn, không sinh hai vận đơn. Vì mã không được trùng, đơn đã huỷ muốn tạo lại thì nên đặt mã mới (ví dụ thêm hậu tố -2).
  • cod_amount mặc định 0, trang Tạo đơn ghi tối đa 50.000.000 đ; insurance_value tối đa 5.000.000 đ. Đoạn trên để shop trả phí (payment_type_id 1) và thu hộ bằng tổng đơn, tức gồm cả phí ship khách đã thấy trên web.
  • pick_shift là mảng mã ca lấy từ GET v2/shift/date. Mã sinh theo ngày nên luôn gọi lấy danh sách, đừng hard-code; không gửi thì mặc định ca gần kề. pick_station_id (mã bưu cục) dùng khi shop tự mang hàng ra bưu cục.
  • cod_failed_amount là tiền thu của người nhận khi giao thất bại, ứng với tính năng “GTB – Thu tiền” trên app GHN.
  • Phản hồi có data.order_code (mã vận đơn), data.fee, data.total_fee, data.expected_delivery_time. Webhook bắt đầu bắn ngay khi đơn có mã này.
  • Tra lại một đơn: GET v2/shipping-order/detail?order_code= hoặc detail-by-client-code?client_order_code=.

Bước 5: in vận đơn

In gồm hai bước: gọi POST v2/a5/gen-token với order_codes[] (tối đa 10.000 mã một lần) để lấy token in, rồi gắn token vào link in của khổ cần dùng: A5 là printA5, 80×80 là print80x80, 52×70 là print52x70. Token in hết hạn sau khoảng 30 phút, nên tạo ngay trước khi in:

/**
 * Tạo link in vận đơn GHN; token in sống khoảng 30 phút nên gọi ngay trước khi in.
 *
 * @param string $order_code Mã vận đơn GHN.
 * @param string $size       'printA5', 'print80x80' hoặc 'print52x70'.
 * @return string|WP_Error
 */
function devvn_ghn_print_url( $order_code, $size = 'printA5' ) {
	if ( ! in_array( $size, array( 'printA5', 'print80x80', 'print52x70' ), true ) ) {
		$size = 'printA5';
	}
	$data = devvn_ghn_request( 'POST', 'v2/a5/gen-token', array( 'order_codes' => array( $order_code ) ) );
	if ( is_wp_error( $data ) ) {
		return $data;
	}
	return devvn_ghn_host() . '/a5/public-api/' . $size . '?token=' . rawurlencode( $data['token'] );
}

Gen-token chạy kiểu tất cả hoặc không: một mã sai là cả lô không có token (ORDER_NOT_FOUND). Staging dùng cùng đường dẫn trên dev-online-gateway.ghn.vn, hàm trên đã tự đổi theo hằng số. Chọn khổ giấy, máy in nhiệt và tuỳ chọn hiển thị trên vận đơn mình viết ở bài in vận đơn GHN.

Bước 6: huỷ đơn và các thao tác sau khi tạo

POST v2/switch-status/cancel nhận order_codes[], kèm reason_code tuỳ chọn: GHN-CO001 (lấy hàng quá lâu), GHN-CO002 (hết hàng), GHN-CO003 (người nhận hết nhu cầu), GHN-CANCEL-OTHER, và reason để ghi thêm.

/**
 * Huỷ một vận đơn GHN, trả true khi GHN xác nhận đã huỷ.
 *
 * @param string $order_code Mã vận đơn GHN.
 * @return bool|WP_Error
 */
function devvn_ghn_cancel( $order_code ) {
	$data = devvn_ghn_request(
		'POST',
		'v2/switch-status/cancel',
		array(
			'order_codes' => array( $order_code ),
			'reason_code' => 'GHN-CO003',
		)
	);
	if ( is_wp_error( $data ) ) {
		return $data;
	}
	return ! empty( $data[0]['result'] );
}
  • Huỷ cũng là tất cả hoặc không: một mã sai là cả lô bị từ chối. Mã hợp lệ nhưng trạng thái không cho huỷ nữa thì phần tử tương ứng có result: false.
  • POST v2/shipping-order/update cập nhật một phần; trường sửa được tuỳ trạng thái (ready_to_pick sửa được hết, đơn đã giao, đã huỷ, thất lạc… không sửa gì). Đổi cod_amount qua API này cần otp gửi về số điện thoại tài khoản.
  • POST v2/switch-status/return (trả hàng) và v2/switch-status/storing (giao lại) xử lý từng đơn và luôn trả HTTP 200, nên phải kiểm result của từng phần tử.

Nhận trạng thái đơn qua webhook

Đừng hẹn giờ gọi API detail cho từng đơn. GHN có webhook: khi đơn đổi trạng thái, GHN gửi POST JSON tới URL bạn khai. Theo tài liệu GHN tháng 10/2026, URL được dán vào mục Cấu hình webhook trên Developer Portal (menu tài khoản → Cấu hình webhook → tab Order → Tạo webhook), khoảng 15 phút sau có hiệu lực; staging và production cấu hình riêng. Tài khoản chưa thấy mục này thì gửi đường dẫn cho GHN qua CSKH để được khai giúp.

  • Trả HTTP 2xx là xong. 4xx (trừ 408, 429) bị coi là thất bại hẳn, không gửi lại; 5xx, 408, 429 hay timeout thì GHN gửi lại theo lịch giãn dần.
  • Cùng một sự kiện có thể tới nhiều lần: khử trùng theo OrderCode + Type + Time.
  • Tài liệu không nêu chữ ký số cho payload; xác thực bằng khoá bí mật tự đặt qua mục Header tùy chỉnh.

Webhook có nhiều chi tiết riêng nên mình tách thành bài khác. Payload, bảng mã trạng thái và code REST route nhận webhook có ở bài webhook GHN.

Lỗi gọi API GHN thường gặp

Lỗi, dấu hiệuNguyên nhân hay gặp
HTTP 401 “Authorization header is required!”Thiếu header Token, hoặc token rỗng.
HTTP 401 dù token đúngĐã bật danh sách IP nhưng IP máy chủ không có trong đó.
USER_ERR_COMMONThiếu trường bắt buộc, dữ liệu sai, weight bằng 0, required_note sai giá trị.
PHONE_INVALIDSố điện thoại người nhận sai định dạng.
CLIENT_NOT_OWNER_OF_SHOPShopId không thuộc tài khoản của token.
ROUTE_NOT_FOUND_SERVICEKhông có tuyến cho cặp quận/huyện với dịch vụ đã chọn.
SERVICE_NOT_FOUND_CONFIG_FEEChưa có cấu hình phí cho dịch vụ đã chọn; thử gói khác hoặc hỏi GHN.
SERVER_ERROR_COMMON (500)Lỗi phía GHN; thử lại sau, đừng gọi dồn.
HTTP 200, data nullSai hoặc thiếu province_id ở danh mục phường/xã mới.
HTTP 200, body rỗngavailable-services không tìm thấy shop.
ORDER_NOT_FOUNDGen-token có mã vận đơn sai, cả lô không có token in.
HTTP 422 SECURITY_CHECK_FAILEDAPI bưu cục đòi header captcha với một số tài khoản.
OTP_NOT_VALIDĐổi cod_amount qua API update mà OTP sai.

Rẽ nhánh theo code_message chứ đừng so chuỗi message: câu chữ có thể đổi bất cứ lúc nào. Khi nhờ người khác xem lỗi, gửi payload và phản hồi nhưng nhớ xoá token, số điện thoại khách.

Những chỗ tài liệu GHN chưa rõ, nên thử trước ở staging

  • Tài liệu chưa mô tả cách tính phí với địa chỉ 2 cấp, như đã nói ở bước 3.
  • Hai trang ghi giới hạn COD khác nhau: trang Tạo đơn ghi cod_amount tối đa 50.000.000 đ, còn trang Cập nhật COD ghi tối đa 5.000.000 đ.
  • API update bắt buộc otp khi đổi cod_amount, còn API updateCOD lại không nhắc tới OTP.
  • Trang Tạo đơn chỉ có payment_type_id 1 và 2, còn trang Thông tin đơn ghi “1 (và 4, 5)” là shop trả.
  • Hình minh hoạ ở trang chủ tài liệu dùng endpoint in khác với trang In đơn (trang này ghi v2/a5/gen-token), và mình theo trang In đơn.
  • Số lần gửi lại webhook ở phần cấu hình cho chọn 1–5 lần, còn bảng lịch chờ liệt kê tới 11 mốc.
  • Các trang API ghi token “do GHN cấp”, còn trang Lấy Token lại nói shop tự lấy, không cần nhờ GHN.

Tài liệu chỉ có API tạo từng đơn (huỷ, in, trả, giao lại mới nhận mảng mã), nên tạo nhiều đơn là gọi lần lượt. Cache danh mục và báo giá để giảm số lần gọi.

Web là WooCommerce: tự code hay dùng plugin?

Cách nối bằng plugin, không cần code, có trong video dưới.

Gọi được API mới xong một phần việc. Phần tốn công là mọi thứ xung quanh: ô địa chỉ 2 cấp ở trang thanh toán, nhiều kho, cache phí và đường lùi khi API chậm, hộp thoại tạo đơn cho nhân viên, in vận đơn, webhook, email báo mã vận đơn…

  • Tự code hợp khi làm hệ thống riêng (không phải WooCommerce), hoặc chỉ cần một API lẻ như tính phí.
  • Dùng plugin hợp khi web là WooCommerce và cần chạy ngay.

Với plugin kết nối GHN với WooCommerce của mình, những bước ở trên đã có sẵn:

  • Phần kết nối chỉ cần chọn Môi trường Product (khachhang.ghn.vn) hoặc Test (5sao.ghn.dev) rồi dán Token Key, không có ô ShopId vì plugin lấy danh sách kho qua v2/shop/all khi bấm “Đồng bộ kho hàng từ GHN”.
  • Ở trang thanh toán, plugin gọi available-services rồi fee cho từng gói như bước 3 (cache 2 ngày); GHN không trả được giá thì hiện “Phí vận chuyển sẽ báo sau.” để khách vẫn đặt hàng.
  • Sau khi có đơn, plugin cho tạo vận đơn từ hộp thoại trong đơn (điền sẵn kho, khối lượng, tiền thu hộ, gói cước), in khổ A5, 52×70 hoặc 80×80, huỷ vận đơn, và nhận webhook để đơn bị huỷ chuyển Đã huỷ, giao xong chuyển Hoàn thành.
  • Dev muốn tuỳ biến mà không sửa lõi thì có các filter như ghn_get_fee_args, ghn_pre_data_before_create_order và action devvn_after_ghn_create_order, xem danh sách ở trang hook của plugin GHN.
Tab Cài đặt GHN của plugin WooCommerce: chọn Môi trường Product hoặc Test và dán Token Key
Với plugin, phần kết nối là chọn môi trường và dán token. Token trong ảnh đã che, dữ liệu mẫu.

Plugin chính tạo vận đơn theo từng đơn, nhân viên kiểm rồi bấm. Muốn đơn tự đẩy sang GHN theo trạng thái hay đăng nhiều đơn một lúc thì cần addon đăng đơn tự động/hàng loạt, mua thêm và có license riêng. Chi tiết phần kết nối có ở trang kết nối API GHN trong tài liệu plugin. So sánh các cách kết nối GHN với website (nền tảng có sẵn, tự code, plugin) có ở bài kết nối GHN với website.

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

API GHN có môi trường để chạy thử không?

Có. Staging ở dev-online-gateway.ghn.vn, tài khoản thử đăng ký ở 5sao.ghn.dev, token riêng với tài khoản thật. Theo trang chủ tài liệu GHN, gọi thử ở staging không phát sinh đơn thật.

Tài liệu api.ghn.vn cũ đi đâu rồi?

Đã chuyển sang developer.ghn.vn. Đường dẫn trang tài liệu cũ giờ trả 404, nên anh em tra endpoint ở developer.ghn.vn (bản tiếng Việt dưới /vi/docs/).

Tạo đơn có cần quận/huyện không?

Gửi is_new_to_address: true thì không: chỉ cần to_ward_name và to_province_name theo danh mục mới. Để mặc định (false) thì phải có to_district_name. Riêng tính phí bằng API fee vẫn cần mã quận và mã phường kiểu cũ.

Anh em tích hợp mà vướng chỗ nào cứ để lại bình luận, nhớ che token, ShopId và số điện thoại trước khi dán log nhé. Chúc anh em chạy thử trên staging là ra mã vận đơn ngay! 🚀

Bài viết trong series

Kết nối WooCommerce với Giao Hàng Nhanh

0/9đã đọc
  1. 1 Đăng ký tài khoản GHN cho shop: từng bước trên web và app Đang đọc Đã đọc
  2. 2 Lấy API token GHN và ShopId: từng bước, IP tin cậy và lỗi 401 Đang đọc Đã đọc
  3. 3 Kết nối GHN với website: nền tảng, API hay plugin WooCommerce Đang đọc Đã đọc
  4. 4 Cách tạo đơn GHN trên máy tính, điện thoại, Excel và từ website Đang đọc Đã đọc
  5. 5 Cách tính phí ship GHN: khối lượng quy đổi, bảng giá, phụ phí Đang đọc Đã đọc
  6. 6 Phí COD GHN: phí thu hộ, lịch chuyển tiền và đối soát Đang đọc Đã đọc
  7. 7 In vận đơn GHN: chọn khổ A5, 80 x 80, 52 x 70 và cách in từng bước Đang đọc Đã đọc
  8. 8 Webhook GHN: tự cập nhật trạng thái đơn WooCommerce, kèm code PHP Đang đọc Đã đọc
  9. 9 Tích hợp API GHN bằng PHP cho WordPress: tính phí, tạo đơn, in, huỷ Đang đọc Đã đọc
Sản phẩm nổi bật của Toản
x