Skip to content

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ị

  1. Tài khoản chính có gói Basic trở lên và được bật tính năng HĐĐT.
  2. 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 đó.
  3. 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.
  4. 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_HEX

X-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ườngQuy định
connection_idBắt buộc, ID kết nối đang hoạt động của tài khoản
environmentBắt buộc sandbox hoặc production, phải khớp kết nối
external_refBắ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_idTù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_nameTên đơn vị mua hàng, tối đa 400 ký tự
buyer_tax_codeMã số thuế, tối đa 14 ký tự
buyer_citizen_idCCCD, tối đa 12 ký tự
buyer_contact_nameHọ tên người mua, tối đa 100 ký tự
buyer_addressĐịa chỉ, tối đa 400 ký tự
buyer_phoneTối đa 20 ký tự
buyer_emailEmail hợp lệ, tối đa 50 ký tự
payment_methodMặc định TM/CK, tối đa 50 ký tự
noteTối đa 255 ký tự
itemsMảng 1–100 dòng
confirm_productionBoolean 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àngQuy định
nameTên hàng bắt buộc; tối đa 500 ký tự
codeMã 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ự
quantitySố JSON > 0, làm tròn 4 chữ số thập phân
unit_priceSố JSON ≥ 0, làm tròn 2 chữ số thập phân
tax_rateBắ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_rateTù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_eligibleBoolean, 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_ref duy 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ụ 2 khác 2.0 trong 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""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áiCách xử lý
draftNhá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
failedXem lỗi và xử lý trên Dashboard; không tự tạo đơn mới để thử lại
unknownKế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

Ý nghĩa
200 / 201Thành công / tạo nháp mới
400 / 413 / 415JSON sai / quá lớn / sai Content-Type
401Sai khóa, chữ ký, timestamp hoặc tài khoản bị khóa
403Sai loại tài khoản, không đủ gói/quyền hoặc demo
404Không có hóa đơn/kết nối thuộc tài khoản này
405Không phải POST
409Mã đơn trùng dữ liệu khác, đang xử lý hoặc cần đối soát
422Dữ liệu không hợp lệ, chưa cấu hình mẫu hoặc nhà cung cấp từ chối
429Quá 60 request/phút
500 / 502 / 503Lỗ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.