Integrate Ace Data Cloud OAuth 2.0
Allow users to authorize your application to access their Ace Data Cloud profile, service subscriptions, API Keys, usage, or orders. Use standard Authorization Code + S256 PKCE: the application opens the authorization page, and after the user agrees, it receives a one-time code, which is then exchanged for an OAuth access token.
OAuth access tokens are used to access account resource APIs; calling model APIs uses the corresponding API Key. OAuth authorization does not automatically create API Keys, activate services, or add balance.
¶ Choose an Integration Method
| Method | Applicable Scenario | After Authorization Is Complete |
|---|---|---|
| Standard OAuth Redirect | Independent websites, desktop, or mobile clients; embedded websites initiate authorization themselves | The browser returns to the application's registered redirect_uri, and the application processes code and state |
| Studio Website Component Authorization | A website embeds the Studio homepage and wants to use the visitor's current Studio account | Studio sends an authorization result message to the application; the application does not redirect, and the original page is retained |
Standard redirects do not require embed=studio, embed_origin, or postMessage. In Studio component mode, the host creates the authorization page and maintains the message bridge; third parties should integrate according to the component protocol at the end of this document, and should not construct these two parameters themselves.
¶ Endpoints and Discovery Document
curl https://auth.acedata.cloud/.well-known/oauth-authorization-server
| Purpose | Address |
|---|---|
| Browser Authorization Page | https://auth.acedata.cloud/oauth2/authorize |
| Exchange/Refresh OAuth token | POST https://auth.acedata.cloud/oauth2/token |
| Submit Revocation Request | POST https://auth.acedata.cloud/oauth2/revoke |
| Current User Information | GET https://auth.acedata.cloud/api/v1/users/me |
| OAuth Application Management Page | OAuth Applications |
| Application List/Registration API for Logged-in Users | https://auth.acedata.cloud/api/v1/oauth2/applications/ |
| Public Application Information Used by the Authorization Page | GET https://auth.acedata.cloud/api/v1/oauth2/application/<client_id>/ |
| User's Existing API Keys | GET https://platform.acedata.cloud/api/v1/credentials/?user_id=<User ID>&limit=100 |
The application management API requires account login authentication and is not an anonymous dynamic registration (DCR) API. The MCP service's own discovery/registration address is a different entry point from the account OAuth endpoints in this table; when connecting to a specific MCP, follow that service's documentation and discovery results.
The discovery document is used to obtain endpoints and protocol capabilities. The current flow returns OAuth tokens and does not return an id_token; do not handle it as a complete OpenID Connect login flow merely because an openid scope exists.
¶ Permission Scopes: Use Current Scope Names
scope consists of space-separated, case-sensitive permission names. New applications use specific permissions such as profile:read, email:read, and phone:read.
¶ Where to Obtain Requestable Permissions
After logging in, the OAuth application list API GET /api/v1/oauth2/applications/ returns available_scopes; the permission selector on the application management page uses the same catalog. These are the optional scopes that ordinary self-service applications should use.
Discovery's scopes_supported includes all permission names recognized by the platform; it does not mean your application can request each of them. Actual authorization checks all of the following:
- Whether the permission may be delegated to this application type;
- Whether the permission has been registered in this OAuth application's allowed scopes;
- Whether the current user has the requested permission.
Unknown, unregistered, non-delegable, or permissions the user does not possess will return invalid_scope. Authorization code exchange and refresh also recheck them; permissions will not be automatically added or elevated.
¶ Permissions Available to Ordinary Self-Service Applications
The following is the current catalog (2026-10-09); use available_scopes during integration.
| Scope | Purpose |
|---|---|
profile:read / profile:write |
Read/update profile |
email:read |
Read email |
phone:read |
Read phone number and region |
billing-profile:read / billing-profile:write |
Read/manage billing profile |
applications:read / applications:write |
Read/manage service subscriptions |
credentials:read / credentials:write |
Read/manage API Keys |
usage:read |
Read usage records |
orders:read / orders:write |
Read/create and manage orders |
auto-recharge:read / auto-recharge:write |
Read/manage automatic recharge settings |
recharge-cards:redeem |
Redeem recharge cards |
coin:read |
Read token-related information |
distribution:read |
Read distribution information |
connections:read / connections:write |
Read/manage connections |
skills:read / skills:write |
Read/manage skills |
openid |
The current implementation also allows UserInfo to return basic profile information; no ID Token is returned |
offline_access |
Request a refresh token to refresh after the access token expires |
For example, use profile:read to read only the profile; add email:read if email is also needed; use profile:read credentials:read to read existing API Keys. Request credentials:write only when you truly need to create or modify Keys, and add offline_access only when refresh is needed.
Some permissions are available only to platform-approved trusted applications, such as sites:read, sites:write, and platform-tokens:read; other permissions are not available to ordinary third-party OAuth applications. Client types public/confidential describe whether a secret can be safely stored and are unrelated to whether an application is trusted. Self-service applications cannot enable trusted permissions themselves.
account is the account permission selector used by the designated official account management MCP, not an “all permissions” scope for third-party applications, and cannot be mixed with other scopes. Third parties should explicitly list the specific permissions they need.
¶ Migrating from Old Documentation
| Old Syntax | Current Integration |
|---|---|
profile |
profile:read |
email |
email:read |
phone |
phone:read |
platform, platform:read, platform:write |
List the specific permissions actually needed individually; there is no universal “select all” replacement value |
Old names are not accepted as compatible aliases. Update the scopes registered for the OAuth application, the authorization URL, and your own permission checks at the same time; modifying application configuration does not add permissions to already issued tokens, and users must reauthorize.
¶ Step 1: Register an OAuth Application
Open OAuth Application Management, create an application, and fill in:
- Name, description, application Logo: displayed on one side of the application on the authorization page.
- Client type: choose Public for pure frontend, mobile, or desktop clients; choose Confidential for services that can keep the secret on the backend.
- Redirect URL: use HTTPS in production, and register the complete URL that the application actually uses to handle callbacks.
- Permissions: select the scopes actually required from the directory provided on the page.
Save the client_id. Confidential clients must store the client_secret on the server side and must not place it in HTML, frontend build variables, or URLs. Public clients do not use a secret when exchanging tokens; even if the registration response includes this field, it must not be treated as a credential that the client can keep secret.
redirect_uri must match exactly during registration, authorization, and token exchange, including the path, trailing slash, and query parameters. Each account can create up to 20 applications. The Secret is shown only during creation/rotation; after rotation, the old secret can no longer be used for exchanges that require client authentication.
¶ Step 2: The button initiates authorization
For each connection click, generate a new random state and code_verifier, and calculate:
code_challenge = BASE64URL(SHA256(code_verifier))
Store the state, verifier, and callback URL in the application's own session, then open the authorization page with URL-encoded parameters:
https://auth.acedata.cloud/oauth2/authorize
?response_type=code
&client_id=<client_id>
&redirect_uri=<complete callback URL>
&scope=profile%3Aread%20credentials%3Aread
&state=<random value for this time>
&code_challenge=<S256 challenge value for this time>
&code_challenge_method=S256
Public clients must send S256 PKCE. Although Discovery also lists plain, Public clients must not use it. Confidential clients are also recommended to use S256; if PKCE was used when initiating authorization, the corresponding verifier must be submitted during exchange.
After the user consents, they return to redirect_uri, and the query parameters include code and the returned state unchanged. When denied, error=access_denied, an error description, and state are returned. Before handling the callback, first verify that the state matches the current session. The authorization code is valid for 10 minutes and can only be exchanged once; after failure, initiate the connection again, and do not repeatedly refresh the old callback URL.
¶ Step 3: Exchange for an OAuth token
Public clients exchange directly from the browser or client; Confidential clients exchange on their own backend. The request body supports form encoding, and the following variables represent the data obtained by the application during this authorization:
curl https://auth.acedata.cloud/oauth2/token \
--data-urlencode grant_type=authorization_code \
--data-urlencode "client_id=$CLIENT_ID" \
--data-urlencode "redirect_uri=$REDIRECT_URI" \
--data-urlencode "code=$AUTHORIZATION_CODE" \
--data-urlencode "code_verifier=$PKCE_VERIFIER"
Confidential clients must also add the client_secret form field on the server side. client_secret_post is currently supported; do not treat HTTP Basic client authentication as a supported method.
From a successful response, read access_token, token_type, expires_in, and the actually granted scope. A refresh_token is available only if offline_access was requested and granted. Do not treat the OAuth access token as the model API's API Key, and do not write tokens, verifiers, or authorization codes into logs, analytics events, or shared links.
¶ Step 4: Read user information and the API Key
First use the OAuth access token to request UserInfo:
curl https://auth.acedata.cloud/api/v1/users/me \
-H "Authorization: Bearer $OAUTH_ACCESS_TOKEN"
Returned fields depend on the scope: id is the user identifier; profile:read or openid can read basic profile information (such as username, nickname, avatar, is_verified, date_joined, registration_method); email requires email:read, and phone number and region require phone:read. Do not rely on unauthorized fields always being present.
When an existing API Key is needed and credentials:read has been granted, use the id from UserInfo as the filter condition:
curl "https://platform.acedata.cloud/api/v1/credentials/?user_id=$USER_ID&limit=100&offset=0" \
-H "Authorization: Bearer $OAUTH_ACCESS_TOKEN"
The returned list uses count and items, and the Key field is items[*].token, not data[0].value. user_id=me is not a valid user ID for this API. Continue reading multi-page data using offset/limit; do not treat the first page as all credentials.
Select the applicable credential based on the application, service, and valid status; do not blindly take the first one. An empty list does not mean authorization failed; the user may not have created an applicable Key yet. The permission control of the OAuth access token and the service scope, quota, and expiration conditions of the API Key itself are different constraints.
Allowing API Key access means that a third party can store and use it. Revoking the OAuth request or clearing the application's login state will not invalidate an API Key that has already been copied; the user must revoke or rotate the Key separately in credential management.
¶ Runnable Public / PKCE browser example
Deploy the following page to your registered REDIRECT_URI, and replace CLIENT_ID and the callback URL. The button initiates standard authorization, and after returning to the same page, it reads the profile information and the number of existing Keys. The example does not display or persist Keys; production applications consume the results according to their actual use. First verify it in an independent browser page, then perform acceptance according to the iframe requirements below.
<!doctype html>
<html lang="en">
<meta charset="utf-8">
<title>Connect Ace Data Cloud</title>
<button id="connect" type="button">Connect Account</button>
<p id="status" role="status">Not connected</p>
<script type="module">
const CLIENT_ID = 'replace with your client_id';
const REDIRECT_URI = 'https://your-app.example/connect';
const AUTH_ORIGIN = 'https://auth.acedata.cloud';
const button = document.querySelector('#connect');
const status = document.querySelector('#status');
const storageKey = state => `ace-oauth:${state}`;
function base64url(bytes) {
return btoa(String.fromCharCode(...bytes))
.replace(/\+/g, '-').replace(/\//g, '_').replace(/=+$/, '');
}
async function requestJson(url, options = {}) {
const response = await fetch(url, { ...options, signal: AbortSignal.timeout(15000) });
if (!response.ok) throw new Error(`API returned HTTP ${response.status}`);
return response.json();
}
button.addEventListener('click', async () => {
button.disabled = true;
try {
const state = base64url(crypto.getRandomValues(new Uint8Array(24)));
const verifier = base64url(crypto.getRandomValues(new Uint8Array(32)));
const digest = await crypto.subtle.digest('SHA-256', new TextEncoder().encode(verifier));
sessionStorage.setItem(storageKey(state), JSON.stringify({
verifier, redirectUri: REDIRECT_URI, createdAt: Date.now()
}));
const url = new URL('/oauth2/authorize', AUTH_ORIGIN);
url.search = new URLSearchParams({
response_type: 'code', client_id: CLIENT_ID, redirect_uri: REDIRECT_URI,
scope: 'profile:read credentials:read', state,
code_challenge: base64url(new Uint8Array(digest)), code_challenge_method: 'S256'
}).toString();
window.location.assign(url);
} catch {
status.textContent = 'Unable to start connection. Please check your browser storage settings and try again.';
button.disabled = false;
}
});
async function handleCallback() {
const url = new URL(window.location.href);
if (!url.searchParams.has('code') && !url.searchParams.has('error')) return;
button.disabled = true;
try {
const state = url.searchParams.get('state');
const raw = state && sessionStorage.getItem(storageKey(state));
if (!raw) throw new Error('Authorization session does not match. Please reconnect.');
const transaction = JSON.parse(raw);
const age = Date.now() - transaction.createdAt;
if (!Number.isFinite(age) || age < 0 || age > 10 * 60 * 1000 ||
transaction.redirectUri !== REDIRECT_URI || !transaction.verifier) {
throw new Error('Authorization session has expired. Please reconnect.');
}
sessionStorage.removeItem(storageKey(state));
const code = url.searchParams.get('code');
const denied = url.searchParams.get('error');
for (const key of ['code', 'state', 'error', 'error_description']) url.searchParams.delete(key);
window.history.replaceState({}, '', url);
if (denied) throw new Error('Authorization was not completed. You can reconnect.');
if (!code) throw new Error('Callback is missing the authorization code. Please reconnect.');
status.textContent = 'Connecting…';
const token = await requestJson(`${AUTH_ORIGIN}/oauth2/token`, {
method: 'POST',
body: new URLSearchParams({
grant_type: 'authorization_code', client_id: CLIENT_ID,
redirect_uri: REDIRECT_URI, code, code_verifier: transaction.verifier
})
});
if (!token.access_token) throw new Error('Authorization token was not obtained.');
const headers = { Authorization: `Bearer ${token.access_token}` };
const profile = await requestJson(`${AUTH_ORIGIN}/api/v1/users/me`, { headers });
if (!profile.id) throw new Error('User identifier was not obtained.');
const keysUrl = new URL('https://platform.acedata.cloud/api/v1/credentials/');
keysUrl.search = new URLSearchParams({ user_id: profile.id, limit: '100' }).toString();
const credentials = await requestJson(keysUrl, { headers });
if (!Array.isArray(credentials.items)) throw new Error('Credential list format does not match.');
const keyCount = credentials.items.filter(item => typeof item.token === 'string' && item.token).length;
status.textContent = `Connected ${profile.nickname || profile.username || profile.id}; this page has ${keyCount} API Keys.`;
} catch (error) {
status.textContent = `Connection was not completed: ${error.message || 'Network error'} Please click connect again.`;
} finally {
button.disabled = false;
}
}
await handleCallback();
</script>
</html>
The callback and entry point in the example must be on the same origin and in the same browser tab/iframe in order to read the previously saved sessionStorage. Production applications should manage tokens within their own session scheme, handle expiration, and clear authorization URL parameters after receiving the response; the main page interface should render first, and network failures should affect only the connection status.
¶ iframe in white-label websites
¶ Customer website redirects to the authorization page itself
Use standard OAuth parameters and its own callback handling; window.location.assign() navigates the current iframe, while the parent page remains on the white-label domain. There is no need to add embed=studio for the icon or implement the Studio messaging protocol.
The following conditions must be met:
- The application page and callback page use HTTPS, are actually accessible, and allow embedding by the white-label parent domain through an iframe; check both parties'
Content-Security-Policy: frame-ancestorsandX-Frame-Options. - The sandbox allows the scripts, forms, and origin/storage capabilities required by the page. Supporting popup login also requires corresponding popup capabilities; prefer using the Website component configuration provided by Studio.
- The browser allows the storage required for authorization state in the current embedding scenario. The white-label site login state and the AuthFrontend login state may differ; do not assume that being logged in to the white-label site means being logged in to the authorization page.
- The token exchange service in the callback is available, and cross-origin requests allow the application origin; DNS, CORS, and HTTP errors must all display a retryable status and must not stop the entire page from rendering.
The left side of the authorization page reads the OAuth application Logo; the right side reads the favicon/logo of that Site when the browser can provide the parent origin and the parent domain passes verified-site validation; otherwise, it displays the default icon. This only determines brand presentation, does not establish a login session, and does not provide account permissions to the customer page.
Current limitation: When ordinary OAuth needs to log in again inside an iframe, there is still a gap in the flow for continuing back to the authorization page after login is completed. Before launch, you must separately verify “Auth already logged in” and “Auth not logged in/expired.” Before this gap is fixed and verified, first-time login can use a separate page to complete standard OAuth, or use the Studio component authorization below; do not promise that all iframe scenarios are available based only on logged-in test results.
¶ Use the Studio current account for authorization
Enable OAuth in Studio under “Settings → Home → Website Components,” and fill in the client_id and registered callback address. The website and callback must use the same HTTPS origin and must differ from the Studio origin. Third-party pages need to:
- Send
acedatacloud:oauth:readyto the exact parent site origin, receive and validateacedatacloud:oauth:init. - The button generates state and a PKCE verifier, and sends
acedatacloud:oauth:authorize(state, S256 challenge); the verifier remains within its own page. - Validate the parent window, origin, and state, handle the code or error in
acedatacloud:oauth:result, and exchange it through the standard token endpoint. Studio displays the authorization page in the component area. After the user agrees, it returns the authorization code by message, and the application does not need to redirect. When not logged in, it returnslogin_required; when denied, it returnsaccess_denied; when waiting times out, it returnsauthorization_timeout. Account changes rebuild the application iframe and cancel incomplete authorizations.
The component currently requests profile:read credentials:read and does not include offline_access. Do not assume that a refresh token will always be received. embed=studio and embed_origin are maintained by the Studio host and the authorization page; the client page only needs to follow the message protocol.
For a complete button example, see the Studio Website Component OAuth Integration Guide.
¶ Lifecycle, Refresh, and Revocation
The default validity period of an access token is 15 days. Refresh or reauthorize should be scheduled based on the expires_in in each response; the default validity period of a refresh token is 30 days, and the period is affected by service configuration. Do not hardcode these two values as permanent commitments.
After being granted offline_access, you can call:
curl https://auth.acedata.cloud/oauth2/token \
--data-urlencode grant_type=refresh_token \
--data-urlencode "refresh_token=$OAUTH_REFRESH_TOKEN"
Refresh returns new tokens, and the application should save the latest values. During refresh, it checks whether the application is still valid, the user is available, and permissions are still granted. On failure, clear your own connection state and reauthorize.
The current OAuth refresh implementation does not guarantee that the old refresh token becomes invalid immediately after refresh. Do not treat receiving a new token as meaning the old token has been revoked, and do not use rotating secrets as a substitute for terminating existing tokens.
The revocation request is called as follows:
curl https://auth.acedata.cloud/oauth2/revoke \
--data-urlencode "token=$OAUTH_ACCESS_TOKEN"
The current revocation endpoint returns HTTP 200, but does not implement immediate invalidation of issued JWTs. The application should clear its own saved tokens and must not treat 200 as proof that all issued tokens or API Keys have become invalid. For leaked or copied API Keys, revoke or rotate the corresponding credentials separately.
¶ Common Errors and Acceptance
| Symptom/Error | Investigation |
|---|---|
invalid_scope |
Use current permission names; check application registration scopes, delegation restrictions, and user permissions. The full Discovery directory is not an application-selectable directory |
invalid_request |
Required parameters, exact callback address, Public client S256 challenge and method |
invalid_client |
client ID, application status, Confidential client secret; check whether Basic authentication was used incorrectly |
invalid_grant |
code expired/consumed, verifier or redirect URI mismatch; application and permission changes are also checked during refresh |
access_denied |
User denied; restore the connect button, do not repeatedly pop up authorization |
unsupported_grant_type |
Currently supports authorization_code, refresh_token |
| HTTP 401/403 | Distinguish invalid login session, token, authorized scope, and access permissions of the resource itself |
Failed to fetch, DNS/CORS errors |
Verify the actual request host and client backend availability; do not attribute it to “the user has no Key” |
| Authorized but cannot read Key | Use platform.acedata.cloud, the actual user ID, items[*].token; distinguish empty lists, pagination, and failed responses |
| Entire page blank after authorization return | Render the page first; handle errors separately for callback, token, UserInfo, and credential requests; do not let initialization exceptions stop the entire page |
| iframe does not continue after login | Handle according to the existing restrictions above; test not logged in, expired, denied, and reconnect scenarios |
Before delivery, verify at least: correct callback, denial, incorrect state, expired/consumed code, no Key, network failure, token expiration, missing required scope, and first login and account switching in iframe scenarios. Do not save complete codes, tokens, or API Keys in test evidence.
¶ Two-Step Verification and Reauthorization
After a user enables two-step verification, Ace Data Cloud completes authenticator verification code or one-time recovery code validation on the authorization page, and then delivers the authorization code. Your application continues to use the standard OAuth flow and should not request, save, or forward authenticator secrets or recovery codes from users.
After a user enables, changes, disables two-step verification, or recovers an account, previous account sessions, refresh tokens, and related platform authorization tokens may become invalid. When receiving an invalid session or invalid_grant, restart a user-visible authorization flow. Do not repeatedly use old authorization codes, and do not attempt to bypass verification. The authentication method for regular model API Keys remains unchanged; if account compromise is suspected, separately check and revoke affected API Keys.
