Chuyển tới nội dung chính

Sản xuất & phát hành thẻ vật lý — phía issuer

Phía khách hàng vs phía issuer

Trang này là phía issuer / nhà cung cấp (xperise) — những gì diễn ra bên trong xperise (và các đối tác sản xuất) sau khi có nhu cầu thẻ vật lý. Nó được tách khỏi các luồng hướng khách hàng để hai góc nhìn không bị lẫn:

Phía khách hàng (corporate admin)Phía issuer (xperise) — phần này
Corporate Portal — đặt, gán, kiểm soát thẻSinh PAN, xuất file perso, quản lý khoá
Issue Card flow — form phát hành & workflow order physical cardsNhận UID chip từ bureau, kiểm kê tồn kho
Card Management specAllocate cho corporate, personalize chip, đối soát

xperise chạy chương trình thẻ closed-loop, nên xperise chính là issuer — sở hữu dải số thẻ, khoá mật mã và toàn bộ vòng đời thẻ. Service thực thi là card-core; phần in và personalize plastic do perso bureau (nhà sản xuất thẻ) làm.

Đây là luồng đang chạy trong code, không phải mô hình tổng quát

Trang này mô tả đúng các endpoint, trạng thái và ràng buộc mà card-core đang triển khai (module physical-cards + chip). Một số bước của mô hình issuer kinh điển (QA, SHIPPED, DELIVERED, PIN mailer, EMV/BIN của Visa–Mastercard) chưa/không tồn tại trong hệ thống — xem Những gì card-core chưa mô hình hoá.

Bốn máy trạng thái cần phân biệt​

Luồng thẻ vật lý không phải một chuỗi trạng thái duy nhất, mà là bốn máy trạng thái độc lập:

Đối tượngBảngCác trạng thái
Print order (lệnh in một lô)card_print_ordersordered → exported → (canceled)
Stock (từng miếng plastic)physical_card_stockstatus: ordered → available → allocated → activated, hoặc spoiled / retired
perso_state (trục riêng): null → PENDING → PERSONALIZED / LOCKED
Thẻ (bản ghi thẻ thật)cardsinactive → active → (blocked / expired)
Credential chipcard_chipsPENDING → PERSONALIZED / LOCKED → (REVOKED)

Điểm mấu chốt: ordered không phải available. Một hàng ordered là "đã có số thẻ nhưng plastic chưa tồn tại" — chưa ai biết UID chip của nó, nên UID buộc phải do bureau trả về sau. Chỉ khi có UID, hàng đó mới thành available và mới được allocate.

Luồng thực tế​

Perso trước hay sau activate — chọn một

Ghi chip trước khi corporate activate (bước 4b) là đường nên dùng khi muốn bureau/plant hoàn thiện thẻ rồi mới giao cho corporate tự activate và gán cost center. Nếu bỏ qua bước 4b, credential sinh ra ở activate vẫn là PENDING và phải qua trạm perso ở bước 6 như trước.

Chi tiết từng bước​

1. Tạo print order — stock vào ordered​

System admin (Store admin) hoặc platform key
POST /admin/physical-cards/print-orders
Idempotency-Key: <bắt buộc>
FieldKiểuBắt buộcMô tả
countnumber (1–10000)✅Số thẻ cần in
expires_atdate-time✅Hạn in trên plastic; được mang sang thẻ khi activate
batch_idstring—Mã lô bureau quote lại; tự sinh PO-XXXXXXXXXX nếu bỏ trống
design_codestring—Artwork bureau cần in
include_panboolean (mặc định true)—Artwork có in PAN đầy đủ hay không
metadataobject—Ghi chú tự do

card-core mint count PAN và count pan_token ngay tại đây, rồi park chúng thành stock ordered với chip_uid = NULL. Prefix PAN lấy theo thứ tự CARD_PAN_PREFIX_PHYSICAL → CARD_PAN_PREFIX → 968686 (pseudo-BIN nội bộ, không cần BIN của Visa/Mastercard). Đặt riêng prefix cho thẻ vật lý là tuỳ chọn nhưng nên làm: PAN in và PAN sinh nội bộ nằm ở hai dải rời nhau nên không bao giờ tranh nhau cùng một số.

Vì sao response không trả file

Endpoint này không trả về file perso. Một response mang theo file mà bị mất giữa đường sẽ làm mất count số thẻ không thể thu hồi. PAN được ghi xuống DB trước, file lấy riêng ở bước 2.

pan_token là QR in trên plastic — một identifier opaque, unique trên toàn bộ stock, không phải secret. Chính nó là khoá join cho file bureau trả về ở bước 3.

2. Xuất file cho bureau — order sang exported​

GET /admin/physical-cards/print-orders/{order_id}/file
→ 200 text/csv

CSV theo RFC 4180, mỗi plastic một dòng:

include_pan = true
record_no,batch_id,pan_token,pan,pan_last4,expiry,design_code
1,PO-9F2C1A44B8,ptok_Zm9vYmFyYmF6cXV4,9686860000000018,0018,08/29,XPR-BLACK

Khi include_pan = false, cột pan không xuất hiện — plastic chỉ mang QR (pan_token) và last4.

Response kèm hai header để đối soát: X-Record-Count và X-Content-SHA256.

Mỗi lần tải:

  • order chuyển exported, ghi exported_at / exported_by;
  • export_count tăng, file_sha256 lưu digest của đúng nội dung vừa giao.

Endpoint cho phép xuất lại (bureau mất file, transfer lỗi) nhưng không bao giờ im lặng — "ai đang giữ bản copy" luôn là câu hỏi trả lời được. Order canceled thì trả 409 order_canceled. Thẻ đã activate tự động rơi khỏi bản xuất lại: pan_token đã được chuyển sang bản ghi thẻ và NULL hoá ở stock, nên không còn gì để in.

3. Nhận UID chip về — stock ordered → available​

Bureau in xong, đọc UID chip của từng plastic rồi trả file. card-core nhận file đó:

POST /admin/physical-cards/uid-import
→ 200 { imported, skipped, failed, spoiled, order_ids }
Request
{
"chip_generation": "MIFARE_CLASSIC_EV1_1K",
"interface_caps": "CONTACTLESS",
"cards": [
{ "pan_token": "ptok_Zm9vYmFyYmF6cXV4", "chip_uid": "04a1b2c3d4e580" }
],
"spoiled_pan_tokens": ["ptok_YmFkcGxhc3RpYw"]
}

Ba quyết định thiết kế đáng chú ý:

  1. Khoá join là pan_token, không phải PAN và không phải order. Token đã in trên plastic dưới dạng QR nên bureau chỉ cần đọc nó lên — file trả về không cần chứa PAN. Token unique trên toàn bộ stock nên mỗi dòng tự tìm đúng plastic và tự mang theo lô của nó: một file bureau trả gộp nhiều batch upload một lần được. Truyền order_id trong body nếu muốn siết lại một lô — khi đó dòng lạc lô sẽ fail với wrong_order.
  2. chip_generation là bắt buộc (theo dòng, hoặc default cả lô). Nó quyết định đường personalization — export khoá Crypto1 chỉ áp dụng cho Classic — nên plastic không rõ generation bị từ chối ngay, thay vì lưu NULL rồi mới lộ ra ở trạm perso khi thẻ đã nằm trong tay.
  3. Best-effort theo từng dòng, mỗi dòng một transaction riêng. Một dòng lỗi không rollback phần còn lại; operator chỉ submit lại phần failed. Một file 10k dòng abort toàn bộ thì không dùng được trong vận hành. Endpoint idempotent tự nhiên (plastic đã nhận vào sẽ vào skipped) nên không cần Idempotency-Key.

Các lỗi theo dòng: unknown_pan_token (404), wrong_order (409), invalid_stock_state (409), chip_bound_elsewhere (409), chip_uid_taken (409), chip_generation_required (422).

spoiled_pan_tokens là các plastic bureau làm hỏng. Hàng vẫn được giữ lại và chỉ đổi status = 'spoiled' — xoá đi sẽ trả PAN về bộ sinh số, và một số đã từng được in thì không bao giờ được phát lại. Token không chuyển được (không tồn tại, đã qua ordered, hoặc ngoài lô đã pin) được báo lại trong failed với code not_spoilable, không bị bỏ lặng.

Biến thể deprecated

POST /admin/physical-cards/print-orders/{order_id}/uid-import làm đúng việc trên nhưng pin theo path. Đã deprecated — token trong mỗi dòng vốn đã xác định được plastic — chỉ giữ để bureau và script cũ không vỡ.

4. Allocate cho corporate — stock sang allocated​

POST /admin/physical-cards/allocate
Idempotency-Key: <bắt buộc>

corporate_id cộng một selector:

SelectorÝ nghĩa
stock_idsDanh sách thẻ cụ thể
batch_idThẻ available trong lô đó
count (1–1000)N thẻ available cũ nhất — có thể kết hợp batch_id để giới hạn trong lô

Không selector nào → 422 missing_selector. Với stock_ids, hàng nào không available → 409 stock_not_available. Với batch_id/count, thiếu hàng → 422 insufficient_stock; query dùng FOR UPDATE SKIP LOCKED nên hai lượt allocate song song không giành nhau cùng một plastic.

4b. Pre-personalization — ghi chip trước khi corporate activate​

Bước tuỳ chọn, dành cho system admin / platform key. Nó tồn tại vì trước đây mọi byte trạm perso ghi đều phải lấy từ một hàng card_chips, mà hàng đó cần thẻ, mà thẻ chỉ có sau khi corporate activate — buộc plastic phải đi tới corporate ở trạng thái trắng rồi quay về để perso.

Thực tế không byte nào cần tới thẻ: khoá diversified derive từ chip_uid, block 4 là một token opaque, block 5 là (key_version, perso epoch, expiry in trên thẻ) và block 6 là MAC phủ ba thứ đó. Việc cần làm là giữ chỗ token + perso epoch trên hàng stock để activate nhận lại nguyên văn — sinh mới bất kỳ giá trị nào sẽ để lại một thẻ mà block 6 không còn verify, tức thẻ đọc ra như bị tamper ngay lần tap đầu.

System admin / platform key
POST /admin/physical-cards/perso/prepare # throttle 60/phút, ghi audit KEY_ISSUE
POST /admin/physical-cards/perso/confirm

Cả hai nhận một selector: stock_id, chip_uid, hoặc pan_token (QR trên plastic — tiện nhất khi thẻ đang trong tay). Không có selector nào → 422 missing_selector.

prepare trả về đúng những gì trạm ghi lên thẻ:

FieldMô tả
card_tokenBlock 4 — token opaque, 16 byte hex. Không phải PAN
key_version, kdf_refHandle khoá diversified trong HSM — không bao giờ là khoá thật
perso_epochThời điểm perso được phủ trong block 5; cố định ngay khi giữ chỗ
issuer_block, expected_mac, counter_blockNội dung block 5 / 6 / counter — chỉ Classic
sector_keysKhoá Crypto1 + trailer của cả 16 sector — chỉ Classic, null với DESFire
layoutVị trí từng field trên Classic 1K — đọc từ đây, không hardcode

Chỉ nhận plastic đang available hoặc allocated (ghi trước hay sau khi allocate đều được) và đã có chip_uid — plastic chưa qua uid-import thì chưa tồn tại về mặt vật lý nên không có UID để diversify khoá → 409 chip_unknown. Plastic đã activated → 409 invalid_stock_state: thẻ đã tồn tại và sở hữu credential của nó, perso đi qua /cards/{card_id}/chip/perso/* ở bước 6.

Idempotent theo từng plastic: token và perso epoch được giữ chỗ một lần rồi echo lại ở mọi lần gọi sau, nên ghi lại sau một lần lỗi sẽ cho đúng cùng bộ byte chứ không phải một lần personalize thứ hai. Ghi xong gọi confirm với result = WRITTEN (hoặc LOCKED — cũng là thành công, chỉ là trạm seal luôn thẻ) → stock.perso_state sang PERSONALIZED / LOCKED. Lần ghi thất bại thì đừng confirm: chỗ giữ vẫn còn, prepare lại là được.

perso_state là trục riêng, không phải status — plastic vẫn là stock cho tới khi corporate activate nó. Cả hai giá trị đều xuất hiện trong GET /admin/physical-cards.

prepare phát khoá sống

Trên Classic, response mang khoá Crypto1 thật. Giữ trong memory cho một thẻ, không log, không ghi xuống đĩa, không forward — hệt như POST /chip/keys. Mỗi lần gọi đều rơi vào chip_auth_logs với mode = KEY_ISSUE (không có credential_id vì thẻ chưa tồn tại; hàng log khoá theo chip_uid

  • card_token). DESFire chỉ nhận kdf_ref — khoá DESFire không bao giờ ra khỏi HSM.

5. Corporate admin kích hoạt stock → thẻ thật​

Corporate admin
POST /physical-cards/activate
Idempotency-Key: <bắt buộc>

Body kế thừa toàn bộ field phát hành của CreateCardDto (user_id — bỏ trống hoặc '0' là thẻ bearer, cost center, limits, verification_preference, funding_source, payment_channels, topup_mode, …) cộng thêm selector stock:

FieldMô tả
stock_id hoặc chip_uidThẻ stock cần kích hoạt (một trong hai)
applet_aidAID (hex) cho personalization chip, ví dụ F51840

Bên trong một transaction, card-core:

  1. Lock hàng stock, kiểm tra nó thuộc corporate của caller và đang allocated. Stock không tồn tại hoặc ngoài phạm vi đều trả 404 — không để lộ sự tồn tại qua 403.
  2. Tạo bản ghi cards với form_factor = 'physical', dùng lại nguyên văn PAN/token/expiry đã in trên plastic (không sinh mới, và PAN không bao giờ được reveal lại vì nó đã in trên thẻ). Plastic chỉ-QR (include_pan = false, không có PAN) sẽ rơi về đường sinh PAN nội bộ. Validate cardholder / bearer / limits / static_code giống hệt thẻ ảo.
  3. Bind credential chip PHYSICAL. Plastic chưa pre-personalize → credential mint token riêng và ở perso_state = PENDING. Plastic đã ghi ở bước 4b → credential nhận lại nguyên văn card_token + perso epoch đã nằm trên chip, và lên PERSONALIZED / LOCKED ngay (counter được seed) — tap được mà không cần ghé trạm perso. Chỗ giữ mà không ai confirm vẫn được dùng lại token (chip có thể đang mang nó) nhưng giữ PENDING để trạm perso confirm sau.
  4. Chuyển vật liệu thẻ sang bản ghi thẻ: stock sang activated, pan_token / pan_hash / encrypted_pan được NULL hoá ở stock — thẻ là nguồn dữ liệu duy nhất từ đây.

Response trả về thẻ, stock_id, chip_uid, và chip.perso_state (PENDING, hoặc PERSONALIZED/LOCKED nếu đã pre-personalize). Field pan chỉ xuất hiện khi card-core tự sinh PAN (plastic chỉ-QR); PAN đã in sẵn không bao giờ được echo lại.

Activate ở đây ≠ thẻ tiêu được

Bước này tạo thẻ ở status = inactive. Thẻ chỉ tiêu được sau bước 7. Tên "activate" ở đây nói về stock (allocated → activated), không phải về thẻ.

6. Personalization: nạp khoá lên chip — PENDING → PERSONALIZED​

Credential vừa tạo còn PENDING: khoá diversified chưa nằm trên chip nên chưa tap được (chip-tap từ chối credential_not_active). Trạm perso — desk station hoặc điện thoại Android — chạy bốn call:

POST /cards/{card_id}/chip/perso/job # back office: cấp job token + QR (platform / system admin)
POST /cards/{card_id}/chip/perso/prepare # trạm perso: lấy card_token + kdf_ref
POST /chip/keys # bộ khoá Crypto1 cho một thẻ Classic (throttle 60/phút, ghi audit)
POST /cards/{card_id}/chip/perso/confirm # kết quả → PERSONALIZED hoặc LOCKED

Trạm giữ platform key; điện thoại giữ perso job token scan từ QR — một thẻ, vài phút, tiêu ở bước confirm. Cấp job mới sẽ đóng job cũ đang mở của cùng thẻ: bản in mới phải vô hiệu hoá bản in nó thay thế, nếu không hai operator có thể làm cùng một thẻ từ hai QR khác nhau.

POST /chip/keys là surface duy nhất phát khoá sống, nên mọi lần gọi đều rơi vào chip_auth_logs với mode = KEY_ISSUE.

confirm với result = LOCKED cũng là kết thúc thành công (perso hoàn tất và khoá lại), không phải kill — cả PERSONALIZED và LOCKED đều tap được.

7. Cardholder kích hoạt thẻ — inactive → active​

Cửa duy nhất "bật tiền lên":

Cardholder
POST /my/cards/{card_id}/activate # khởi tạo OTP challenge
POST /my/otp-challenges/{id}/confirm # nhập mã → status = active

Trước bước này thẻ inert: một plastic mất trên đường vận chuyển không authorize được gì.

Không có PIN

Hệ thống không dùng PIN — nên cũng không có PIN mailer để in hay gửi. CVM là verification_preference: qr_app (mặc định), sms_otp, hoặc static_code. Xem Transaction authentication (CVM).

8. Companion perso — thẻ vật lý cho thẻ ảo đã tồn tại​

Trường hợp khác: bureau in plastic mang PAN của một thẻ (ảo) đã có, không phải từ stock.

POST /physical-cards/perso-return
→ 200 { bound, skipped, failed }

Ở đây khoá mapping là PAN đầy đủ (qua pan_hash — unique toàn cục nên khớp đúng một thẻ), rồi gắn credential PHYSICAL vào thẻ đó: không thẻ mới, không PAN mới. keys_loaded (mặc định true) nói bureau đã nạp khoá diversified lên chip chưa → PERSONALIZED (tap được ngay, counter được seed) hoặc PENDING (xác nhận lại ở trạm perso sau).

PAN phải 16 chữ số và hợp Luhn, ngược lại 422 invalid_pan. Best-effort theo dòng và idempotent tự nhiên (chip đã bind đúng thẻ đó → skipped), nên không cần Idempotency-Key. Corporate admin chỉ thao tác trong phạm vi thẻ của mình; system admin làm được mọi corporate.

Quản lý khoá​

Chip tap giữ secret trong secure element. card-core derive khoá riêng cho từng thẻ từ issuer master key, diversify theo chip_uid:

  • Master key lấy qua KeyRing (chip-master). Hôm nay là CHIP_ISSUER_MASTER_KEY trong env (EnvKeyRing); đổi sang KMS/HSM không cần sửa caller.
  • kdf_ref (handle của khoá diversified) được lưu trên credential; khoá thật không nằm trong file perso — file perso chỉ có pan_token, PAN (nếu artwork in), last4, expiry, design.
  • Vì chip_uid chỉ biết được sau khi plastic tồn tại, khoá không thể pre-bake vào QR job hay vào file in — đó là lý do cấu trúc bước 3 phải là "UID quay về" chứ không phải "UID gửi đi".

Đây là cái làm mỗi lần tap sinh một cryptogram khác nhau và làm thẻ không clone được.

Bảo mật PAN​

Cơ chếChi tiết
Lưu trữencrypted_pan (ciphertext) + pan_hash để tra cứu + pan_hash_key_version để rotate
Đường ra duy nhấtFile export ở bước 2 — có đếm (export_count) và có digest (file_sha256)
Không reveal lạiPAN đã in không bao giờ trả về qua API; chỉ PAN do card-core sinh mới hiện một lần ở response activate
Không tái sử dụng sốPlastic hỏng giữ hàng ở spoiled; xoá hàng sẽ trả PAN về bộ sinh số
Chống cạn dải sốMint theo vòng (tối đa 8), mỗi vòng chỉ sinh lại phần thiếu; hết vòng → 503 pan_allocation_failed (dải quá chật, cần BIN mới thay vì retry)

Chip: thế hệ & giao diện​

chip_generationGhi chú
DESFIRE_EV1, DESFIRE_EV2, DESFIRE_EV3EV2/EV3 bật proximity_check_enabled khi đã PERSONALIZED
MIFARE_CLASSIC_1K, MIFARE_CLASSIC_EV1_1KĐường Crypto1 — chỉ Classic mới export khoá qua POST /chip/keys

interface_caps: CONTACTLESS (mặc định), CONTACT, DUAL.

Không phải EMV

Đây là MIFARE, không phải EMV. Không có BIN Visa/Mastercard, không có applet EMV, không có certificate scheme — closed-loop nên xperise tự định nghĩa toàn bộ.

Hai kiểu artwork​

In PAN trên thẻ (include_pan: true)Chỉ QR (include_pan: false)
File exportCó cột panKhông có PAN trong file
Plastic mangPAN + last4 + QRChỉ QR (pan_token) + last4
PAN của thẻ khi activateDùng lại PAN đã incard-core sinh PAN nội bộ
Rủi roPAN rời hệ thống theo filePAN không bao giờ ra khỏi hệ thống

Cả hai vẫn tap được như nhau — pan_token chỉ là identifier, thanh toán luôn cần CVM.

Những gì card-core chưa mô hình hoá​

Cần biết rõ để không viết tài liệu hay UI dựa trên trạng thái không tồn tại:

Hạng mụcHiện trạng
QA, SHIPPED, DELIVEREDKhông có trong card_print_orders (chỉ ordered / exported / canceled). QA và vận chuyển đang nằm ngoài card-core
Đối soát nhận hàngKhông có endpoint "receive"; đối soát thực chất là bước uid-import (imported / skipped / failed / spoiled)
retired (stock)Có trong enum nhưng chưa endpoint nào set
Reissue khi hết hạn / thay thẻ mấtChưa có luồng riêng — hiện phải revoke credential rồi chạy lại print order → allocate → activate
PINKhông tồn tại (xem bước 7)
Ship model (bulk về công ty vs drop-ship cho nhân viên)Quyết định vận hành, không phản ánh trong dữ liệu

Tham chiếu API​

Base URL (staging): https://api-staging-core-card.uts.network

EndpointQuyềnIdempotency-Key
POST /admin/physical-cards/print-ordersSystem admin / platform✅
GET /admin/physical-cards/print-ordersSystem admin / platform—
GET /admin/physical-cards/print-orders/{order_id}System admin / platform—
GET /admin/physical-cards/print-orders/{order_id}/fileSystem admin / platform—
POST /admin/physical-cards/uid-importSystem admin / platform—
POST /admin/physical-cards/print-orders/{order_id}/uid-importSystem admin / platform— (deprecated)
POST /admin/physical-cards/allocateSystem admin / platform✅
POST /admin/physical-cards/perso/prepareSystem admin / platform (throttle 60/phút)—
POST /admin/physical-cards/perso/confirmSystem admin / platform—
GET /admin/physical-cards · GET /admin/physical-cards/{stock_id}System admin / platform—
GET /physical-cardsCorporate admin (tự siết theo corporate)—
POST /physical-cards/activateCorporate admin✅
POST /physical-cards/perso-returnCorporate admin / platform / system admin—
POST /cards/{card_id}/chip/perso/jobPlatform / system admin—
POST /cards/{card_id}/chip/perso/prepare · .../confirmPerso station / platform—
POST /chip/keysPerso station / platform (throttle 60/phút)—
POST /my/cards/{card_id}/activateCardholder (OTP)—

Query list stock: status, corporate_id, batch_id, cursor, limit (1–100, mặc định 20). Query list print order: status, batch_id, cursor, limit. Cả hai phân trang bằng cursor base64url(created_at|id), mới nhất trước.

Liên quan​