Z Zise Developers 简体中文
Account Center › Guides

High-risk member actions need a step-up authentication ticket—who issues it and how to use it.

Step-Up Authentication

Some actions are irreversible. An API Key alone is insufficient; the end user themselves must prove their identity again at that moment. We do this on our hosted page, so you never handle authentication factors.

Strict Boundary: We Do Not Accept Your Self-Attestation

Fields such as "step_up_passed": true in a body or x-verified: 1 in a header are not accepted and never will be.

This is not a matter of distrust. The gate protects adding withdrawal addresses, reporting cards lost, and changing PINs— irreversible actions. If callers could declare the gate passed, it would no longer be a gate, just a field. Any SSRF or injection point on your side capable of constructing a body would then directly enable withdrawals to arbitrary addresses.

Viewing a card number / CVV / expiry no longer uses this gate to restrict merchants. Once ownership matches both the merchant and member, plaintext is returned. We only guarantee that our own databases and logs do not store these three values.

Your responsibilities in this flow are therefore just two: give the URL to the end user, and resend with the ticket afterward.

Actions That Require It

ActionEndpointaction
Add a withdrawal addressPOST /v1/withdraw-addresseswithdraw_address_add
Report lost / replace cardPOST /v1/cards/{id}/lostcard_lost:<卡 id>
Set card PINPOST /v1/cards/{id}/pincard_pin:<卡 id>
QR paymentPOST /v1/qrpay/paymentsqrpay_pay:<码值摘要>
Execute exchange (when the pair requires verification)POST /v1/exchange/ordersexchange:<FROM>:<TO>
Wealth subscription / redemption (when the product requires verification)POST /v1/earn/subscriptions · POST /v1/earn/redemptionsearn_subscribe:<产品 id> · earn_redeem:<产品 id>

⚠ Deleting a withdrawal address (DELETE /v1/withdraw-addresses/{id}) does not require step-up— deletion improves safety and should not be obstructed. Likewise, shipping address creation, editing, and deletion do not require it: the real risk is changing the default address → applying for a physical card → sending it to an attacker; the gate belongs at order submission.

Three Rules for When It Is Required

  1. Always—the first six rows above. These are irreversible or directly spend funds.
  2. Configuration-dependent—the exchange / transfer / wealth rows. An admin switch determines the requirement, and you can read it: GET /v1/earn/products/{id} exposes require_step_up. Reading ahead and including the step in your flow is smoother than adding it after a 400.
  3. Every QR payment. require_step_up from GET /v1/qrpay/config is always true; it is exposed so you can design the flow in advance.

⚠ Before 2026-08-14, exchange always rejected without a hosted_url—

pairs configured to require verification were therefore permanently unexecutable through the Open API.

Exchange now uses the same two-stage hosted flow as other actions. If you added a branch

telling users to contact support for this code, you can remove it.

Interaction

First call (without a ticket) → 400:


{
  "type": "invalid_request_error",
  "code": "step_up_required",
  "message": "Strong authentication is required. See hosted_url / challenge_id.",
  "request_id": "…",
  "challenge_id": "chl_9f2c…",
  "hosted_url": "https://…/hosted/step-up/chl_9f2c…",
  "expires_at": 1754872500
}

Give hosted_url to the end user through an in-app browser, SMS, or push notification. The page is on our domain. We send a 6-digit code to their registered email, and they pass by entering it correctly. expires_at is Unix seconds; validity is 5 minutes.

⚠ We do not call you back after completion. The page issues no session and returns nothing to you— it only marks the ticket passed. Ask the user to select “I have completed verification,” or resend when the page closes.

Second call: the same idempotency key and body, with one extra header:


POST /v1/withdraw-addresses
x-idempotency-key: <与第一次完全相同>
x-step-up: chl_9f2c…

The body must also be byte-for-byte identical to the first request. Recalculate the signature because the timestamp and nonce change.

The Only Idempotency Exception Where Resending the Same Key Executes Again

The usual rule is: the same idempotency key returns the same result, including errors. If the first request returned 400, sending the same key again replays that exact 400.

Step-up authentication is the only exception, explicitly handled on the server.

The reason: same-key matching hashes only the body, not headers, while x-step-up is a header— the hash is identical with or without it. Without this exception, the documented resend would hit the replay branch and return the old step_up_required unchanged; the business handler would never execute. The flow could never complete at the code level.

Therefore:

Ticket Binding and Validity

A ticket is bound to three things. Any mismatch is treated as no ticket (you receive a fresh step_up_required, not an error explaining the mismatch):

BindingWhat happens without it
MemberA ticket completed by A could move B's money
ActionA card activation ticket could reveal card credentials
Passed flagAn issued ticket would be immediately usable, reducing the whole chain to self-attestation

Plus two rules:

Viewing Card Credentials Does Not Use a Step-Up Ticket

POST /v1/cards/{id}/secure-session validates merchant, member, and card ownership, then calls the upstream in real time, returning plaintext card number / CVV / expiry. It issues no hosted_url and does not enter the idempotency cache.

Troubleshooting

SymptomLikely cause
Resending still returns step_up_required, with a new challenge_idThe user did not finish the page; the ticket exceeded 5 minutes; or x-step-up contains the full hosted_url instead of challenge_id
Resending returns 409 idempotency_key_reusedThe body differs from the original, even by one space, or this key was used on another endpoint
A new idempotency key succeeded, but reconciliation shows an extra operationYou resent an already completed operation with a new key. This case must retain the original key

⚠ Another exception to normal behavior: after a successful resend, the key does not cache the success result. Sending the same request with it again returns 409 idempotency_in_progress, rather than replaying success, and stays that way until the 24-hour window ends.

In this flow, the idempotency key is used only for the pair “initial call → resend with ticket.” Stop using it after the resend gives a definitive response. If no response arrives because the connection breaks, query the result through a GET endpoint instead of blindly sending again.