Skip to content

Getting Started ​

The API is standard REST / HTTP. You can call it from any language with an HTTP client. No dedicated SDK installation is required.

API Host ​

  • HTTP API - https://webapi.moomoo.com
  • WebSocket Quote - wss://webapi-quote.moomoo.com/ws
  • WebSocket Trade - wss://webapi-trade.moomoo.com/ws

INFO

Time unit differences: quote APIs mostly use Unix milliseconds (for example listing_date), with some fields in seconds (for example wrt_maturity_date); trading and crypto business times use Unix microseconds (for example order create_time, history start/end, and cash-log begin_time). The auth header X-Timestamp uses milliseconds. Date fields such as data_date use YYYY-MM-DD in the security's market timezone.

Choose an Authentication Method ​

moomoo OpenAPI supports two authentication methods. We recommend using Method 1: OAuth 2.1 + PKCE first.

Authentication methodRecommendationBest forRequest authentication
Method 1: OAuth 2.1 + PKCERecommendedThird-party apps, user authorization, and apps that access account or trading resources on behalf of usersAuthorization: Bearer {access_token}
Method 2: Traditional API KeyCompatibleServer-side systems, server jobs, and compatibility with existing API Key integrationsX-Api-Key + Authorization: {signature_base64}

OAuth 2.1 + PKCE is the recommended authentication method. It uses Bearer Tokens to call APIs, so you do not need to store an API private key or calculate a signature for every REST request.

Use Cases ​

Use this method for third-party apps, desktop apps, mobile apps, web apps, and other scenarios that require user authorization.

Scopes ​

During OAuth consent, the user can grant the following scopes. After you exchange the code for a token, the scope field echoes what was granted (space-separated):

ScopeDescription
quote:readRead core market data such as real-time quotes and historical candlesticks
quote:writeManage watchlists
trade:readQuery account assets, positions, historical orders and fills, and related reads
trade:writePlace, modify, and cancel orders, and complete order confirm
  • For funds, positions, orders, and fills only, trade:read is enough.
  • Place, modify, cancel, and order confirm require trade:write.
  • If the API returns insufficient permission, confirm the token scope covers the endpoint; to expand access, have the user authorize again.

trade:write means the API layer may accept trading write requests. It does not replace whether the account is eligible to trade a given market — check enable_market (and related fields) on authorized trading accounts.

Step 1: Register an OAuth Client ​

Run the following command to register an OAuth client and obtain a client_id:

bash
curl -X POST https://webapi.moomoo.com/oauth2/register \
     -H "Content-Type: application/json" \
     -d '{
            "redirect_uris": ["http://localhost:60355/callback"],
            "token_endpoint_auth_method": "none",
            "grant_types": ["authorization_code","refresh_token"],
            "response_types": ["code"],
            "client_name": "My moomoo OpenAPI"
        }'

Response example:

json
{
  "client_id": "4a8bcd69-e915-4778-9583-17ad0e9e6a80",
  "client_id_issued_at": 1782357937,
  "client_name": "My moomoo OpenAPI",
  "redirect_uris": ["http://localhost:60355/callback"],
  "grant_types": ["authorization_code", "refresh_token"],
  "token_endpoint_auth_method": "none",
  "response_types": ["code"],
  "registration_access_token": "2827551884d13cbed3ea58280b44765a56eae0cca39587b732bda239b322a35a",
  "registration_client_uri": "https://webapi.moomoo.com/oauth2/register/4a8bcd69-e915-4778-9583-17ad0e9e6a80",
  "scope": "quote:read quote:write trade:read trade:write accid:*",
  "pkce_required": true
}

Save the client_id for later steps.

Step 2: Direct the User to Authorize and Receive the Authorization Code ​

After obtaining the client_id, generate and temporarily store state and code_verifier. Then build the authorization URL and direct the user to open it in a browser.

code_verifier should be a high-entropy random string. code_challenge is calculated from code_verifier:

text
code_challenge = BASE64URL-ENCODE(SHA256(code_verifier))

Store state and code_verifier in the current authorization session. They are required later for callback validation and token exchange.

Request URL:

text
GET https://webapi.moomoo.com/oauth2/authorize/confirm

This URL is the browser authorization page. The user signs in, selects the scopes to authorize, and confirms authorization on this page. Developers do not need to call the internal server authorization interface directly.

Query parameters:

ParameterRequiredExampleDescriptionSource
client_idYes4a8bcd69-e915-4778-9583-17ad0e9e6a80OAuth client ID that identifies your appReturned by the OAuth client registration API
code_challengeYesstlSAHmH-iuYaK76djkKQpu7Jk1uAh_Dq09M_EYXDXkPKCE challenge used to prevent misuse if the authorization code is interceptedCalculated from code_verifier as BASE64URL-ENCODE(SHA256(code_verifier))
code_challenge_methodYesS256Method used to calculate code_challengeAlways pass S256
redirect_uriYeshttp://localhost:60355/callbackCallback URL after the user completes authorizationMust exactly match one of the redirect_uris used when registering the OAuth client
response_typeYescodeOAuth response typeAlways pass code
stateYes{random_state}Random string used to prevent CSRF attacks. You can also use it to preserve business contextGenerated by the developer and validated during callback handling

Example authorization URL after encoding and assembling the parameters. It is split across lines for readability. Remove line breaks and indentation before opening it in a browser.

text
https://webapi.moomoo.com/oauth2/authorize/confirm?
  client_id=4a8bcd69-e915-4778-9583-17ad0e9e6a80&
  code_challenge=stlSAHmH-iuYaK76djkKQpu7Jk1uAh_Dq09M_EYXDXk&
  code_challenge_method=S256&
  redirect_uri=http%3A%2F%2Flocalhost%3A60355%2Fcallback&
  response_type=code&
  state={random_state}

Authorization callback:

redirect_uri is an HTTP endpoint provided by your app. It is not an OpenAPI endpoint. Using http://localhost:60355/callback as an example, after the user completes authorization, the browser sends a GET request to your app with code and state in the query string:

text
http://localhost:60355/callback?code={authorization_code}&state={state}

Handle it like a normal HTTP request:

  1. Listen on localhost:60355 for the /callback route.
  2. Read code and state from the query string.
  3. Check that the returned state matches the value saved before authorization.
  4. If the check passes, use code and the saved code_verifier in Step 3 to exchange for tokens.

Pseudocode:

text
on GET /callback:
  code = query["code"]
  state = query["state"]

  if state != saved_state:
    return "invalid state"

  exchange code and saved_code_verifier for tokens

authorization_code can only be used to exchange for tokens. It is not an access token for OpenAPI calls. The authorization code expires in 5 minutes and can only be used successfully once. Exchange it for tokens immediately after receiving the callback. If it expires or is reused, start the authorization flow again.

Step 3: Exchange the Authorization Code for Tokens ​

Body parameters:

ParameterRequiredExampleDescription
grant_typeYesauthorization_codeAlways pass authorization_code
codeYes{authorization_code}Authorization code returned in the callback
client_idYes4a8bcd69-e915-4778-9583-17ad0e9e6a80OAuth client ID
redirect_uriYeshttp://localhost:60355/callbackMust exactly match the redirect_uri used in the authorization URL
code_verifierYes{code_verifier}Original random string used to generate code_challenge

Request example:

bash
curl -X POST https://webapi.moomoo.com/oauth2/token \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "grant_type=authorization_code" \
     -d "code={authorization_code}" \
     -d "client_id=4a8bcd69-e915-4778-9583-17ad0e9e6a80" \
     -d "redirect_uri=http://localhost:60355/callback" \
     -d "code_verifier={code_verifier}"

Response example:

json
{
  "access_token": "xxxx",
  "token_type": "Bearer",
  "expires_in": 7200,
  "refresh_token": "yyyy",
  "scope": "quote:read trade:read accid:123456"
}

Response fields:

FieldDescription
access_tokenAccess token for calling business APIs; valid for 2 hours (expires_in is typically 7200)
token_typeFixed value: Bearer
expires_inLifetime of access_token, in seconds
refresh_tokenRefresh token used to obtain a new access_token after the current one expires; default lifetime is 14 days, and can be set to 3 hours–30 days when creating the Client
scopeScopes authorized by the user, separated by spaces

Step 4: Refresh the Access Token ​

When access_token expires, use refresh_token to obtain a new access_token. Keep refreshing within the refresh_token lifetime so automated calls do not fail when access_token expires.

Body parameters:

ParameterRequiredExampleDescription
grant_typeYesrefresh_tokenAlways pass refresh_token
refresh_tokenYes{refresh_token}Refresh token returned when exchanging tokens
client_idYes4a8bcd69-e915-4778-9583-17ad0e9e6a80OAuth client ID

Request example:

bash
curl -X POST https://webapi.moomoo.com/oauth2/token \
     -H "Content-Type: application/x-www-form-urlencoded" \
     -d "grant_type=refresh_token" \
     -d "refresh_token={refresh_token}" \
     -d "client_id=4a8bcd69-e915-4778-9583-17ad0e9e6a80"

The flow above is the main Public Client + PKCE flow. If you use a Confidential Client with token_endpoint_auth_method=client_secret_post, include client_secret in the request body when refreshing.

Response example:

json
{
  "access_token": "zzzz",
  "token_type": "Bearer",
  "expires_in": 7200,
  "scope": "quote:read trade:read accid:123456"
}

The refresh request does not rotate refresh_token. Continue storing the original refresh_token securely.

Token lifetime and long-running jobs

  • access_token: Valid for 2 hours. When it expires, exchange refresh_token for a new one—no browser re-authorization required.
  • refresh_token: Default lifetime is 14 days; when creating the Client you can set it to 3 hours–30 days. Within that window you can keep exchanging for access_token.
  • Backend system maintenance: Does not invalidate a still-valid refresh_token, and does not require re-authentication.
  • Cloud server use: After the first user authorization, store refresh_token securely and let your Linux cloud service refresh and call APIs automatically—no daily browser login. Re-run the authorization flow only after refresh_token itself expires.

Step 5: Call a REST API ​

After obtaining access_token, include the Bearer Token in the Authorization request header for REST API calls:

text
Authorization: Bearer {access_token}

You do not need to calculate a request signature or pass client_id again in REST API requests. The server identifies the user, OAuth client, and authorized scopes from access_token, then checks whether the token has the required permissions for the requested API.

The following example queries the Hong Kong market trading calendar:

bash
curl -X GET "https://webapi.moomoo.com/api/v1.0/quote/trading-days?market=HK&start=2025-12-22&end=2025-12-26" \
     -H "Authorization: Bearer {access_token}"

Parameter description:

ParameterPositionExampleDescription
marketQueryHKMarket prefix
startQuery2025-12-22Start date in yyyy-MM-dd format
endQuery2025-12-26End date in yyyy-MM-dd format

If access_token expires or is invalid, refresh it with refresh_token and retry. If the response indicates insufficient permission, check whether the scopes authorized by the user cover the requested API.

OAuth Advantages

  • No need to store an API private key
  • No need to calculate a signature for every REST request
  • Token-based authorization is better suited for user authorization in third-party apps

Token Security

Store OAuth Tokens securely in your application, such as in an encrypted file or a secure keychain. Do not store them in environment variables.

Method 2: Traditional API Key (Compatible) ​

Traditional API Key authentication is mainly used for compatibility with existing server-side integrations. Before each REST API call, sign the request content with your private key.

Use Cases ​

Use this method for server-side systems, scheduled server jobs, internal tools, and other scenarios where the developer manages the key material directly.

Step 1: Create an AppKey ​

Go to https://open.moomoo.com/dashboard, open User Center, create an AppKey, and upload the public key. When creating the AppKey, choose a signature algorithm and keep the corresponding private key secure locally.

Traditional API Key authentication uses asymmetric key signatures. When creating an AppKey, choose a signature algorithm and upload the corresponding public key. When calling APIs, the client signs the request content with the local private key. The server uses the AppKey to find the public key and algorithm, then verifies the signature.

Supported signature algorithms:

AlgorithmDescription
Ed25519Sign the signing string directly with an Ed25519 private key
RSA-SHA256Sign the signing string with an RSA private key using PKCS#1 v1.5 + SHA256

Private Key Security

The private key should only be stored in your own secure environment. Do not upload it to the platform, commit it to a code repository, or write it in plaintext to logs or configuration files.

Step 2: Construct the Signature ​

Before each REST API call, sign the current request content with the private key corresponding to the AppKey. The signing string consists of 5 fields joined by newline characters \n:

text
{timestamp_ms} + "\n" +
{http_method} + "\n" +
{request_path} + "\n" +
{query_string} + "\n" +
{body_part}

Do not omit the newline separators. Even if query_string or body_part is empty, keep the field position and the newline separators.

Field description:

FieldDescription
timestamp_msCurrent millisecond timestamp, matching the X-Timestamp request header
http_methodHTTP method in uppercase, such as GET or POST
request_pathURL path without domain or query parameters, such as /api/v1.0/quote/trading-days
query_stringRaw query string in the final request, without the leading ?. Use an empty string if there are no query parameters
body_partLowercase hex SHA256 digest of the raw request body bytes. Use an empty string if there is no request body

The signature must be based on the final request that will be sent. The parameter order and URL encoding in query_string must exactly match the actual request. body_part must be calculated from the raw request body bytes. Do not reformat JSON before calculating the digest.

Using the Hong Kong market trading calendar request as an example:

text
GET https://webapi.moomoo.com/api/v1.0/quote/trading-days?market=HK&start=2025-12-22&end=2025-12-26

If timestamp_ms=1782357937000, and the GET request has no request body, the exact signing string is:

text
1782357937000\nGET\n/api/v1.0/quote/trading-days\nmarket=HK&start=2025-12-22&end=2025-12-26\n

Expanded by line:

text
1782357937000
GET
/api/v1.0/quote/trading-days
market=HK&start=2025-12-22&end=2025-12-26
<empty body_part>

The final \n joins query_string and the empty body_part. After signing, Base64-encode the signature bytes and use the result as the value of the Authorization request header.

Algorithm usage:

AlgorithmSigning method
Ed25519Sign the signing string directly with the Ed25519 private key
RSA-SHA256First calculate SHA256 of the signing string, then sign with the RSA private key using PKCS#1 v1.5

Step 3: Call a REST API ​

When calling a REST API, include the following authentication information:

HeaderRequiredDescription
X-Api-KeyYesAppKey ID
AuthorizationYesBase64-encoded signature. For AppKey authentication, use the signature string directly and do not add the Bearer prefix
X-TimestampYesCurrent millisecond timestamp from the client. It must match timestamp_ms in the signing string
X-NonceYesClient-generated random string used to prevent replay. Only letters, digits, underscores, and hyphens are supported. Length: 1-64

The following example calls the trading calendar API with AppKey authentication:

bash
curl -X GET "https://webapi.moomoo.com/api/v1.0/quote/trading-days?market=HK&start=2025-12-22&end=2025-12-26" \
     -H "X-Api-Key: {app_key}" \
     -H "X-Timestamp: {timestamp_ms}" \
     -H "X-Nonce: {nonce}" \
     -H "Authorization: {signature_base64}"

{timestamp_ms} must be the current millisecond timestamp and must match the timestamp_ms used in the signing string in Step 2. {signature_base64} is the Base64-encoded signature result. The server uses X-Api-Key to find the uploaded public key and signature algorithm, then verifies the same signing string.

If the offset between the client timestamp and the server timestamp exceeds the threshold (5 seconds by default), the API returns the error code -12006. The client can obtain the server timestamp through the following API:

bash
curl -X GET https://webapi.moomoo.com/api/v1.0/server-time

Response
{"server_time_ms":"1782971427455"}

General Conventions ​

  • Security identifier: {market}.{code}, such as HK.00700 or US.AAPL.
  • Time: Quote APIs mostly use Unix milliseconds, with some fields in seconds. Trading and crypto business times use Unix microseconds. The auth header X-Timestamp uses milliseconds. Date fields use YYYY-MM-DD in the security's market timezone.
  • Ratios: Percentage values. For example, 1.23 means 1.23%.

Common Error Codes ​

HTTP statusCodeMessageWhat to do
401-12006WebAPI authentication failedFollow the auth guide and adjust how you authenticate
439-12009Blocked by WebAPI security policyCheck that this API is authorized, for example trade read or write
429-12011Request was rate limitedReduce the request rate
401-12102Request token expiredTokens have a default lifetime. Refresh the token in time
504-12301Request timed outRetry later

Next Steps ​