Sản xuất & phát hành thẻ vật lý — 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 cards | Nhận UID chip từ bureau, kiểm kê tồn kho |
| Card Management spec | Allocate 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.
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ượng | Bảng | Các trạng thái |
|---|---|---|
| Print order (lệnh in một lô) | card_print_orders | ordered → exported → (canceled) |
| Stock (từng miếng plastic) | physical_card_stock | status: ordered → available → allocated → activated, hoặc spoiled / retiredperso_state (trục riêng): null → PENDING → PERSONALIZED / LOCKED |
| Thẻ (bản ghi thẻ thật) | cards | inactive → active → (blocked / expired) |
| Credential chip | card_chips | PENDING → 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ế
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
POST /admin/physical-cards/print-orders
Idempotency-Key: <bắt buộc>
| Field | Kiểu | Bắt buộc | Mô tả |
|---|---|---|---|
count | number (1–10000) | ✅ | Số thẻ cần in |
expires_at | date-time | ✅ | Hạn in trên plastic; được mang sang thẻ khi activate |
batch_id | string | — | Mã lô bureau quote lại; tự sinh PO-XXXXXXXXXX nếu bỏ trống |
design_code | string | — | Artwork bureau cần in |
include_pan | boolean (mặc định true) | — | Artwork có in PAN đầy đủ hay không |
metadata | object | — | 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ố.
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:
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, ghiexported_at/exported_by; export_counttăng,file_sha256lư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 }
{
"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ú ý:
- 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ềnorder_idtrong body nếu muốn siết lại một lô — khi đó dòng lạc lô sẽ fail vớiwrong_order. chip_generationlà 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.- 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àoskipped) nên không cầnIdempotency-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.
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_ids | Danh sách thẻ cụ thể |
batch_id | Thẻ 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.
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ẻ:
| Field | Mô tả |
|---|---|
card_token | Block 4 — token opaque, 16 byte hex. Không phải PAN |
key_version, kdf_ref | Handle khoá diversified trong HSM — không bao giờ là khoá thật |
perso_epoch | Thời điểm perso được phủ trong block 5; cố định ngay khi giữ chỗ |
issuer_block, expected_mac, counter_block | Nội dung block 5 / 6 / counter — chỉ Classic |
sector_keys | Khoá Crypto1 + trailer của cả 16 sector — chỉ Classic, null với DESFire |
layout | Vị 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ốngTrê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ậnkdf_ref— khoá DESFire không bao giờ ra khỏi HSM.
5. Corporate admin kích hoạt stock → thẻ thật
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:
| Field | Mô tả |
|---|---|
stock_id hoặc chip_uid | Thẻ stock cần kích hoạt (một trong hai) |
applet_aid | AID (hex) cho personalization chip, ví dụ F51840 |
Bên trong một transaction, card-core:
- 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 qua403. - Tạo bản ghi
cardsvớiform_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. - 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ăncard_token+ perso epoch đã nằm trên chip, và lênPERSONALIZED/LOCKEDngay (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. - 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.
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":
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ì.
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_KEYtrong 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_uidchỉ 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ất | File export ở bước 2 — có đếm (export_count) và có digest (file_sha256) |
| Không reveal lại | PAN đã 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_generation | Ghi chú |
|---|---|
DESFIRE_EV1, DESFIRE_EV2, DESFIRE_EV3 | EV2/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.
Đâ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 export | Có cột pan | Không có PAN trong file |
| Plastic mang | PAN + last4 + QR | Chỉ QR (pan_token) + last4 |
| PAN của thẻ khi activate | Dùng lại PAN đã in | card-core sinh PAN nội bộ |
| Rủi ro | PAN rời hệ thống theo file | PAN 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ục | Hiện trạng |
|---|---|
QA, SHIPPED, DELIVERED | Khô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àng | Khô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ất | Chưa có luồng riêng — hiện phải revoke credential rồi chạy lại print order → allocate → activate |
| PIN | Khô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
| Endpoint | Quyền | Idempotency-Key |
|---|---|---|
POST /admin/physical-cards/print-orders | System admin / platform | ✅ |
GET /admin/physical-cards/print-orders | System admin / platform | — |
GET /admin/physical-cards/print-orders/{order_id} | System admin / platform | — |
GET /admin/physical-cards/print-orders/{order_id}/file | System admin / platform | — |
POST /admin/physical-cards/uid-import | System admin / platform | — |
POST /admin/physical-cards/print-orders/{order_id}/uid-import | System admin / platform | — (deprecated) |
POST /admin/physical-cards/allocate | System admin / platform | ✅ |
POST /admin/physical-cards/perso/prepare | System admin / platform (throttle 60/phút) | — |
POST /admin/physical-cards/perso/confirm | System admin / platform | — |
GET /admin/physical-cards · GET /admin/physical-cards/{stock_id} | System admin / platform | — |
GET /physical-cards | Corporate admin (tự siết theo corporate) | — |
POST /physical-cards/activate | Corporate admin | ✅ |
POST /physical-cards/perso-return | Corporate admin / platform / system admin | — |
POST /cards/{card_id}/chip/perso/job | Platform / system admin | — |
POST /cards/{card_id}/chip/perso/prepare · .../confirm | Perso station / platform | — |
POST /chip/keys | Perso station / platform (throttle 60/phút) | — |
POST /my/cards/{card_id}/activate | Cardholder (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
- Issue Card flow — workflow order physical cards hướng khách hàng mà các bước trên nằm sau.
- Card Management spec — tham chiếu đầy đủ các chức năng thẻ.
- Card Payment — vòng đời thẻ, CVM, các phương thức chấp nhận.
- Card activation — chi tiết bước 7.