Appearance
API hóa đơn điện tử
API dành cho server của khách hàng tạo nháp, gửi nháp tới nhà cung cấp, phát hành và tra cứu hóa đơn điện tử qua kết nối đã cấu hình trên Pay2S. API này không tạo link thanh toán, không cộng tiền và không tự xác minh đơn hàng bên ngoài đã thanh toán.
Chuẩn bị
- Tài khoản chính có gói Basic trở lên và được bật tính năng HĐĐT.
- Trong Xuất hóa đơn tự động, tạo kết nối, kiểm tra đăng nhập nhà cung cấp, đồng bộ mẫu và chọn mẫu mặc định. Nếu truyền
store_id, cửa hàng phải được gán mẫu thuộc kết nối đó. - Lấy Partner Code, Access Key, Secret Key của tài khoản chính Pay2S (cùng bộ khóa dùng tích hợp thanh toán). Không dùng khóa Partner API độc lập, khóa nhân viên/cửa hàng hoặc token đăng nhập Dashboard.
- Dùng kết nối Sandbox để kiểm tra. Tài khoản demo không được gọi bộ API này. Production cần được Pay2S bật quyền phát hành thật.
Không gửi thông tin đăng nhập nhà cung cấp hóa đơn trong request. Chỉ giữ Secret Key ở backend của bạn.
Endpoint và xác thực
Tất cả thao tác dùng:
http
POST https://api.pay2s.vn/api/v1/einvoices
Content-Type: application/json
X-Partner-Code: YOUR_PARTNER_CODE
X-Access-Key: YOUR_ACCESS_KEY
X-Timestamp: UNIX_SECONDS
X-Signature: LOWERCASE_HMAC_SHA256_HEXX-Timestamp là Unix timestamp 10 chữ số, lệch giờ server tối đa 300 giây. JSON tối đa 256 KiB. Giới hạn 60 request/phút/tài khoản, tính chung các thao tác; HTTP 429 thì đợi phút kế tiếp.
Chuỗi ký gồm 4 dòng, ngăn bằng ký tự LF (\n), không thêm xuống dòng cuối:
text
{timestamp}
POST
/api/v1/einvoices
{sha256_hex_cua_nguyen_body_JSON}signature = HMAC-SHA256(secret_key, chuỗi_ký), xuất hex chữ thường. Ký đúng chuỗi JSON sẽ gửi; không serialize lại body sau khi ký. Timestamp và chữ ký nằm ở header, không nằm trong JSON. Không thêm query string vào endpoint.
Node.js
js
import { createHash, createHmac } from 'node:crypto';
async function einvoice(payload) {
const path = '/api/v1/einvoices';
const body = JSON.stringify(payload);
const timestamp = String(Math.floor(Date.now() / 1000));
const digest = createHash('sha256').update(body, 'utf8').digest('hex');
const canonical = `${timestamp}\nPOST\n${path}\n${digest}`;
const signature = createHmac('sha256', process.env.PAY2S_SECRET_KEY)
.update(canonical, 'utf8').digest('hex');
const response = await fetch(`https://api.pay2s.vn${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-Partner-Code': process.env.PAY2S_PARTNER_CODE,
'X-Access-Key': process.env.PAY2S_ACCESS_KEY,
'X-Timestamp': timestamp,
'X-Signature': signature
},
body,
signal: AbortSignal.timeout(90000)
});
return { httpStatus: response.status, result: await response.json() };
}
console.log(await einvoice({ action: 'connections' }));PHP cURL
php
function einvoice(array $payload): array {
$path = '/api/v1/einvoices';
$body = json_encode($payload, JSON_UNESCAPED_UNICODE | JSON_THROW_ON_ERROR);
$timestamp = (string)time();
$canonical = $timestamp . "\nPOST\n" . $path . "\n" . hash('sha256', $body);
$signature = hash_hmac('sha256', $canonical, getenv('PAY2S_SECRET_KEY'));
$ch = curl_init('https://api.pay2s.vn' . $path);
curl_setopt_array($ch, [
CURLOPT_POST => true,
CURLOPT_POSTFIELDS => $body,
CURLOPT_RETURNTRANSFER => true,
CURLOPT_CONNECTTIMEOUT => 10,
CURLOPT_TIMEOUT => 90,
CURLOPT_HTTPHEADER => [
'Content-Type: application/json',
'X-Partner-Code: ' . getenv('PAY2S_PARTNER_CODE'),
'X-Access-Key: ' . getenv('PAY2S_ACCESS_KEY'),
'X-Timestamp: ' . $timestamp,
'X-Signature: ' . $signature,
],
]);
$response = curl_exec($ch);
$httpStatus = curl_getinfo($ch, CURLINFO_HTTP_CODE);
$error = curl_error($ch);
curl_close($ch);
if ($response === false) throw new RuntimeException($error);
return ['httpStatus' => $httpStatus, 'result' => json_decode($response, true, 512, JSON_THROW_ON_ERROR)];
}1. Lấy danh sách kết nối
json
{"action":"connections"}Trả về kết nối đang hoạt động thuộc tài khoản đã xác thực, không trả mật khẩu/token của nhà cung cấp:
json
{"status":true,"data":{"connections":[{"id":12,"name":"Hóa đơn thử","provider":"matbao_hddt","environment":"sandbox"}]}}Các ID trong ví dụ là minh họa; dùng ID thực tế từ API. ID là số nguyên JSON ở request.
2. Tạo nháp tại Pay2S
json
{
"action": "create_draft",
"connection_id": 12,
"environment": "sandbox",
"external_ref": "ORDER_20260918_001",
"buyer_contact_name": "Bán cho người tiêu dùng",
"payment_method": "TM/CK",
"note": "Đơn hàng từ website",
"items": [
{"code":"SP01","name":"Sản phẩm mẫu","unit":"Cái","quantity":2,"unit_price":100000,"tax_rate":10}
]
}Thành công lần đầu: HTTP 201. Chưa gọi nhà cung cấp và chưa phát hành.
json
{"status":true,"message":"Đã tạo bản nháp Sandbox.","data":{"id":101,"status":"draft","external_ref":"ORDER_20260918_001"}}Lưu data.id làm invoice_id cho các bước sau.
| Trường | Quy định |
|---|---|
connection_id | Bắt buộc, ID kết nối đang hoạt động của tài khoản |
environment | Bắt buộc sandbox hoặc production, phải khớp kết nối |
external_ref | Bắt buộc, mã đơn riêng của bạn; 1–100 ký tự chữ/số/_/-, phân biệt hoa thường |
store_id | Tùy chọn, số nguyên; bỏ trống hoặc 0 để dùng mẫu mặc định của kết nối |
buyer_name | Tên đơn vị mua hàng, tối đa 400 ký tự |
buyer_tax_code | Mã số thuế, tối đa 14 ký tự |
buyer_citizen_id | CCCD, tối đa 12 ký tự |
buyer_contact_name | Họ tên người mua, tối đa 100 ký tự |
buyer_address | Địa chỉ, tối đa 400 ký tự |
buyer_phone | Tối đa 20 ký tự |
buyer_email | Email hợp lệ, tối đa 50 ký tự |
payment_method | Mặc định TM/CK, tối đa 50 ký tự |
note | Tối đa 255 ký tự |
items | Mảng 1–100 dòng |
confirm_production | Boolean true bắt buộc khi tạo nháp Production; vẫn chưa phát hành |
Phải có buyer_name hoặc buyer_contact_name. Thông tin người mua dùng cùng quy tắc Dashboard: doanh nghiệp cần đủ tên đơn vị, MST, địa chỉ; cá nhân cần họ tên và địa chỉ. Thiếu bộ thông tin định danh đầy đủ thì tên hiển thị chuyển thành Bán cho người tiêu dùng, bỏ thông tin định danh rời rạc. Đọc và kiểm tra dữ liệu nháp bằng detail trước khi phát hành.
| Trường dòng hàng | Quy định |
|---|---|
name | Tên hàng bắt buộc; tối đa 500 ký tự |
code | Mã hàng tùy chọn, tối đa 50 ký tự |
unit | Đơn vị tính tùy chọn, tối đa 50 ký tự |
quantity | Số JSON > 0, làm tròn 4 chữ số thập phân |
unit_price | Số JSON ≥ 0, làm tròn 2 chữ số thập phân |
tax_rate | Bắt buộc, giá trị hệ thống nhận: -2, -1, 0, 3.5, 5, 8, 10; phải phù hợp loại mẫu và nhà cung cấp |
direct_vat_rate | Tùy chọn: 1, 2, 3, 5; dùng khi giảm thuế cho mẫu hóa đơn bán hàng trực tiếp |
vat_reduction_eligible | Boolean, mặc định false; hệ thống kiểm tra điều kiện/mẫu khi áp dụng |
-1 là không chịu thuế, -2 là không kê khai tính nộp thuế. Các mã này là quy ước dữ liệu API; chọn đúng theo nghiệp vụ và mẫu đang dùng. Mỗi số và giá trị mỗi dòng tối đa 10^12. Tổng tiền do backend tính, không nhận tổng do client tự khai. Không truyền owner_user_id, user_id, source_order_id, template_id, provider, credentials hoặc idempotency_key.
Chống trùng và timeout
external_refduy nhất theo tài khoản chính, xuyên các kết nối/môi trường. Nên đặt tiền tố khác nhau cho đơn Sandbox và Production.- Cùng mã và dữ liệu: trả lại cùng ID, HTTP 200,
duplicate: true. Không tạo thêm hóa đơn. - Cùng mã nhưng thay đổi dữ liệu/kết nối: HTTP 409. Không tự đổi mã đơn để vượt lỗi này.
- Đã xóa nháp trên Dashboard thì mã cũ không được tạo lại. Dùng mã mới chỉ khi bạn thực sự muốn tạo hóa đơn thay thế.
- Nếu timeout khi
create_draft, ký lại request với timestamp mới nhưng giữ nguyên mã và dữ liệu. Các giá trị số nên giữ nguyên cách biểu diễn giữa các lần gọi (ví dụ2khác2.0trong dấu vân tay dữ liệu). - API có khóa xử lý đồng thời; HTTP 409 “đang được xử lý” thì đợi vài giây rồi gọi lại.
3. Gửi nháp sang nhà cung cấp
json
{"action":"submit_draft","connection_id":12,"environment":"sandbox","invoice_id":101}Dùng logic tạo nháp đã có trên Dashboard. Bản nháp đã có mã nhà cung cấp sẽ không được tạo lại. Đây chưa phải thao tác ký/phát hành. Kết quả và khả năng tạo nháp phụ thuộc nhà cung cấp.
4. Phát hành
Sau khi gửi nháp thành công và đã kiểm tra thông tin:
json
{"action":"publish","connection_id":12,"environment":"sandbox","invoice_id":101}Với Production, cả bước tạo/gửi nháp/phát hành đều cần "environment":"production" và "confirm_production":true, đúng kết nối Production và quyền phát hành của tài khoản. API không tự chuyển môi trường và không tự bật quyền.
publish thực hiện ký/cấp số hóa đơn thật ở Production. Không dùng để điều chỉnh hoặc thay thế hóa đơn đã phát hành. Gọi lại hóa đơn đã issued trả trạng thái đã phát hành, không phát hành lần nữa.
5. Tra cứu và đồng bộ trạng thái
Đọc dữ liệu đã lưu trên Pay2S, không gọi nhà cung cấp:
json
{"action":"detail","connection_id":12,"environment":"sandbox","invoice_id":101}Kết quả nằm trong data.invoice: ID, trạng thái, môi trường, mẫu/ký hiệu, số hóa đơn (invoice_number), người mua, items, subtotal, tax_total, grand_total, thời gian và provider_pdf_url/provider_xml_url khi nhà cung cấp có trả link. Có has_provider_reference để kiểm tra đã có mã nhà cung cấp chưa.
Yêu cầu đồng bộ từ nhà cung cấp:
json
{"action":"sync","connection_id":12,"environment":"sandbox","invoice_id":101}Sau sync, gọi detail để lấy bản cập nhật. Một số nhà cung cấp cần đủ mã tham chiếu trước khi đồng bộ được.
| Trạng thái | Cách xử lý |
|---|---|
draft | Nháp; xem has_provider_reference để biết đã gửi nhà cung cấp chưa |
processing | Đang xử lý, đợi và tra cứu |
issued | Đã phát hành |
failed | Xem lỗi và xử lý trên Dashboard; không tự tạo đơn mới để thử lại |
unknown | Kết quả chưa rõ, đồng bộ/đối soát trước khi thử lại |
Nếu timeout ở bước gửi nháp/phát hành, tra cứu trước. Không tạo hóa đơn mới chỉ vì client không nhận được phản hồi. API chặn gửi/phát hành lại khi processing hoặc unknown. Phiên bản này dùng polling, chưa có webhook riêng cho trạng thái HĐĐT.
6. Lấy PDF/XML
json
{"action":"files","connection_id":12,"environment":"sandbox","invoice_id":101,"file_type":"all"}file_type: all (mặc định), pdf, xml. Kết quả JSON có data.pdf_base64, data.xml_base64; giải mã Base64 thành tệp. Trường có thể rỗng nếu nhà cung cấp không hỗ trợ. Một số nhà cung cấp chỉ cho tải sau khi phát hành. API không luôn trả một URL tải công khai.
Mã HTTP
| Mã | Ý nghĩa |
|---|---|
| 200 / 201 | Thành công / tạo nháp mới |
| 400 / 413 / 415 | JSON sai / quá lớn / sai Content-Type |
| 401 | Sai khóa, chữ ký, timestamp hoặc tài khoản bị khóa |
| 403 | Sai loại tài khoản, không đủ gói/quyền hoặc demo |
| 404 | Không có hóa đơn/kết nối thuộc tài khoản này |
| 405 | Không phải POST |
| 409 | Mã đơn trùng dữ liệu khác, đang xử lý hoặc cần đối soát |
| 422 | Dữ liệu không hợp lệ, chưa cấu hình mẫu hoặc nhà cung cấp từ chối |
| 429 | Quá 60 request/phút |
| 500 / 502 / 503 | Lỗi hệ thống/nhà cung cấp hoặc chưa sẵn sàng; giữ mã đơn, kiểm tra trạng thái trước khi thử lại |
Luôn kiểm tra cả HTTP và status trong JSON. Khi có error_reference, gửi mã này cho hỗ trợ; không gửi Secret Key.
