Skip to main content

Mã lỗi luồng thanh toán merchant

Tài liệu này liệt kê toàn bộ mã lỗi có thể trả về ở ba endpoint của một lần thanh toán tại quầy:

Thứ tự gọiEndpointVai trò
1POST /merchant/chip/tap/prepareXin khoá Crypto1 để đọc thẻ Classic và tăng counter (chỉ dùng cho chip_tap)
2POST /merchant/chip/tap/cryptogramKý tap sau khi đã tăng counter (chỉ dùng cho chip_tap)
3POST /merchant/payments/authorizeTạo giao dịch

Card-core tách bạch 2 tầng lỗi:

TầngHTTPHình dạng responseÝ nghĩa
Lỗi request / hệ thống4xx, 5xx{ "code": "...", "message": "...", "details"? }Request sai, không có quyền, hoặc hệ thống hỏng — chưa có giao dịch nào được tạo
Từ chối nghiệp vụ (decline)201{ "status": "declined", "decline_reason": "...", ... }Request hợp lệ, giao dịch đã được ghi nhận nhưng bị từ chối

POS/app không được coi 201 + status=declined là lỗi kỹ thuật. Đó là kết quả hợp lệ của một lần thanh toán và phải hiển thị decline_reason cho nhân viên.

Hai endpoint chip ở mục 1–2 không có tầng decline: chúng chỉ trả 200 hoặc một mã lỗi HTTP.


1. POST /merchant/chip/tap/prepare​

Trả 200 kèm key material (token_key_a, counter_key_b, issuer_block, expected_mac, layout). Key chỉ giữ trong RAM cho một lần tap — không cache, không ghi xuống đĩa.

1.1 Xác thực & tần suất​

HTTPcodeNguyên nhânXử lý phía client
401unauthorizedKhông gửi credential; X-Merchant-Key không tồn tại hoặc merchant không activeKiểm tra credential
401token_expiredBearer JWT merchant hết hạn / sai chữ kýRefresh token rồi retry
401session_invalidSession sau access token đã bị thu hồiĐăng nhập lại
403forbiddenToken hợp lệ nhưng không được phép trên route nàyKhông retry
429rate_limitedVượt 120 request / 60 giây (tính theo IP)Backoff rồi retry

Giới hạn 120/60s là cho cả cửa hàng, không phải cho từng máy: throttler khoá theo IP, mà một shop NAT chung một IP. Một tap tốn 2 request (prepare + cryptogram).

1.2 Validate request​

HTTPcodeNguyên nhân
422validation_failedchip_uid không phải chuỗi hex, thiếu chip_uid, hoặc gửi thừa field lạ

1.3 Trạng thái credential​

HTTPcodeNguyên nhânXử lý phía client
404credential_unknownchip_uid không thuộc issuer nàyHiển thị "không phải thẻ của hệ thống"; không retry
409credential_revokedCredential đã bị thu hồi (mất/cắp/nghi ngờ clone)Hướng dẫn chủ thẻ liên hệ issuer
409credential_not_activeperso_state chưa phải PERSONALIZED hoặc LOCKED (thường là PENDING — personalization chưa hoàn tất)Thẻ cần perso lại
409not_classicThẻ là DESFire — khoá DESFire không rời HSM, không có key exportDùng luồng DESFire, không dùng broker này

1.4 Lỗi hệ thống​

HTTPcodeNguyên nhân
500internal_errorKhông dẫn xuất được khoá sector (dữ liệu credential hỏng: UID ngắn hơn 4 byte, card_token sai độ dài, thiếu khoá cho sector cấu hình trong layout)

2. POST /merchant/chip/tap/cryptogram​

Gọi sau khi đã tăng counter trên thẻ và đọc lại giá trị mới. Trả 200 kèm nonce, cryptogram, key_version, card_token, counter — đưa thẳng vào authorize với entry_mode=chip_tap.

Endpoint này dùng cùng bộ mã lỗi với prepare (mục 1.1 → 1.4), cộng thêm:

2.1 Validate request​

HTTPcodeNguyên nhân
422validation_failedchip_uid không phải hex; counter không phải số nguyên, < 1, hoặc > 4294967295 (uint32)

counter phải là giá trị sau khi tăng, không phải giá trị trước. chip_counters được seed = 0 lúc personalization nên tap đầu tiên luôn ≥ 1.

2.2 Counter​

HTTPcodeNguyên nhânXử lý phía client
409counter_replaycounter không lớn hơn last_counter của lần tap đã verify gần nhấtTăng counter trên thẻ và đọc lại trước khi ký — không retry với cùng giá trị

Đây là lá chắn cố ý. Cùng điều kiện đó nếu lọt tới authorize sẽ thành decline chip_counter_replay, và với CHIP_AUTO_REVOKE_ON_REPLAY=true (mặc định) sẽ thu hồi credential — thẻ thật ngừng hoạt động, phải perso lại. Chặn tại đây biến một bug của app thành 409 vô hại thay vì làm hỏng thẻ.


3. POST /merchant/payments/authorize — lỗi HTTP (4xx / 5xx)​

3.1 Xác thực & phân quyền​

HTTPcodeNguyên nhânXử lý phía client
401unauthorizedKhông gửi credential; X-Merchant-Key không tồn tại hoặc merchant không ở trạng thái active; X-API-Key saiKiểm tra lại credential, không retry tự động
401token_expiredBearer JWT của merchant hết hạn hoặc không verify được chữ kýGọi refresh token rồi retry
401session_invalidSession đứng sau access token đã bị thu hồiĐăng nhập lại
403forbiddenToken hợp lệ nhưng không được phép dùng cho route nàyKhông retry
403forbidden_sub_account_allocationSub-account operator truyền sub_account_id của người khác — chỉ admin và parent key mới tạo hộ được. Áp dụng cho cả POST /merchant/payment-intentsBỏ sub_account_id, hoặc dùng credential admin
429rate_limitedVượt giới hạn tần suấtBackoff rồi retry

3.2 Validate request​

HTTPcodeNguyên nhân
422validation_failedBody không hợp lệ: amount sai định dạng số tiền, currency không khớp ^[A-Z]{3}$, mcc không phải 4 chữ số, entry_mode ngoài enum, cryptogram/nonce không phải hex, counter âm, key_version ngoài 0–255, thừa field lạ…
422missing_idempotency_keyThiếu header Idempotency-Key (bắt buộc)
422merchant_mismatchbody.merchant_id khác merchant của API key đang dùng
422sub_account_not_foundbody.sub_account_id không phải sub-account đang active của merchant này (sai merchant / đã xoá / đã disable / không tồn tại đều chung một lỗi). Áp dụng cho cả POST /merchant/payment-intents

3.3 Idempotency​

HTTPcodeNguyên nhânXử lý phía client
409idempotency_conflictDùng lại Idempotency-Key cũ với payload khácSinh key mới cho giao dịch mới
409request_in_flightRequest gốc với key này vẫn đang xử lýRetry nguyên văn sau vài trăm ms

3.4 Sai điều kiện theo entry_mode​

HTTPcodeNguyên nhân
422validation_failedentry_mode=manual_pan nhưng card_token không phải PAN đủ 16 chữ số
422validation_failedentry_mode=card_qr nhưng card_token không đúng dạng ptok_…
422validation_failedentry_mode=chip_tap nhưng card_token không phải 32 ký tự hex thường
422validation_failedentry_mode=chip_tap nhưng thiếu cryptogram, counter hoặc nonce
422unsupported_entry_modeentry_mode không được phép trên surface merchant (ví dụ s2s — chỉ dành cho platform)

Merchant chỉ được dùng 4 mode: token, manual_pan, card_qr, chip_tap.

3.5 Không tìm thấy credential​

HTTPcodemessageBối cảnh
404not_foundcard token not foundentry_mode=token, one-time token không tồn tại
404not_foundcard not foundmanual_pan / card_qr, PAN hoặc pan_token không khớp thẻ nào
404not_foundchip credential not foundchip_tap, chip token không tồn tại

Token/PAN/chip không tồn tại → 404 (không có thẻ nào để gán decline). Token/chip tồn tại nhưng không hợp lệ → 201 decline (xem mục 4).

Lưu ý khác biệt so với hai endpoint chip ở trên: ở prepare/cryptogram, UID lạ trả 404 credential_unknown; ở authorize, chip token lạ trả 404 not_found.

3.6 Mở challenge xác thực chủ thẻ​

Chỉ xảy ra với manual_pan / card_qr / chip_tap, sau khi đã qua hết các gate nghiệp vụ:

HTTPcodeNguyên nhân
422no_registered_phoneThẻ không có số điện thoại nào — cả chủ thẻ lẫn người dùng được link thẻ — → không phát được SMS OTP
422otp_recipient_invalidotp_recipient_id không phải một trong các số của thẻ (xem 5.4) — chưa mở giao dịch nào
422static_code_unavailableThẻ cấu hình verification_preference=static_code nhưng chưa có mã tĩnh
502otp_delivery_failedNotification Center trả lỗi hoặc timeout — toàn bộ auth bị rollback, chưa có giao dịch treo

3.7 Lỗi hệ thống​

HTTPcodeNguyên nhân
500internal_errorLỗi không lường trước; response không lộ chi tiết

4. POST /merchant/payments/authorize — decline_reason (HTTP 201)​

Liệt kê theo đúng thứ tự gate mà authorize chạy qua — gate nào chặn trước thì đó là decline_reason được trả về.

4.1 One-time token — chỉ entry_mode=token​

decline_reasonNguyên nhânHướng xử lý
token_invalid_or_expiredToken đã dùng, đã hết hạn, hoặc số tiền vượt max_amount gắn trên tokenYêu cầu khách mở lại QR trong app
card_link_inactiveToken do nhân viên được chia sẻ thẻ tạo, nhưng quyền dùng thẻ của họ đã ngoài thời gian công ty cho phép (hoặc đã bị gỡ)Khách dùng thẻ khác; quyền dùng thẻ do admin công ty cấp lại
card_locked_too_many_attemptsThẻ đang bị khoá 15 phút do nhập sai mã xác thực 5 lần — chỉ áp dụng cho token có PINChờ hết 15 phút, hoặc dùng cách thanh toán khác
velocity_limit_exceededQuá 5 lần mở challenge cho cùng một thẻ trong 5 phút — chỉ áp dụng cho token có PINChờ, đừng cho khách mint token liên tục

⚠️ Token có PIN không approved ngay tại authorize. Token được mint với ttl_seconds > 180 mang theo một PIN 6 số (app hiển thị cho chủ thẻ đúng một lần). Với token đó authorize trả pending_verification + verification.method = "token_pin", và sale chỉ chốt sau khi PIN qua POST /merchant/payments/{auth_id}/verify — xem mục 5.

ttl_seconds mặc định là 179 — nằm ngay dưới ngưỡng. App không truyền ttl_seconds thì vẫn nhận token không PIN, approve ngay tại authorize, đúng như trước. Vào luồng PIN là opt-in: phải chủ động xin TTL > 180.

Mặc định trước đây là 300; hạ xuống 179 chính là để POS đang chạy không bị đổi hành vi. Đổi lại, token không chỉ định TTL giờ sống 179s thay vì 300s. POS nào muốn chắc chắn thì vẫn nên xử lý được cả hai dạng response của entry_mode=token.

4.2 Chip tap — chỉ entry_mode=chip_tap​

decline_reasonNguyên nhânHướng xử lý
chip_cryptogram_invalidCMAC không verify — sai key, sai thứ tự message, sai cách encode counterLỗi tích hợp, không retry tap
chip_nonce_replayedNonce này đã được dùng cho một cryptogram hợp lệ trước đóXin nonce mới cho mỗi lần tap
chip_counter_replay ⚠️Counter không tăng — tín hiệu cloneXem cảnh báo bên dưới
chip_credential_revokedCredential đã bị thu hồi (mất/cắp/nghi ngờ lộ)Hướng dẫn chủ thẻ liên hệ issuer
chip_credential_not_activeCredential còn ở trạng thái PENDING — chưa confirm personalizationCần perso lại thẻ

⚠️ chip_counter_replay thu hồi credential ngay trong cùng transaction khi CHIP_AUTO_REVOKE_ON_REPLAY=true (mặc định). Thẻ sẽ ngừng hoạt động và phải personalize lại. Một bug ở app làm replay counter sẽ hỏng thẻ thật — chạy staging với CHIP_AUTO_REVOKE_ON_REPLAY=false khi đang tích hợp.

Nếu app luôn đi qua /merchant/chip/tap/cryptogram thì counter sai đã bị chặn sớm bằng 409 counter_replay (mục 2.2) và không bao giờ tới được decline này.

4.3 Trạng thái thẻ — mọi entry mode​

decline_reasonNguyên nhân
card_inactiveThẻ chưa kích hoạt
card_blockedThẻ bị khoá
card_expiredThẻ hết hạn (theo status hoặc theo expires_at)
card_outside_validity_windowThẻ đang ngoài các khoảng thời gian admin công ty cho phép dùng (validity của thẻ) — ai trả cũng bị từ chối
channel_not_allowedKênh chấp nhận tương ứng đang tắt trên thẻ (chỉ áp dụng khi xuất trình thẻ vật lý: manual_pan, card_qr, chip_tap)
card_locked_too_many_attemptsThẻ đang bị khoá 15 phút do nhập sai mã xác thực 5 lần

4.4 Velocity — manual_pan, card_qr, chip_tap​

decline_reasonNguyên nhân
velocity_limit_exceededQuá 5 lần mở challenge cho cùng một thẻ trong 5 phút (chống spam SMS và brute-force PIN)

4.5 Hạn mức thẻ — mọi entry mode​

decline_reasonNguyên nhân
mcc_not_allowedMCC nằm ngoài allowed_mccs của thẻ
merchant_not_allowedMerchant nằm ngoài allowed_merchant_ids của thẻ
per_transaction_limit_exceededVượt hạn mức mỗi giao dịch
daily_limit_exceededVượt hạn mức ngày (đã tính cả hold đang giữ)
monthly_limit_exceededVượt hạn mức tháng (đã tính cả hold đang giữ)

4.5b Ngân sách tháng của công ty — mọi entry mode​

decline_reasonNguyên nhân
corporate_monthly_limit_exceededVượt corporate_settings.monthly_limit của công ty (đã tính cả hold đang giữ)

Khác monthly_limit_exceeded ở chỗ đây là trần của toàn công ty, không phải của một thẻ — nên nó decline mọi thẻ trong công ty cùng lúc. Chỉ phát sinh khi công ty đặt monthly_limit_action = 'block'; mặc định là 'warn', chỉ báo chứ không chặn.

Cửa này trễ một giao dịch theo thiết kế: không chặn cho tới khi đã có một capture vượt vạch, nên giao dịch làm tràn ngân sách vẫn qua và giao dịch kế tiếp mới bị chặn. Refund kéo chi tiêu về dưới trần, hoặc admin nâng trần, đều tự mở khoá — không cần thao tác gì thêm.

4.6 Số dư — mọi entry mode​

decline_reasonNguyên nhân
insufficient_fundsSố dư khả dụng của ví thẻ (đã trừ hold active) không đủ

Đây là decline do card-core tự sinh. Ví Xperise hết tiền là mã khác — service_payment_insufficient_funds (mục 4.9).

4.7 Policy nội bộ (ZEN decision table) — mọi entry mode​

decline_reasonNguyên nhân
policy_violationDecision table từ chối mà không nêu lý do, hoặc engine lỗi/không có JDM và số tiền vượt degraded_threshold (fail-closed)
(chuỗi tuỳ biến)Decision table trả về reason riêng của doanh nghiệp — ví dụ vượt ngân sách chuyến công tác, cần approval

4.8 Employee policy hook (xperise-backend) — mọi entry mode​

decline_reasonNguyên nhân
employee_policy_violationHook trả decision=declined mà không kèm reason
(chuỗi tuỳ biến)reason do hook trả về
employee_policy_unavailableHook trả non-2xx, timeout (4s) hoặc lỗi mạng — fail-closed, không bao giờ bỏ qua âm thầm

Khi EMPLOYEE_POLICY_URL không cấu hình, gate này được bỏ qua hoàn toàn.

4.9 Ví Xperise (service-payment) — chỉ entry_mode=token​

Với manual_pan / card_qr / chip_tap, việc trừ ví diễn ra ở POST /merchant/payments/{auth_id}/verify — không ở authorize.

decline_reasonresult_codeNguyên nhân
service_payment_auth_failed13Ví từ chối credential/chữ ký của card-core
service_payment_invalid_request20Ví từ chối định dạng request
service_payment_duplicate_order41(partner_name, order_id) đã tồn tại — external_ref bị trùng
service_payment_user_invalid42User không hợp lệ phía ví
service_payment_in_flight43Một giao dịch cùng key đang xử lý — retry nguyên văn
service_payment_failed99Ví báo lỗi chung
service_payment_insufficient_funds1001Ví không đủ số dư
service_payment_declined(khác)result_code không nằm trong danh sách trên
service_payment_unavailable—HTTP non-2xx, timeout (10s) hoặc lỗi mạng — fail-closed

Mọi mã ở bảng này đều mang tiền tố service_payment_ — đó là cách duy nhất để POS phân biệt "ví Xperise từ chối" với decline do chính card-core sinh ra. Đặc biệt lưu ý service_payment_insufficient_funds (ví hết tiền, mục này) khác hẳn insufficient_funds (hạn mức/số dư thẻ, mục 4.6 và 4.10): hai tình huống cần hướng dẫn người dùng khác nhau.

message do ví trả về không nằm trong response — nó chỉ được ghi log kèm một lệnh curl tái hiện đúng request đã gửi.

Khi SERVICE_PAYMENT_URL / CLIENT_CODE / SECRET_KEY chưa cấu hình, gate này được bỏ qua.

4.10 Sale mode (auto_capture=true) — chỉ entry_mode=token​

decline_reasonNguyên nhân
insufficient_fundsSố dư ví thẻ không còn đủ tại thời điểm post ledger

5. Trường hợp pending_verification​

manual_pan, card_qr, chip_tap — và token khi token đó có PIN — khi qua hết mọi gate không trả approved mà trả:

{
"status": "pending_verification",
"verification": {
"method": "sms_otp",
"expires_at": "2026-07-31T09:12:00.000Z",
"attempts_remaining": 5,
"phone_masked": "****1234",
"verification_deadline": "2026-07-31T09:19:00.000Z",
"resends_remaining": 2,
"can_resend_at": "2026-07-31T09:09:30.000Z",
"recipient_required": false,
"recipients": null
}
}

method là một trong ba:

methodMã cần nhậpphone_maskedGhi chú
sms_otpMã gửi qua SMS****1234Gửi lại được
static_codePIN in trên thẻnullKhông có gì gửi lại
token_pinPIN của one-time token, app hiện đúng một lần lúc mintnullKhông có gì gửi lại; hết hạn cùng token

token_pin ưu tiên hơn verification_preference của thẻ: PIN đó là mã được phát ra cùng chính token này, nên là mã duy nhất chủ thẻ có thể có trong tay.

Không có tiền nào bị giữ ở bước này — spam authorize không khoá được số dư, và một authorize mới cho cùng thẻ + merchant sẽ tự động void auth pending_verification cũ.

5.1 Hai cái đồng hồ — expires_at vs verification_deadline​

Đây là chỗ dễ hiểu sai nhất, và là lý do POS không phải tạo giao dịch mới khi mã hết hạn:

TrườngMặc địnhHết hạn thì sao
expires_at5 phút (OTP_TTL_SECONDS)Chỉ mã hiện tại chết. Auth vẫn pending_verification → xin mã mới
verification_deadline10 phút (VERIFICATION_DEADLINE_SECONDS)Cả giao dịch chết → declined / verification_expired, phải authorize lại

static_code và token_pin là ngoại lệ: cả hai PIN đều đã được phát ra một lần và không có kênh nào để phát lại — resends_remaining luôn 0, can_resend_at: null, và verification_deadline bằng luôn expires_at.

Với token_pin thì cái đồng hồ đó là thời hạn còn lại của token, không phải OTP_TTL_SECONDS: PIN không thể sống lâu hơn token nó bảo vệ, nên một token ttl_seconds=600 được xuất trình ở giây thứ 500 chỉ còn 100 giây để nhập PIN.

5.2 POST /merchant/payments/{auth_id}/verify​

LoạiMãNguyên nhân
HTTP 422verification_not_pendingAuth không có challenge nào đang chờ
HTTP 422otp_recipient_requiredChưa có mã nào được gửi — thẻ được link nhiều người, POS phải chọn số trước (5.4). Không trừ lượt nhập. details.recipients là danh sách số
HTTP 422otp_expiredMã đã quá expires_at nhưng giao dịch còn sống → gọi /resend-verification, đừng tạo giao dịch mới. details trả kèm verification_deadline, resends_remaining, can_resend_at
Decline 200verification_expiredĐã quá verification_deadline — hết đường cứu, authorize lại
Decline 200card_locked_too_many_attemptsSai mã lần thứ 3 → khoá manual_pan/card_qr/chip_tap và mọi token có PIN trên thẻ 15 phút. Nếu challenge là token_pin thì token cũng bị đánh dấu đã dùng luôn
Decline 200(toàn bộ mục 4.9)Trừ ví Xperise thực hiện tại bước này
Decline 200insufficient_fundsSố dư không đủ khi tạo hold hoặc post ledger

/verify trả HTTP 200 (không phải 201 như /authorize), kể cả khi status=declined.

Gọi /verify trên một auth đã captured / approved / declined / voided / expired không phải lỗi — API trả về trạng thái hiện tại của auth.

5.3 POST /merchant/payments/{auth_id}/resend-verification​

Gửi lại SMS OTP cho giao dịch còn đang chờ. Trả HTTP 200 + auth kèm block verification mới. Dùng khi khách không nhận được SMS, hoặc mã đã quá expires_at mà chưa quá verification_deadline.

Nó re-arm lại đúng challenge cũ (mã mới, expires_at mới, attempts_remaining về 3) chứ không mở challenge mới — nên không tốn slot velocity (velocity_limit_exceeded đếm số challenge/thẻ/5 phút). Mã cũ chết ngay khi endpoint này trả về.

Không nhận Idempotency-Key: "gửi mã nữa" là một side effect thật, replay lại response cũ sẽ im lặng không gửi SMS. Cooldown là thứ giữ cho việc bấm 2 lần vô hại.

LoạiMãNguyên nhân
HTTP 429resend_too_soonCòn trong cooldown (mặc định 30s). details.retry_after_seconds nói còn phải đợi bao lâu
HTTP 422resend_limit_reachedHết quota resend của giao dịch (mặc định 2 → tổng 3 SMS). details.max_resends
HTTP 422resend_not_applicablemethod=static_code (PIN in trên thẻ) hoặc method=token_pin (PIN phát cùng token) — không có gì để gửi lại. Với token_pin, muốn có PIN mới thì phải mint token mới trong app
HTTP 422verification_not_pendingAuth đã settle hoặc challenge đã dùng hết
HTTP 422otp_recipient_requiredChallenge chưa gửi mã (recipient_required: true) mà body không có recipient_id. details.recipients là danh sách số để chọn
HTTP 422otp_recipient_invalidrecipient_id không phải một trong các số của thẻ
Decline 200verification_expiredĐã quá verification_deadline — endpoint decline luôn auth và trả về, POS biết ngay phải authorize lại

Body (tuỳ chọn): { "recipient_id": "…" } — gửi mã tới số đó trong verification.recipients (5.4). Không có body thì gửi lại đúng số cũ.

Trần SMS thực tế mỗi thẻ: 3 challenge/5 phút × (1 + 2 resend) = 9 SMS/thẻ/5 phút.

Một thẻ sms_otp có thể nhận mã ở nhiều số: số của chủ thẻ, rồi số của từng người dùng được link thẻ (theo thứ tự link), chỉ những ai có số điện thoại trong hồ sơ. Khi có hơn một số, block verification liệt kê chúng (chỉ số đã che, không kèm tên):

"verification": {
"method": "sms_otp",
"phone_masked": null,
"recipient_required": true,
"recipients": [
{ "recipient_id": "1024", "phone_masked": "****1234" },
{ "recipient_id": "2048", "phone_masked": "****5678" }
],
"resends_remaining": 2,
"can_resend_at": "2026-07-31T09:07:00.000Z",
…
}
  • Authorize không kèm otp_recipient_id (POS chưa biết thẻ có những số nào): giao dịch vẫn mở pending_verification nhưng chưa gửi SMS nào — recipient_required: true, phone_masked: null. POS cho khách chọn số của mình trong recipients, rồi gọi POST /payments/{auth_id}/resend-verification với { "recipient_id": "…" }. Lần gửi đầu này không tính là resend: không chờ cooldown (can_resend_at = lúc authorize), không trừ resends_remaining.
  • Authorize kèm otp_recipient_id: gửi luôn tới số đó (POS đã có recipients của thẻ từ một giao dịch trước).
  • Đã gửi rồi mà khách chọn nhầm số: resend-verification với recipient_id khác — tính là một lần resend (cooldown + quota như thường).
  • Thẻ chỉ có một số: recipients: null, hành vi như cũ — gửi luôn tới số đó.
  • /verify khi chưa gửi mã → 422 otp_recipient_required, không mất lượt nhập.
  • GET /merchant/payments/{auth_id} trả lại cả recipients, nên POS mất response authorize vẫn chọn tiếp được.

6. Bảng tra nhanh cho POS​

Tình huốngNhận biếtHành động
prepare trả 404 credential_unknown—"Không phải thẻ của hệ thống", dừng
prepare trả 409 credential_revoked / credential_not_active / not_classic—Không retry; hướng dẫn theo mục 1.3
cryptogram trả 409 counter_replay—Tăng counter trên thẻ, đọc lại, ký lại — không retry cùng giá trị
Bị 429 rate_limited ở luồng chipNhiều máy chung IPBackoff; lưu ý mỗi tap tốn 2 request
Mất kết nối giữa chừng ở authorizeKhông có responseRetry cùng Idempotency-Key; nếu nhận 409 request_in_flight thì đợi rồi retry tiếp
Bị 409 idempotency_conflictPayload đã đổiSinh key mới, coi như giao dịch mới
Retry cùng Idempotency-Key cho giao dịch đang pending_verificationNhận lại nguyên response 201 gốc (cùng auth_id, cùng verification.expires_at / verification_deadline dạng ISO datetime) — replay là snapshot lúc tạo, không phản ánh trạng thái hiện tạiDùng auth_id để tiếp tục /verify. Nếu verification_deadline đã qua thì /verify trả 201 declined / verification_expired → authorize lại với key mới
Bị 201 declinedstatus=declinedHiển thị decline_reason, không retry tự động
Bị 201 pending_verificationCó block verificationNhập mã → gọi /verify
pending_verification với recipient_required: trueThẻ được link nhiều người, chưa gửi SMSCho khách chọn số trong verification.recipients → /resend-verification với recipient_id (5.4)
Khách bảo số nhận mã không phải số của mìnhrecipients khác null/resend-verification với recipient_id khác — tính là một lần resend
Khách không nhận được SMSCòn pending_verification, resends_remaining > 0POST /payments/{auth_id}/resend-verification sau can_resend_at — không tạo giao dịch mới
/verify trả 422 otp_expiredQuá expires_at, chưa quá verification_deadlineGọi /resend-verification rồi nhập mã mới. Giao dịch vẫn sống
Bị 429 resend_too_soon—Đợi details.retry_after_seconds giây rồi gọi lại
Bị 422 resend_limit_reachedresends_remaining = 0Hết quota mã cho giao dịch này → authorize lại (tốn 1 slot velocity)
Bị 422 resend_not_applicablemethod=static_codeĐọc PIN in trên thẻ, không có SMS nào để gửi lại
Bị 422 resend_not_applicablemethod=token_pinHỏi khách PIN mà app đã hiện lúc quét QR; nếu khách không còn thì mint token mới
Decline verification_expiredQuá verification_deadline (10 phút)Giao dịch chết, authorize lại từ đầu
Decline velocity_limit_exceeded3 giao dịch OTP cho cùng thẻ trong 5 phútĐợi hết cửa sổ 5 phút, hoặc chuyển sang entry_mode=token (QR app). Lưu ý resend không tính vào đây
Mất response của authorize—GET /merchant/payments/{auth_id} trả lại cả block verification để tiếp tục, không cần tạo giao dịch mới
Bị 502 otp_delivery_failed—Không có auth treo, có thể authorize lại từ đầu