Skip to content

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

OrderCheck ItemFailure ResultTrigger Timing
1Withdrawal restriction flag (risk control)Direct rejection ("Account temporarily unable to withdraw")Entering funds page / clicking confirm
2Withdrawal blacklistDirect rejection ("Account restricted from withdrawing funds")Entering funds page
3Dormant account / inactive accountDirect rejection ("Dormant accounts do not support withdrawal")Entering funds page / clicking confirm
4Securities account statusRejection ("Securities account not opened/closed")Entering page
5Negative equity checkRejection ("Account has outstanding debt")Clicking confirm withdrawal
6GDCA certificationRejection ("GDCA not completed")Clicking confirm withdrawal
7NSS questionnaireRejection ("NSS questionnaire not completed")Clicking confirm withdrawal
8Bank card validityPrompt to re-bind cardSelecting receiving account
9Bank account authentication statusRejection ("Bank account not authenticated")Selecting receiving account
10Online account opening deposit thresholdRejection ("Card binding requires transfer >= HKD 10,000 or USD 1,500")Selecting receiving account / clicking confirm
11Available withdrawal methodsRestrict selectable channelsSelecting receiving account
12Currency-market consistencyRejectionClicking confirm
13Fee calculationDisplay for user confirmation (CHATS/RTGS shows popup)Clicking confirm
14Channel routingDetermine method valueClicking confirm
More restriction scenarios (low frequency but noteworthy)
Check ItemFailure ResultNotes
Mainland China bank cardRejection ("Withdrawal to mainland China bank accounts not supported")System directly blocks
A-share Connect RMB restrictionRejection ("A-share Connect CNH only supports withdrawal to HK banks")Only CNH + non-HK bank
Payoneer accountRejection ("Payoneer accounts do not support withdrawal")Business restriction
Puerto Rico regionRejectionRegional restriction
Margin withdrawable amountRejection ("Exceeds cash withdrawable amount")Margin account specific
Bank currency mismatchRejection ("Bank account currency does not match withdrawal currency")e.g., HKD account withdrawing USD
Airstar BST not authorizedRejection ("BST not authorized")Only for bank-initiated withdrawal scenario
Institutional account not authorizedRejectionInstitutional 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 in withdraw/src/app/Business/CreatorBase.php
  • Adjust blacklist → hk-withdraw-blacklist-go service management interface (database configuration, takes effect immediately)
  • Modify channel routing logic → withdraw/src/app/Business/CreatorBase.phpcalcMethod() method
  • Modify fee calculation → CreatorBase.phpgetFee() 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:

  1. User's bank card status is normal
  2. Withdrawal method is correctly set
  3. 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$stepTemplates array
  • Add new approval step → Create new Step class (implementing IFStep interface) + 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:

ChannelPrerequisite
CGB FPSFPS batch submission complete
CHATS/RTGSFile export complete
BOC FPSFPS 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:

ResultTask StatusFollow-up
SuccessDONE(2)Notify user, withdrawal complete
FailureRemains PROCESSING(1)Ops intervenes, may switch channel and retry
TimeoutRemains 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
  1. Create new xxx_list table (reference cmb_list structure)
  2. Create two queue events: SyncXxxWithdraw (pull transactions) + XxxWithdrawCreate (create task)
  3. Register in Queue.php's $_enableEvent
  4. 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:

EventWhat It Does
FundWithdrawCreateReceive redemption completion notification
FundWithdrawWaitWait for redemption funds to arrive in securities account
FundWithdrawArrivalTimeCheck expected arrival time
FundWithdrawSuccessFunds arrived → Create withdrawal task with fund template
FundWithdrawFailedArrival 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.

EventWhat It Does
CurrentDepositRedeemSuccessRedemption successful → Advance withdrawal task
CurrentDepositRedeemRejectedRedemption rejected → Mark withdrawal task as failed
CurrentDepositRedeemSbaCreateRetryRetry 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):

  1. Ops initiates reversal operation in CRM
  2. System checks status — only DONE tasks can be reversed
  3. Updates task status to REVERSE(5)
  4. SBA executes reverse fund operation
  5. 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:

TimeSystem Behavior
Daily 11:00Cutoff — Non-auto withdrawals submitted after this time are marked as "next day processing"
Next day 08:30System 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 ObserveWhat 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

MisconceptionFact
"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)

I want to...Go to
Understand why a specific rule exists and whether it can be changedWithdrawal Rules Manual
See technical details of a specific channel from Remittance to bankChannel Execution Manual
Troubleshoot a withdrawal issue by symptomWithdrawal Troubleshooting
Push a withdrawal-related requirement changeWithdrawal Change Guide
Look up what a status code/field meansWithdrawal Data Dictionary
Was this page helpful?

内部业务文档 · 仅限 moomoo 团队使用