Withdrawal Lifecycle
About This Page
What: Follow a payment through the entire withdrawal journey — from the moment a user clicks "Submit" to the money arriving in the bank card, what happens at each step Audience: Product managers who need to understand the complete withdrawal path Prerequisites: Withdrawal Methods OverviewReading time: 5 minutes Owner: Withdrawal Product Manager
Key Takeaway: A withdrawal from submission to completion passes through six major stages: pre-checks → task creation → risk control detection → three-step approval → auto/manual execution → result confirmation. Most withdrawals complete automatically within the Confirm → Remittance steps.
Full Journey Overview
A withdrawal goes through these stages from start to finish:
Below is the complete narrative of each step in chronological order.
Step 1: User Submission + Pre-checks
After the user clicks "Submit Withdrawal" in the App, before the withdrawal task is created, the system performs a series of pre-checks. Requests that fail pre-checks do not create a withdrawal task — the user sees an error message directly in the App.
Maximum Withdrawable Amount
The system first calculates the user's maximum withdrawable amount:
Maximum Withdrawable Amount = min(Real-time Net Assets, Min_ELV, Max Credit Limit)If the withdrawal amount exceeds the maximum withdrawable amount, it is directly rejected. The calculation differs between margin accounts and cash accounts; margin accounts also need to deduct margin requirements.
Pre-check Checklist
| Order | Check Item | Failure Result | Trigger Timing |
|---|---|---|---|
| 1 | Withdrawal restriction flag (risk control) | Direct rejection ("Account temporarily unable to withdraw") | Entering funds page / clicking confirm |
| 2 | Withdrawal blacklist | Direct rejection ("Account restricted from withdrawing funds") | Entering funds page |
| 3 | Dormant account / inactive account | Direct rejection ("Dormant accounts do not support withdrawal") | Entering funds page / clicking confirm |
| 4 | Securities account status | Rejection ("Securities account not opened/closed") | Entering page |
| 5 | Negative equity check | Rejection ("Account has outstanding debt") | Clicking confirm withdrawal |
| 6 | GDCA certification | Rejection ("GDCA not completed") | Clicking confirm withdrawal |
| 7 | NSS questionnaire | Rejection ("NSS questionnaire not completed") | Clicking confirm withdrawal |
| 8 | Bank card validity | Prompt to re-bind card | Selecting receiving account |
| 9 | Bank account authentication status | Rejection ("Bank account not authenticated") | Selecting receiving account |
| 10 | Online account opening deposit threshold | Rejection ("Card binding requires transfer >= HKD 10,000 or USD 1,500") | Selecting receiving account / clicking confirm |
| 11 | Available withdrawal methods | Restrict selectable channels | Selecting receiving account |
| 12 | Currency-market consistency | Rejection | Clicking confirm |
| 13 | Fee calculation | Display for user confirmation (CHATS/RTGS shows popup) | Clicking confirm |
| 14 | Channel routing | Determine method value | Clicking confirm |
More restriction scenarios (low frequency but noteworthy)
| Check Item | Failure Result | Notes |
|---|---|---|
| Mainland China bank card | Rejection ("Withdrawal to mainland China bank accounts not supported") | System directly blocks |
| A-share Connect RMB restriction | Rejection ("A-share Connect CNH only supports withdrawal to HK banks") | Only CNH + non-HK bank |
| Payoneer account | Rejection ("Payoneer accounts do not support withdrawal") | Business restriction |
| Puerto Rico region | Rejection | Regional restriction |
| Margin withdrawable amount | Rejection ("Exceeds cash withdrawable amount") | Margin account specific |
| Bank currency mismatch | Rejection ("Bank account currency does not match withdrawal currency") | e.g., HKD account withdrawing USD |
| Airstar BST not authorized | Rejection ("BST not authorized") | Only for bank-initiated withdrawal scenario |
| Institutional account not authorized | Rejection | Institutional account specific |
These error codes belong to the 140670xxx series, complete list in Withdrawal Data Dictionary § Pre-check Error Codes.
If requirements change: Modifying pre-checks
- Add new check item → Add to
check*()method chain inwithdraw/src/app/Business/CreatorBase.php - Adjust blacklist →
hk-withdraw-blacklist-goservice management interface (database configuration, takes effect immediately) - Modify channel routing logic →
withdraw/src/app/Business/CreatorBase.php→calcMethod()method - Modify fee calculation →
CreatorBase.php→getFee()method
Step 2: Task Creation + Channel Routing
After all checks pass, the system creates a withdrawal task (writes to the tasks table) and simultaneously determines the withdrawal channel.
Channel routing is automatically decided by calcMethod():
Key boundary: Even if the bank card supports BST, if it's CMB/CMBC + offshore RMB CNH, the system does not select BST but leaves it for ops to manually select. Because CMB/CMBC BST has additional restrictions for CNH. Airstar is not affected.
When method = null, ops manually selects from 9 available channels at the Confirm step.
After task creation, status is PENDING (0), and multiple async events are queued (risk control detection, bank card status updates, etc.) to begin subsequent processing.
Step 3: Risk Control Detection
Within seconds of task creation, the system automatically executes high-risk detection (HighRiskCheck), using 6 factors to determine if this withdrawal is abnormal.
Of the 6 factors, only 4 trigger additional review (USER, AREA, FRAUDULENT, SWIFT); when hit, the task template upgrades from default to unusual — approval goes from 2 steps to 3 steps.
FREQUENCY and AMOUNT only log, they do not trigger additional review — because they already have independent safety mechanisms (10 transactions per day limit, three-tier limit system).
Detailed 6-factor explanation and bitmask rules → Withdrawal Rules Manual § High-Risk Determination
Step 4: Three-Step Approval
After risk control detection completes, the task status becomes PROCESSING (1), entering the approval flow. Up to three steps:
Step 1: Audit (High-risk Review) — unusual template only
Only withdrawals flagged as abnormal require this. Ops staff reviews whether the withdrawal poses a risk — checking user account status, withdrawal destination, whether the amount is abnormal.
Most normal withdrawals skip this step.
Step 2: Confirm (Confirm Instructions) — All withdrawals
All withdrawals pass through this step. Ops confirms:
- User's bank card status is normal
- Withdrawal method is correctly set
- For tasks with
method = null, manually select the withdrawal channel
BST extra verification: Confirm validates that BST authorization (Mandate) status is OPEN. If it's not OPEN, the withdrawal cannot proceed — ops needs to guide the user to complete BST authorization first.
Important: Confirm does not execute the actual transfer; this action is reserved for Remittance.
Step 3: Remittance (Remit Funds) — All withdrawals
This is the step where funds actually leave. The system first checks auto-withdrawal conditions (6 conditions detailed in Rules Manual):
- All pass → Automatically calls
startTransfer(), entering the auto-execution path in the next section - Any fails → Degrades to manual; ops confirms then manually triggers transfer
Approval can be fully automatic
For BST channel normal withdrawals (non-unusual), if all auto-withdrawal conditions are met, both Confirm and Remittance are automatically advanced by the system. From the user's perspective it's "submitted and arrives within minutes".
If requirements change: Modifying approval flow
- Modify approval steps →
withdraw/src/app/Business/Task.php→$stepTemplatesarray - Add new approval step → Create new Step class (implementing
IFStepinterface) + add to template array - Modify auto-withdrawal conditions →
withdraw/src/app/Business/AutoSetting.php - Detailed change guide → Withdrawal Change Guide
Step 5(A): BST Fully Automated Execution
If the channel is BST (auto_bs), after Remittance calls startTransfer(), it enters the fully automated path:
CMB/CMBC: Results obtained in real-time via Socket bidirectional link, typically completes in seconds.
Airstar: Results obtained via REST API in two-phase polling — fast polling (AsbBstTransfer every 5 seconds, up to 10 times, ~50 seconds) and fallback sync (SyncAsbBstWithdraw continuously syncing within a 2-hour window). If still incomplete after fallback sync, it's marked as abnormal and requires ops to manually query the Airstar API to confirm bank-side status.
Balance deduction note: Remittance first deducts the withdrawal amount from auto_settings. If the balance falls below the alarm threshold → sends Feishu alert; falls below the stop threshold → automatically closes auto-withdrawal for that currency. This deduction happens before the bank transfer — even if the bank rejects it, the balance has already been deducted and requires ops to manually restore.
Channel technical details → Channel Execution Manual § BST
Step 5(B): Manual Channel Execution
All non-BST channels (online banking, FPS, traditional) are ops-driven:
Key difference: Non-BST channels call startTransfer() at the Confirm step (not Remittance), because it's a synchronous call — SBA immediately returns transfer_manual. Then after ops completes the transfer at the bank, they click confirm at the Remittance step.
Some channels have prerequisites at Remittance:
| Channel | Prerequisite |
|---|---|
| CGB FPS | FPS batch submission complete |
| CHATS/RTGS | File export complete |
| BOC FPS | FPS submission complete |
Channel technical details → Channel Execution Manual
Step 6: Result Processing
Whether BST or manual channel, the final result is one of three outcomes:
| Result | Task Status | Follow-up |
|---|---|---|
| Success | DONE(2) | Notify user, withdrawal complete |
| Failure | Remains PROCESSING(1) | Ops intervenes, may switch channel and retry |
| Timeout | Remains PROCESSING(1) | Ops queries bank status, manually confirms or retries |
BST callback code reference:
- 0 = Success → Task DONE
- -5 = Timeout → Automatically switches to backup server for retry
- -6 = Bank rejection → Marked as failed, requires manual handling
Detailed exception troubleshooting → Withdrawal Troubleshooting
Other Trigger Methods
The above describes the standard path of "user initiates withdrawal in moomoo App". There are several non-standard trigger methods:
Bank-Initiated Withdrawal
CMB, CMBC, and Airstar support bank-side initiated withdrawal — the user operates transfer-out in the bank app, and moomoo passively receives:
The system pulls bank transactions every minute via queue consumers. When a new bank-side withdrawal is discovered, it automatically creates a task with method fixed as auto_bs, entering the standard approval flow.
cmb_list / ms_list record statuses: 0=pending → 1=processing → 2=success (task created) / 3=failed
If requirements change: Supporting new bank's bank-initiated withdrawal
- Create new
xxx_listtable (referencecmb_liststructure) - Create two queue events:
SyncXxxWithdraw(pull transactions) +XxxWithdrawCreate(create task) - Register in
Queue.php's$_enableEvent - Integrate bank-side transaction query API
Fund Redemption Withdrawal
Fund redemption is not user-initiated withdrawal, but the system automatically creates a withdrawal task after successful redemption:
Fund redemption has an independent chain of 5 events:
| Event | What It Does |
|---|---|
| FundWithdrawCreate | Receive redemption completion notification |
| FundWithdrawWait | Wait for redemption funds to arrive in securities account |
| FundWithdrawArrivalTime | Check expected arrival time |
| FundWithdrawSuccess | Funds arrived → Create withdrawal task with fund template |
| FundWithdrawFailed | Arrival failed → Requires manual handling |
This chain may take T+1 to T+3 days, depending on the fund redemption arrival time.
Fund Redemption ≠ User-Initiated Withdrawal
Trigger method differs (system automatic vs user manual), pre-flow differs (must wait for funds to transfer from fund account to securities account), but risk control handling is the same (both go through HighRiskCheck).
Cash Treasure Redemption Withdrawal
Cash Treasure (demand deposit product) redemption is similar to fund redemption, but the callback method differs — via SrvPush (service push) callback rather than queue polling.
| Event | What It Does |
|---|---|
| CurrentDepositRedeemSuccess | Redemption successful → Advance withdrawal task |
| CurrentDepositRedeemRejected | Redemption rejected → Mark withdrawal task as failed |
| CurrentDepositRedeemSbaCreateRetry | Retry when SBA creation fails |
CRM-Initiated Withdrawal
Ops can initiate a withdrawal for a user directly from the CRM system (without the user operating in the App). Applicable scenarios:
- User cannot log into App (e.g., device malfunction)
- Batch refund/compensation operations
- Special circumstances requiring ops assistance
CRM-initiated withdrawals follow the same approval flow and risk control detection as user-initiated withdrawals, but skip some App-side pre-checks (such as GDCA).
OM Account Withdrawal
OM (Omnibus) accounts are consolidated position accounts. During withdrawal, the Remittance step first executes OmWithdrawDeduct (OM deduction), deducting funds from the OM sub-account. If OM deduction fails, the withdrawal task is stuck at the Remittance step and requires manual handling.
OM withdrawal also supports BST auto-withdrawal, but the prerequisite is that OM deduction has been completed.
Reversal
When funds need to be recovered after a withdrawal is completed (DONE), ops initiates a reversal (REVERSE):
- Ops initiates reversal operation in CRM
- System checks status — only DONE tasks can be reversed
- Updates task status to REVERSE(5)
- SBA executes reverse fund operation
- Notifies user that withdrawal has been reversed
Trigger reasons: bank return, erroneous withdrawal, post-hoc risk control intercept.
Async-Driven: Why There Are Sometimes Waits
You may have noticed that many steps in the withdrawal flow have intervals of seconds to minutes. This is because the withdrawal system uses event-driven queues — each async operation is an "event" placed in a database queue table, processed sequentially by background processes.
11:00 Cutoff Rule and 8:30 Batch Processing
Withdrawal tasks have a critical day-cut time point:
| Time | System Behavior |
|---|---|
| Daily 11:00 | Cutoff — Non-auto withdrawals submitted after this time are marked as "next day processing" |
| Next day 08:30 | System automatically transitions previous day's post-11:00 tasks from "next day processing" to "processing", ops begins handling |
This means non-auto withdrawals (non-BST channels) submitted by users after 11:00 won't actually begin processing until next day 08:30. BST auto-withdrawals are not affected by this rule (processed anytime within service hours).
NSS Name Screening
Cross-border wire transfers (tele_transfer) must pass NSS (Name Screening Service) checks before execution, verifying the payee name is not on sanctions lists. Withdrawals failing NSS require manual escalation, with ops contacting the compliance team for assessment.
| What You Observe | What Actually Happened |
|---|---|
| "Approval didn't start for a few seconds after submission" | Risk control detection event waiting in queue |
| "BST withdrawal took a few minutes" | System is polling bank for results (every 5~60 seconds) |
| "CGB FPS withdrawal took a long time" | CGB polling up to 1000 times (can reach hours) |
| "Fund redemption withdrawal took several days" | Waiting for redemption funds to arrive from fund account to securities account |
Queue consumers are started every minute by cron, using database locks to avoid duplicate consumption. If a dedicated consumer process for a particular event type crashes, those events will be stuck.
This Is Not a Bug
Intervals are the normal behavior of async design. If a withdrawal is abnormally stuck for a long time, the troubleshooting direction is: check if the corresponding queue consumer process is running.
Common Misconceptions
| Misconception | Fact |
|---|---|
| "Once the user clicks submit, the withdrawal begins" | Not yet. The system first executes 14 pre-checks; if any fails, the withdrawal task won't even be created. The user sees an error message directly in the App |
| "Processing = bank is transferring money" | Not necessarily. "Processing" includes the Audit → Confirm → Remittance three-step approval and may be stuck at the ops confirmation stage — the bank hasn't received an instruction yet |
| "BST withdrawal arrives instantly" | CMB/CMBC is typically seconds, but Airstar can take up to 2 hours (fast polling + fallback sync). Even for CMB, there's SBA freeze/deduct/send instruction processing time |
| "Cannot withdraw after 11:00" | You can still withdraw. BST auto-withdrawal is not affected by this rule. The 11:00 cutoff only affects non-BST channels — those tasks are delayed until next day 08:30 to begin processing |
| "CRM-initiated withdrawal skips risk control" | It does not. CRM-initiated withdrawals go through the same approval flow and risk control detection (HighRiskCheck), only skipping some App-side pre-checks (such as GDCA) |
What to Read Next
| I want to... | Go to |
|---|---|
| Understand why a specific rule exists and whether it can be changed | Withdrawal Rules Manual |
| See technical details of a specific channel from Remittance to bank | Channel Execution Manual |
| Troubleshoot a withdrawal issue by symptom | Withdrawal Troubleshooting |
| Push a withdrawal-related requirement change | Withdrawal Change Guide |
| Look up what a status code/field means | Withdrawal Data Dictionary |