Tích hợp API Viettel Post vào website: token, tính cước, tạo đơn bằng PHP

Chào anh em! Ai từng nhận dự án “web phải tự tính phí và đẩy đơn sang Viettel Post” chắc đã mở tài liệu ở partner2.viettelpost.vn và thấy hơi rối: nhiều API, hai bộ địa danh cũ và mới, token ngắn hạn, token dài hạn… Đọc xong bài này, anh em tự viết được phần tích hợp API Viettel Post vào website bằng PHP (theo kiểu WordPress), từ lúc lấy token tới lúc có mã vận đơn và nhận trạng thái đơn về web.

Tích hợp API Viettel Post 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 partner2.viettelpost.vn/document. Mấy chỗ “bẫy” không ghi trong tài liệu thì mình rút ra từ plugin Viettel Post cho WooCommerce mình làm và bảo trì từ 2018, có nói rõ chỗ nào là kinh nghiệm.

Trước khi code: tài khoản và hai môi trường API Viettel Post

Xem nhanh 6 bước gọi API trong video 2 phút, rồi đọc chi tiết từng bước bên dưới.

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

API Viettel Post dùng chính tài khoản Viettel Post của shop. Tài khoản phải đăng nhập được trên trang Partner (partner2.viettelpost.vn). Viettel Post có hai môi trường, code giống nhau, chỉ khác domain:

  • Môi trường Development (chạy thử) có domain https://partnerdev.viettelpost.vn, tài khoản Development tạo riêng ở trang Partner, mục Bảng điều khiển → Thông tin tài khoản → Development → Tạo tài khoản.
  • Môi trường Production (thật) có domain https://partner.viettelpost.vn, đơn tạo ra ở đây là đơn thật, xem và quản lý trên viettelpost.vn.
Trang Thông tin tài khoản Partner Viettel Post với hai môi trường Production và Development
Trang Partner Viettel Post: thẻ Production và Development (đã che thông tin tài khoản)

Mọi request gửi JSON, kèm header Token (trừ lúc đăng nhập) và Content-Type: application/json. Phản hồi thành công có "error": false, lỗi thì "error": true kèm message. Mình để tài khoản trong wp-config.php (đừng lưu mật khẩu thô vào database) và đổi môi trường bằng một hằng số:

define( 'DEVVN_VTP_DEV', true );
define( 'DEVVN_VTP_USER', 'so-dien-thoai-tai-khoan' );
define( 'DEVVN_VTP_PASS', 'mat-khau' );

Hàm gọi API dùng chung cho cả bài. Nó trả về phần data khi thành công, hoặc WP_Error chứa đúng thông báo lỗi của Viettel Post:

/**
 * Gọi API Viettel Post (POST JSON), trả về phần data hoặc WP_Error.
 *
 * @param string $path  Đường dẫn sau /v2/, ví dụ 'order/getPrice'.
 * @param array  $body  Dữ liệu gửi đi.
 * @param string $token Token xác thực, để trống khi đăng nhập.
 * @return array|WP_Error
 */
function devvn_vtp_request( $path, $body = array(), $token = '' ) {
	$base    = DEVVN_VTP_DEV ? 'https://partnerdev.viettelpost.vn/v2/' : 'https://partner.viettelpost.vn/v2/';
	$headers = array( 'Content-Type' => 'application/json' );
	if ( $token ) {
		$headers['Token'] = $token;
	}
	$res = wp_remote_post(
		$base . $path,
		array(
			'headers' => $headers,
			'body'    => wp_json_encode( $body ),
			'timeout' => 20,
		)
	);
	if ( is_wp_error( $res ) ) {
		return $res;
	}
	$json = json_decode( wp_remote_retrieve_body( $res ), true );
	if ( ! is_array( $json ) || ! empty( $json['error'] ) ) {
		$msg = isset( $json['message'] ) ? $json['message'] : 'Phản hồi không hợp lệ';
		return new WP_Error( 'vtp_api', is_array( $msg ) ? implode( '; ', $msg ) : $msg );
	}
	return isset( $json['data'] ) ? $json['data'] : $json;
}

Bước 1: lấy token Viettel Post (token dài hạn 1 năm)

Theo tài liệu “Token xác thực”, lấy token bằng tài khoản Partner đi qua hai bước:

  1. Gọi user/Login với USERNAME, PASSWORD để nhận token ngắn hạn.
  2. Gọi user/ownerconnect, header Token là token vừa nhận, body vẫn là USERNAME, PASSWORD, để nhận token dài hạn có thời hạn 1 năm.

Token dài hạn thì cất lại dùng cho mọi request, đừng đăng nhập ở mỗi lần tính cước. Mình cache 300 ngày cho chắc, tới hạn tự lấy lại:

/**
 * Lấy token dài hạn Viettel Post, cache lại để không đăng nhập mỗi request.
 *
 * @return string|WP_Error
 */
function devvn_vtp_token() {
	$cached = get_transient( 'devvn_vtp_token' );
	if ( $cached ) {
		return $cached;
	}
	$cred  = array(
		'USERNAME' => DEVVN_VTP_USER,
		'PASSWORD' => DEVVN_VTP_PASS,
	);
	$short = devvn_vtp_request( 'user/Login', $cred );
	if ( is_wp_error( $short ) ) {
		return $short;
	}
	$long = devvn_vtp_request( 'user/ownerconnect', $cred, $short['token'] );
	if ( is_wp_error( $long ) ) {
		return $long;
	}
	set_transient( 'devvn_vtp_token', $long['token'], 300 * DAY_IN_SECONDS );
	return $long['token'];
}

Viettel Post còn một cách khác: tạo token trên viettelpost.vn ở trang Cấu hình tài khoản → Thêm mới token (xác nhận bằng OTP), rồi đổi sang token Partner bằng API user/LoginVTP. Cách này hợp khi bạn không muốn giữ mật khẩu tài khoản trong code.

Bước 2: địa danh, chọn đúng bộ cũ (3 cấp) hay mới (2 cấp)

API Viettel Post không nhận tên tỉnh, tên phường bằng chữ (trừ nhóm API “địa chỉ chi tiết”), mà nhận ID địa danh của Viettel Post. Từ sau sáp nhập tỉnh thành, tài liệu tách hai bộ danh mục:

  • Danh mục địa danh cũ (V2) có ba cấp Tỉnh/TP, Quận/Huyện, Phường/Xã nên phải gửi đủ *_PROVINCE, *_DISTRICT, *_WARD.
  • Danh mục địa danh mới (V3) có hai cấp Tỉnh/TP và Phường/Xã, mẫu tạo đơn trong tài liệu gửi *_DISTRICT là null và chỉ dùng *_PROVINCE + *_WARD.

⚠️ Mỗi địa chỉ phải lấy trọn một bộ: đừng lấy ID tỉnh của bộ này ghép với ID phường của bộ kia. Viettel Post có hẳn mã lỗi “Incorrect data: RECEIVER_PROVINCE with address old or new” cho trường hợp địa chỉ không đúng định dạng cũ hoặc mới. Trang thanh toán của bạn dùng 2 cấp thì lưu ID Tỉnh và ID Phường/Xã theo bộ V3. Bên WooCommerce, chuyện đổi trang thanh toán sang 2 cấp mình viết riêng ở bài WooCommerce địa chỉ 2 cấp.

Bước 3: lấy danh sách dịch vụ khả dụng (getPriceAll)

Mỗi tài khoản, mỗi tuyến đường có bộ dịch vụ khác nhau, nên trước khi tính cước hãy hỏi Viettel Post “tuyến này đi được gói nào”. API order/getPriceAll trả về danh sách dịch vụ chính kèm cước và dịch vụ cộng thêm đi kèm:

$token = devvn_vtp_token();
$list  = devvn_vtp_request(
	'order/getPriceAll',
	array(
		'SENDER_PROVINCE'   => $kho['province_id'],
		'SENDER_DISTRICT'   => $kho['district_id'],
		'SENDER_WARD'       => $kho['ward_id'],
		'RECEIVER_PROVINCE' => $khach['province_id'],
		'RECEIVER_DISTRICT' => $khach['district_id'],
		'RECEIVER_WARD'     => $khach['ward_id'],
		'PRODUCT_TYPE'      => 'HH',
		'PRODUCT_WEIGHT'    => 1000,
		'PRODUCT_PRICE'     => 550000,
		'MONEY_COLLECTION'  => 550000,
		'TYPE'              => 1,
	),
	$token
);
foreach ( (array) $list as $row ) {
	printf( "%s - %s: %d đ (%s)\n", $row['MA_DV_CHINH'], $row['TEN_DICHVU'], $row['GIA_CUOC'], $row['THOI_GIAN'] );
}

Mỗi dòng trả về có:

  • MA_DV_CHINH là mã dịch vụ chính, dùng cho ORDER_SERVICE khi tính cước và tạo đơn.
  • GIA_CUOC là tổng cước đã gồm VAT, chưa gồm phụ phí, còn THOI_GIAN là thời gian cam kết của gói.
  • EXCHANGE_WEIGHT là khối lượng quy đổi từ kích thước (gram).
  • EXTRA_SERVICE là danh sách dịch vụ cộng thêm hợp lệ, trong đó SERVICE_CODE dùng cho ORDER_SERVICE_ADD.

Mấy trường dễ nhầm: PRODUCT_WEIGHT tính bằng gram; TYPE là loại bảng giá (1 trong nước, 0 quốc tế); MONEY_COLLECTION là tiền hàng cần thu hộ, không gồm tiền cước.

Bước 4: tính cước một dịch vụ (getPrice)

Khi đã biết gói khách chọn, gọi order/getPrice để lấy đúng cước của gói đó, kèm dịch vụ cộng thêm nếu có. Tham số gần giống bước 3, thêm ORDER_SERVICE, ORDER_SERVICE_ADD và NATIONAL_TYPE (1 là bảng giá trong nước):

$price = devvn_vtp_request(
	'order/getPrice',
	array(
		'SENDER_PROVINCE'   => $kho['province_id'],
		'SENDER_DISTRICT'   => $kho['district_id'],
		'SENDER_WARD'       => $kho['ward_id'],
		'RECEIVER_PROVINCE' => $khach['province_id'],
		'RECEIVER_DISTRICT' => $khach['district_id'],
		'RECEIVER_WARD'     => $khach['ward_id'],
		'PRODUCT_TYPE'      => 'HH',
		'PRODUCT_WEIGHT'    => 1000,
		'PRODUCT_PRICE'     => 550000,
		'MONEY_COLLECTION'  => 550000,
		'PRODUCT_LENGTH'    => 30,
		'PRODUCT_WIDTH'     => 20,
		'PRODUCT_HEIGHT'    => 10,
		'ORDER_SERVICE'     => 'VCN',
		'ORDER_SERVICE_ADD' => '',
		'NATIONAL_TYPE'     => 1,
	),
	devvn_vtp_token()
);
if ( is_wp_error( $price ) ) {
	error_log( 'VTP getPrice: ' . $price->get_error_message() );
}

Mấy kinh nghiệm khi đưa phần tính cước lên trang thanh toán:

  • Trang thanh toán gọi tính phí mỗi lần khách đổi địa chỉ, nên cache kết quả theo đúng bộ tham số gửi đi (ví dụ md5 của payload, giữ vài giờ tới vài ngày).
  • Khách trả trước (chuyển khoản, ví điện tử) thì gửi MONEY_COLLECTION = 0, và cước cùng các gói khả dụng lúc này có thể khác so với khi thu hộ, tuỳ bảng giá tài khoản.
  • Khi API lỗi thì hiện “phí báo sau” để khách vẫn đặt được hàng, đừng để trang thanh toán chết theo.

Bước 5: tạo đơn Viettel Post qua API (createOrder)

API order/createOrder tạo vận đơn thật (trên Production). Trường bắt buộc gồm thông tin người gửi, người nhận (tên, điện thoại, địa chỉ, ID địa danh), ORDER_PAYMENT và ORDER_SERVICE. ORDER_PAYMENT là loại vận đơn:

  • Loại 1 không thu hộ.
  • Loại 2 thu hộ cả tiền hàng và tiền cước.
  • Loại 3 thu hộ tiền hàng, không thu hộ tiền cước.
  • Loại 4 thu hộ tiền cước, không thu hộ tiền hàng.
$order = wc_get_order( $order_id );
$res   = devvn_vtp_request(
	'order/createOrder',
	array(
		'ORDER_NUMBER'      => (string) $order->get_order_number(),
		'CHECK_UNIQUE'      => true,
		'GROUPADDRESS_ID'   => $kho['groupaddress_id'],
		'DELIVERY_DATE'     => wp_date( 'd/m/Y H:i:s', time() + HOUR_IN_SECONDS ),
		'SENDER_FULLNAME'   => $kho['name'],
		'SENDER_PHONE'      => $kho['phone'],
		'SENDER_ADDRESS'    => $kho['address'],
		'SENDER_PROVINCE'   => $kho['province_id'],
		'SENDER_DISTRICT'   => null,
		'SENDER_WARD'       => $kho['ward_id'],
		'RECEIVER_FULLNAME' => $order->get_formatted_billing_full_name(),
		'RECEIVER_PHONE'    => $order->get_billing_phone(),
		'RECEIVER_ADDRESS'  => $order->get_billing_address_1(),
		'RECEIVER_PROVINCE' => $khach['province_id'],
		'RECEIVER_DISTRICT' => null,
		'RECEIVER_WARD'     => $khach['ward_id'],
		'PRODUCT_NAME'      => 'Đơn hàng #' . $order->get_order_number(),
		'PRODUCT_TYPE'      => 'HH',
		'PRODUCT_QUANTITY'  => $order->get_item_count(),
		'PRODUCT_PRICE'     => (int) $order->get_subtotal(),
		'PRODUCT_WEIGHT'    => 1000,
		'ORDER_PAYMENT'     => 'cod' === $order->get_payment_method() ? 3 : 1,
		'MONEY_COLLECTION'  => 'cod' === $order->get_payment_method() ? (int) $order->get_total() : 0,
		'ORDER_SERVICE'     => 'VCN',
		'ORDER_SERVICE_ADD' => '',
		'ORDER_NOTE'        => 'Cho xem hàng',
	),
	devvn_vtp_token()
);
if ( ! is_wp_error( $res ) ) {
	$order->update_meta_data( '_vtp_ma_van_don', $res['ORDER_NUMBER'] );
	$order->save();
}

Đoạn trên dùng bộ địa danh mới (2 cấp) nên gửi *_DISTRICT là null. Mấy điểm trong tài liệu tạo đơn cần để ý:

  • ORDER_NUMBER trong request là mã đơn của bạn. ORDER_NUMBER trong response là mã vận đơn Viettel Post sinh ra, lưu lại để tra cứu và huỷ.
  • CHECK_UNIQUE: true bật kiểm tra trùng mã đơn: mã đã tồn tại thì Viettel Post trả lỗi, tránh tạo hai vận đơn cho một đơn khi request bị gửi lại.
  • Ở loại vận đơn 3, tiền cước không thu hộ. Nếu shop muốn bưu tá thu luôn phí ship khách đã trả trên web thì gộp phí ship vào MONEY_COLLECTION như đoạn code trên (lấy tổng đơn).
  • PRODUCT_WEIGHT không truyền thì mặc định 50 g. Dịch vụ cộng thêm “Thu tiền xem hàng” (XMG) đi kèm EXTRA_MONEY, tối đa 2 lần tổng cước. Dịch vụ “Phát tận tay xác minh” (PTTX) cần thêm VERIFICATION_CODE (năm sinh hoặc 6 số cuối CCCD người nhận).
  • Trường chữ tối đa 150 byte. Tiếng Việt có dấu tốn nhiều byte hơn số ký tự, nên địa chỉ dài cần cắt bớt.

Bước 6: huỷ đơn và cập nhật trạng thái (UpdateOrder)

API “Cập nhật trạng thái vận đơn” là order/UpdateOrder, gửi TYPE, ORDER_NUMBER (mã vận đơn) và NOTE (lý do, tối đa 150 ký tự). Giá trị TYPE theo tài liệu:

  • Gửi 1 để duyệt đơn hàng.
  • Gửi 2 để duyệt hoàn hoặc 3 để phát tiếp, hai loại này dùng khi đơn ở trạng thái 505 (Viettel Post báo chuyển hoàn).
  • Gửi 4 để huỷ đơn, chỉ áp dụng với đơn chưa nhận thành công (trạng thái nhỏ hơn 200).
  • Gửi 5 để xoá đơn đã huỷ (trạng thái 107).
$cancel = devvn_vtp_request(
	'order/UpdateOrder',
	array(
		'TYPE'         => 4,
		'ORDER_NUMBER' => $order->get_meta( '_vtp_ma_van_don' ),
		'NOTE'         => 'Khách huỷ đơn',
	),
	devvn_vtp_token()
);

Ngoài ra tài liệu còn có API cập nhật thông tin đơn hàng và API lấy link in vận đơn, anh em cần thì xem trong mục “Tài liệu API” trên trang Partner.

Bước 7: nhận trạng thái đơn qua webhook

Muốn đơn trên website tự đổi trạng thái theo Viettel Post thì đừng gọi API tra cứu liên tục, hãy nhận webhook. Shop tự khai webhook trên trang Partner: Bảng điều khiển → Thông tin tài khoản → Cấu hình webhook, nhập Webhook Endpoints (URL trên website của bạn) và Secret parameter, rồi bấm Kiểm tra kết nối.

Trang Cấu hình webhook trên Partner Viettel Post với ô Webhook Endpoints và Secret parameter
Cấu hình webhook trên trang Partner Viettel Post

Ở phía website: kiểm tra secret trong dữ liệu gửi tới, tìm đơn theo mã vận đơn, đổi trạng thái rồi trả lời thật nhanh. Việc nặng (gửi email, đồng bộ kho…) thì đẩy sang chạy nền, đừng bắt Viettel Post chờ.

Lỗi hay gặp khi tích hợp API Viettel Post

Trích từ bảng lỗi trong tài liệu “Môi trường và tham số chung”, kèm nguyên nhân hay gặp:

  • Lỗi “Header Token is required” hoặc “Token invalid or expired” là do thiếu header Token hoặc token đã hết hạn, xoá cache token rồi lấy lại là được. Dùng token Development gọi vào Production (hay ngược lại) cũng ra lỗi này.
  • Lỗi “Invalid owner account or password!” nghĩa là sai tài khoản hoặc mật khẩu ở bước lấy token.
  • Lỗi “Price does not apply to this itinerary!” xuất hiện khi gói dịch vụ không dùng được cho tài khoản hoặc tuyến này, hoặc dịch vụ cộng thêm không hợp với gói chính, nên gọi lại getPriceAll để lấy gói hợp lệ.
  • Lỗi “Invalid [RECEIVER_WARD] / … with address old or new” nghĩa là ID phường/xã không thuộc tỉnh đã chọn, hoặc đang trộn địa danh cũ với mới.
  • Lỗi “Incorrect data: ORDER_SERVICE / ORDER_PAYMENT / DELIVERY_DATE” là do sai mã gói, sai loại vận đơn, hoặc ngày giao sai định dạng hay nằm trong quá khứ.
  • Lỗi “Invalid [MONEY_COLLECTION]” báo tiền thu hộ không hợp lệ (âm, sai kiểu dữ liệu…).
  • Lỗi “Blocking create order, please check the configuration” nghĩa là tài khoản, tuyến hoặc dịch vụ chưa được cấu hình đủ để tạo đơn, trường hợp này phải liên hệ Viettel Post.

Tự code hay dùng plugin Viettel Post có sẵn?

Gọi được API mới là phần đầu của việc tích hợp. Phần tốn thời gian là mọi thứ xung quanh: dữ liệu tỉnh/phường khớp ID Viettel Post (và đổi theo mỗi lần địa giới thay đổi), giao diện trang thanh toán, nhiều kho, chọn gói theo hình thức thanh toán, hộp thoại tạo đơn cho nhân viên, webhook và bảng ánh xạ trạng thái, email báo mã vận đơn cho khách…

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

Nếu web của bạn là WooCommerce, có thể xem plugin kết nối Viettel Post với WooCommerce mình làm: đăng nhập bằng tài khoản Viettel Post, tự tính phí ở trang thanh toán, tạo vận đơn ngay trên đơn hàng WooCommerce, webhook có bảng ánh xạ trạng thái. Plugin cũng có nhiều hook cho lập trình viên để tuỳ biến mà không sửa lõi.

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

API Viettel Post có môi trường test không?

Có. Môi trường Development ở partnerdev.viettelpost.vn, tài khoản tạo riêng ở trang Partner (thẻ Development). Đơn tạo trên Production là đơn thật, nên thử trên Development trước.

Token Viettel Post hết hạn khi nào?

Token lấy từ user/Login là token ngắn hạn. Đổi sang token dài hạn bằng user/ownerconnect, thời hạn 1 năm theo tài liệu. Nên tự lấy lại trước khi hết hạn.

Sau sáp nhập tỉnh thành, gửi địa chỉ thế nào?

Dùng danh mục địa danh mới (V3): Tỉnh/TP và Phường/Xã, gửi *_DISTRICT là null. Người gửi và người nhận dùng cùng một bộ, đừng trộn với danh mục cũ.

Mã vận đơn nằm ở đâu trong phản hồi tạo đơn?

Ở trường ORDER_NUMBER của response. Phản hồi còn có tiền thu hộ, tổng cước và các khoản phí (phí thu hộ, VAT…) để bạn lưu lại đối chiếu.

Anh em tích hợp mà vướng chỗ nào cứ để lại bình luận, nhớ che token và số điện thoại trước khi dán log nhé. Chúc anh em tạo trót lọt vận đơn thử trên Development rồi mới chuyển sang Production! 🚀

Bài viết trong series

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

0/2đã đọ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
Sản phẩm nổi bật của Toản
x