VKL Shield Tài liệu API

#Hướng dẫn chuyển đổi Legacy → v1

#Tổng quan

API v1 thay thế các route legacy /create_order, /capture_order, v.v. bằng chữ ký Ed25519, quản lý key theo từng đối tác, và bảo mật được cải thiện. Hướng dẫn này giúp bạn chuyển đổi từ SDK legacy sang v1.

#Danh sách host

Mục đích Host
Shield API live mặc định (production) live.vkl-shield.com
Shield API sandbox mặc định (testing) sandbox.vkl-shield.com
Shield host riêng (nếu được cấp cho tài khoản đối tác của bạn) do đội vận hành shield cung cấp — dùng host này ở mọi nơi hướng dẫn này ghi host mặc định (script src, selectBase, các lời gọi API)
JWKS (luôn dùng — bất kể request được phục vụ bởi shield host nào) keys.vkl-shield.com/v1/keys (live) / keys.vkl-shield.com/sandbox/v1/keys (sandbox)

#1. Cập nhật URL script

Thay SDK legacy bằng v1:

#PayPal

<!-- BEFORE (legacy) -->
<script src="https://live.vkl-shield.com/assets/paypal-shield-sdk.js"></script>

<!-- AFTER (v1) -->
<script src="https://live.vkl-shield.com/assets/v1/paypal-shield-sdk.js"></script>

#Stripe

<!-- BEFORE (legacy) -->
<script src="https://live.vkl-shield.com/assets/stripe-shield-sdk.js"></script>

<!-- AFTER (v1) -->
<script src="https://live.vkl-shield.com/assets/v1/stripe-shield-sdk.js"></script>

Các ví dụ trên dùng host live mặc định. Dùng sandbox.vkl-shield.com khi test, hoặc shield host riêng nếu tài khoản đối tác của bạn được cấp một host như vậy — xem Danh sách host ở trên.

#2. Cập nhật định dạng shield key

Key legacy: chuỗi đã mã hóa (không có tiền tố) Key v1: shpk_test_* (sandbox) / shpk_live_* (live)

Yêu cầu key v1 mới từ đội vận hành PayPal Shields. Partner ID phải khớp [a-z0-9]{2,32} (ví dụ: acme, mystore, không phải test_mystore).

#3. Cập nhật hình dạng payload callback

#PayPal: tham số onCompleted

Legacy:

onCompleted: function (capture, orderId) {
  // capture = {id, purchase_units: [...]}
  // Chữ ký legacy: không có bằng chứng; store tự tin tưởng vào nội dung body
}

v1:

onCompleted: function (envelope, orderId) {
  // envelope = {
  //   error: false,
  //   data: {id, purchase_units: [...]},
  //   signed_payload: "{...}",
  //   sig: "base64url_sig",
  //   kid: "shsig_xxxxxxxxxxxxxxxxxxxxxxxx",
  //   alg: "Ed25519",
  //   v: 1
  // }

  // PHẢI xác minh sig trước khi dùng data
}

#Stripe: tham số onCompleted

Legacy:

onCompleted: function (result, orderId) {
  // result = {
  //   signed_fields: {...},
  //   signature: "hex_signature",
  //   contact: {...}
  // }
  // Legacy: xác minh bằng HMAC
}

v1:

onCompleted: function (envelope, orderId) {
  // envelope = {
  //   error: false,
  //   data: {...signed_fields},
  //   signed_payload: "{...}",
  //   sig: "base64url_sig",
  //   kid: "shsig_xxxxxxxxxxxxxxxxxxxxxxxx",
  //   alg: "Ed25519",
  //   v: 1,
  //   contact: {...}
  // }

  // PHẢI xác minh sig trước khi dùng data
}

#4. Thay thế xác minh bằng chứng (proof)

#Xác minh legacy (HMAC)

// Node.js legacy (nếu vẫn chạy endpoint legacy)
const crypto = require('crypto');
const canonicalJson = require('canonical-json');
const signature = crypto.createHmac('sha256', SIGNATURE_PRIVATE_KEY)
  .update(canonicalJson(result.signed_fields))
  .digest('base64');
const valid = signature === result.signature;

(SIGNATURE_PRIVATE_KEY là secret dùng chung ở phía server do đội vận hành shield cấp. Xem test vector HMAC legacy để xác minh byte-chính-xác (byte-for-byte).)

#Xác minh v1 (Ed25519/JWKS)

Xem docs/api/examples/verify-signed-payload.mjs để có tham chiếu Node.js đầy đủ (cũng có ví dụ PHP, Python).

Endpoint JWKS chuẩn:

Thứ tự xác minh (quan trọng — xác minh chữ ký TRƯỚC khi parse):

  1. Xác minh envelope.sig dựa trên các byte UTF-8 chính xác của envelope.signed_payload bằng JWKS URL chuẩn (không bao giờ lấy từ envelope)
  2. Chỉ sau khi chữ ký hợp lệ, mới parse: payload = JSON.parse(envelope.signed_payload)
  3. Khẳng định (assert) mọi field từ payload đã parse (cả PayPal & Stripe):
    • typ = 'paypal_capture' (PayPal) hoặc 'stripe_complete' (Stripe), v === 1
    • status === 'COMPLETED' (PayPal) hoặc 'succeeded' (Stripe)
    • env === 'live' (trên live) hoặc 'sandbox' (trên sandbox)
    • partner_id === your_partner_id
    • key_id ∈ your_whitelisted_keys
    • invoice_id === your_order.invoice_id
    • PayPal: amount === order_total (chuỗi decimal), currency === order_currency; capture_id chưa từng gặp trước đó
    • Stripe: amount_received === order_total_cents (số nguyên), chỉ USD; id (PI id) chưa từng gặp trước đó
    • iat nằm trong khoảng [now - 600s, now + 300s]
    • Tùy chọn: reference_id khớp với reference đơn hàng của bạn nếu bạn đã cung cấp

#5. Lịch trình xoay (rotation) key

Hạn dùng key đối tác: 180 ngày sau khi cấp

Quy tắc xoay key: Trước khi hết hạn, thêm key ID mới vào allowlist trong config shield (không cần deploy), chuyển SDK sang key mới, chờ hết TTL cache trang + 10 phút, rồi xóa key ID cũ

Ví dụ mốc thời gian:

Nếu bạn bỏ lỡ việc xoay key: Sau khi hết hạn, mọi lời gọi select trả về 401 SHIELD_KEY_EXPIRED (KHÔNG bị charge)

#6. Không bao giờ nạp cả hai phiên bản trên cùng một trang

<!-- SAI: cả legacy và v1 -->
<script src="https://live.vkl-shield.com/assets/paypal-shield-sdk.js"></script>
<script src="https://live.vkl-shield.com/assets/v1/paypal-shield-sdk.js"></script>
<!-- Cả hai đều dùng window.PaypalShield; hành vi không xác định -->

Chỉ chọn một phiên bản cho mỗi trang. Để chuyển đổi:

  1. Trên staging: đổi URL + test xác minh dựa trên JWKS sandbox
  2. Chờ 7 ngày để TTL cache trang hết hiệu lực
  3. Trên live: cập nhật URL script SDK trong HTML
  4. Theo dõi tỉ lệ lỗi trong 1 tuần
  5. Yêu cầu vô hiệu hóa key legacy từ đội vận hành sau khi đã tự tin

#7. Thay đổi trong xử lý lỗi

#CAPTURED_PROOF_UNAVAILABLE (502)

Quan trọng: Lỗi này có nghĩa là người mua ĐÃ bị trừ tiền trên PayPal.

onFailed: function (error) {
  if (error.issue === 'CAPTURED_PROOF_UNAVAILABLE' || error.code === 'CAPTURED_PROOF_UNAVAILABLE') {
    // KHÔNG hiển thị: "Payment failed, please retry"
    // KHÔNG retry capture — người mua đã bị trừ tiền

    // ĐÚNG: hiển thị "Đã nhận thanh toán, đang xác nhận..."
    showModal('Đã nhận thanh toán! Chúng tôi đang xác nhận đơn hàng của bạn.');

    // Người mua sẽ nhận email xác nhận từ PayPal
    // Chờ webhook AIO transaction.completed (khớp theo settle_ref)
    // Hỏi đội vận hành shield về notification feed của integration của bạn
  }
}

#Chính sách retry — PayPal SDK vs. Stripe SDK

PayPal SDK (v1): Không tự động retry. Lỗi trước-capture của shield nghĩa là KHÔNG bị charge; lỗi capture không có field issue = trạng thái charge KHÔNG XÁC ĐỊNH.

Tình huống lỗi Ý nghĩa Hành động của bạn
Lỗi trước-capture (SHIELD_KEY_INVALID, POOL_MISMATCH, SELECT_TIMEOUT, v.v.) Shield không truy cập được hoặc lỗi key; người mua KHÔNG bị charge Có thể hiển thị "Thử lại" sau khi sửa
Lỗi capture có field issue (SIGNING_UNAVAILABLE, SHIELD_REGISTRY_UNAVAILABLE) Lỗi tạm thời của shield; người mua KHÔNG bị charge Hiển thị "Thử lại"; PayPal SDK không tự retry
Capture trả về 502 CAPTURED_PROOF_UNAVAILABLE Người mua BỊ CHARGE nhưng tạo bằng chứng thất bại Hiển thị "Payment received"; không bao giờ retry; đối soát qua feed
Capture trả về 500 với object lỗi PayPal (ví dụ: INSTRUMENT_DECLINED) Người mua bị từ chối; KHÔNG bị charge Hiển thị lý do từ chối cho người mua (details[0].issue)
Endpoint capture không truy cập được (network/proxy/timeout/non-JSON) Trạng thái charge KHÔNG XÁC ĐỊNH; không có field issue nào được gửi Đối soát qua feed đã cấu hình; không bao giờ tự retry

Stripe SDK (v1): Chỉ route /complete có tự động retry (tiền đã bị trừ rồi); create/select không được retry.

#Đối soát & khôi phục CAPTURED_PROOF_UNAVAILABLE

Mô hình xác nhận — bằng chứng (proof) là chính, AIO là fallback dự phòng.

  1. Chính (đồng bộ): xác minh bằng chứng Ed25519 ở phía server ngay sau callback (trang này) → hoàn tất đơn hàng ngay và hiển thị trang cảm ơn. Không chờ bất kỳ webhook nào.
  2. Fallback / đối soát: webhook AIO transaction.completed (ký bằng HMAC X-Signature: t=…,v1=…, khớp theo settle_ref = custom_id của bạn) hoàn tất các đơn hàng mà bằng chứng chưa từng đến — PayPal CAPTURED_PROOF_UNAVAILABLE, Stripe charged_but_unconfirmed, hoặc người mua đã đóng tab — và có thể dùng để đối soát sau. Nó phải idempotent (lặp lại an toàn) với bước 1 (một đơn hàng đã hoàn tất từ bằng chứng thì webhook này là no-op). Không bao giờ charge người mua lần nữa khi đơn hàng đang chờ fallback.

Khi không thể tạo bằng chứng capture (PayPal CAPTURED_PROOF_UNAVAILABLE, Stripe charged_but_unconfirmed) thì người mua đã bị charge.

Đối soát qua webhook AIO transaction.completed. AIO nhận các webhook từ nhà cung cấp PayPal/Stripe cho các tài khoản merchant của shield và thông báo cho platform của bạn (ký HMAC, header X-Signature: t=…,v1=…; endpoint, secret và hợp đồng payload do đội vận hành shield/AIO cấp). Khớp với đơn hàng của bạn bằng settle_ref bạn đã gửi (PayPal purchase_units[0].custom_id; Stripe: cùng custom_id trong getOrderData, lưu dưới dạng metadata PaymentIntent settle_ref). Chỉ đơn hàng KHÔNG có bằng chứng đã xác minh mới chờ webhook này (hiển thị đã nhận thanh toán, đang xác nhận); không bao giờ yêu cầu người mua trả tiền lại.

#Rollback

Nếu bạn cần quay lại legacy:

  1. Giữ cả hai SDK được phục vụ (legacy vẫn hoạt động)
  2. Đổi URL script về lại legacy
  3. Chờ 7 ngày để TTL cache trang hết hiệu lực
  4. Theo dõi lỗi
  5. Yêu cầu vô hiệu hóa key v1 từ đội vận hành sau khi đã tự tin

#Test trên sandbox

Test chuyển đổi v1 trên sandbox trước:

  1. Yêu cầu key sandbox từ đội vận hành (định dạng: shpk_test_*)
  2. Cập nhật HTML với URL script v1 + key sandbox
  3. Triển khai xác minh dùng endpoint JWKS sandbox: https://keys.vkl-shield.com/sandbox/v1/keys
  4. Chạy test end-to-end với key shpk_test_* (luồng đầy đủ create → capture → verify)
  5. Test các tình huống lỗi: key hết hạn, partner ID sai, payload bị giả mạo
  6. Khi đã tự tin trên sandbox, yêu cầu key live và test trên staging production

#Hỗ trợ

Nếu có câu hỏi hoặc vấn đề: