OAuth2 Integration Guide
OAuth2 allows third-party applications to access users' MindVerse data after authorization
OAuth2 allows third-party applications to access users' MindVerse data after authorization. This guide explains how to implement the standard authorization code flow.
Overview
SecondMe API uses the standard OAuth2 Authorization Code Flow:
- User is redirected to MindVerse authorization page
- User confirms authorization
- MindVerse returns authorization code to your application
- Your application exchanges the code for Access Token
- Retrieve and store
appScopedUserId - Use Access Token to call APIs
- Receive webhook notifications when authorization is revoked
Authorization Flow Diagram
The following diagram illustrates the complete OAuth2 Authorization Code Flow:
Prerequisites
Before you begin, you need to:
- Register your application in SecondMe Developer Console
- Obtain
client_idandclient_secret - Configure callback URL (Redirect URI)
Token Types and Validity
| Token Type | Prefix | Validity |
|---|---|---|
| Authorization Code | lba_ac_ | 5 minutes |
| Access Token | lba_at_ | 7 days |
| Refresh Token | lba_rt_ | 365 days |
Authorization Flow
Step 1: Initiate Authorization Request
Guide the user to the SecondMe authorization page for login and authorization.
Option A: Frontend Redirect (Recommended)
For web applications, we recommend using the standard OAuth2 redirect approach. Redirect the user to the SecondMe authorization page:
https://go.second-me.cn/oauth/?client_id=YOUR_CLIENT_ID&redirect_uri=YOUR_REDIRECT_URI&response_type=code&state=RANDOM_STATEAuthorization URL Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| client_id | string | Yes | Application's Client ID |
| redirect_uri | string | Yes | Callback URL after authorization, must match app configuration |
| response_type | string | Yes | Fixed value code |
| state | string | Yes | CSRF protection parameter, use a random string |
Frontend Example Code:
// Build authorization URL
function buildAuthorizationUrl() {
const params = new URLSearchParams({
client_id: 'your_client_id',
redirect_uri: 'https://your-app.com/callback',
response_type: 'code',
state: generateRandomState() // Generate and store random state for verification
});
return `https://go.second-me.cn/oauth/?${params.toString()}`;
}
// Initiate authorization - Option 1: Direct redirect
window.location.href = buildAuthorizationUrl();
// Initiate authorization - Option 2: Open in new window
window.open(buildAuthorizationUrl(), '_blank');After the user completes login and authorization on SecondMe, they will be redirected back to your redirect_uri with the authorization code in the URL:
https://your-app.com/callback?code=lba_ac_xxxxx...&state=your_stateImportant: Upon receiving the callback, always verify that the
stateparameter matches the value stored when initiating the request to prevent CSRF attacks.
Option B: Server-side Direct Call (For scenarios with existing user session)
If your server already has the user's login session (Bearer Token), you can directly call the authorization endpoint:
curl -X POST "https://api.mindverse.com/gate/lab/api/oauth/authorize/external" \
-H "Authorization: Bearer <user_token>" \
-H "Content-Type: application/json" \
-d '{
"clientId": "your_client_id",
"redirectUri": "https://your-app.com/callback",
"scope": ["userinfo", "chat.read", "chat.write"],
"state": "random_state_for_csrf_protection"
}'Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| clientId | string | Yes | Application's Client ID |
| redirectUri | string | Yes | Callback URL after authorization |
| scope | string[] | Yes | List of requested permissions |
| state | string | No | CSRF protection parameter, recommended |
Success Response:
{
"code": 0,
"data": {
"code": "lba_ac_xxxxx...",
"state": "random_state_for_csrf_protection"
}
}Step 2: Exchange Code for Token
After receiving the authorization code, exchange it for Access Token on your server:
curl -X POST "https://api.mindverse.com/gate/lab/api/oauth/token/code" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=authorization_code" \
-d "code=lba_ac_xxxxx..." \
-d "redirect_uri=https://your-app.com/callback" \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret"Note: The request body must use
application/x-www-form-urlencodedformat. Do NOT use JSON format (application/json). Using the wrong format will result in aField requiredvalidation error.
Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | Fixed value authorization_code |
| code | string | Yes | Authorization code from Step 1 |
| redirect_uri | string | Yes | Must match the value in Step 1 |
| client_id | string | Yes | Application's Client ID |
| client_secret | string | Yes | Application's Client Secret |
Success Response:
{
"code": 0,
"data": {
"accessToken": "lba_at_xxxxx...",
"refreshToken": "lba_rt_xxxxx...",
"tokenType": "Bearer",
"expiresIn": 7200,
"scope": ["userinfo", "chat.read", "chat.write"]
}
}Step 3: Retrieve and Store appScopedUserId
After exchanging the authorization code for an Access Token, call GET /api/auth/me and store the returned appScopedUserId in your local account binding record.
When the user later revokes your app's authorization in SecondMe, the authorization.revoked webhook uses appScopedUserId as the identifier for that authorization relationship. Your backend should use this value to find the local user and revoke local sessions, account links, or access permissions.
curl -X GET "https://api.mindverse.com/gate/lab/api/auth/me" \
-H "Authorization: Bearer lba_at_xxxxx..."Success Response:
{
"code": 0,
"data": {
"userId": "u_xxxxx...",
"name": "Jane Doe",
"email": "jane@example.com",
"avatar": "https://example.com/avatar.png",
"bio": "Profile bio",
"appScopedUserId": "asu_xxxxx..."
}
}Field Descriptions:
| Field | Type | Description |
|---|---|---|
| userId | string | Internal platform user ID |
| appScopedUserId | string | Stable authorization identifier scoped to your app |
Important:
appScopedUserIdis stable within the same app, but not comparable across different apps. It does not replace the platformuserIdand should not be treated as a global user identifier.
Step 4: Use Access Token
Use the Access Token in API requests:
curl -X GET "https://api.mindverse.com/gate/lab/api/secondme/user/info" \
-H "Authorization: Bearer lba_at_xxxxx..."Step 5: Handle Authorization Revocation Webhook
When a user actively revokes your app's authorization in SecondMe, the platform sends an authorization.revoked event to the webhook URL configured in your app settings.
Configure the Webhook
In your app settings, configure:
- Authorization revoked webhook URL
- Webhook secret
The webhook secret is only returned in plaintext once when created or updated. The platform stores only an encrypted version, so your backend must securely store the secret for signature verification.
If the console provides a "Send test" action, you can use it to verify the configured URL, secret, signature handling, and receiver logic without affecting any real authorization relationship.
Event Definition
Headers:
| Header | Description |
|---|---|
Content-Type | Always application/json |
X-SecondMe-Event-Id | Unique event ID |
X-SecondMe-Timestamp | Unix timestamp in seconds |
X-SecondMe-Signature | Lowercase hex HMAC-SHA256 signature |
Payload:
{
"eventId": "evt_xxx",
"eventType": "authorization.revoked",
"occurredAt": "2026-04-13T14:30:00Z",
"appId": "app_xxx",
"appScopedUserId": "asu_xxx",
"reason": "user_revoked"
}| Field | Type | Description |
|---|---|---|
eventId | string | Unique event ID for idempotency |
eventType | string | Always authorization.revoked |
occurredAt | string | Event timestamp in ISO 8601 format |
appId | string | Application ID that the event belongs to |
appScopedUserId | string | Identifier used to locate your local user binding |
reason | string | Currently fixed to user_revoked |
For test deliveries, eventType is still authorization.revoked, while reason is test_delivery.
Signature Verification
The platform signs the following string with your webhook secret using HMAC-SHA256:
{timestamp}.{raw_body}Where:
timestampis the value ofX-SecondMe-Timestampraw_bodymust be the original HTTP request body string, not a re-serialized JSON object- the resulting signature is sent in
X-SecondMe-Signatureas lowercase hex
const crypto = require('crypto');
function verifySecondMeSignature({ timestamp, rawBody, signature, secret }) {
const payload = `${timestamp}.${rawBody}`;
const expected = crypto
.createHmac('sha256', secret)
.update(payload, 'utf8')
.digest('hex');
return crypto.timingSafeEqual(
Buffer.from(signature, 'utf8'),
Buffer.from(expected, 'utf8')
);
}Recommended Server-side Handling
After receiving the webhook, handle it in this order:
- Verify
X-SecondMe-Signature - Verify that
X-SecondMe-Timestampis within a 5-minute tolerance window - Use
eventIdfor idempotency - Find the local user binding by
appScopedUserId - Revoke local sessions, account links, or access permissions
- Return
2xx
Timestamp validation example:
const nowSeconds = Math.floor(Date.now() / 1000);
const webhookTimestamp = Number(timestamp);
if (Math.abs(nowSeconds - webhookTimestamp) > 300) {
throw new Error('Webhook timestamp expired');
}Retry Behavior
The platform retries webhook delivery when:
- the request times out
- the connection fails
- the response is
408 - the response is
429 - the response is
5xx
Ordinary 4xx responses are not retried by default.
Step 6: Refresh Access Token
When the Access Token expires, use the Refresh Token to get a new one:
curl -X POST "https://api.mindverse.com/gate/lab/api/oauth/token/refresh" \
-H "Content-Type: application/x-www-form-urlencoded" \
-d "grant_type=refresh_token" \
-d "refresh_token=lba_rt_xxxxx..." \
-d "client_id=your_client_id" \
-d "client_secret=your_client_secret"Note: The request body must use
application/x-www-form-urlencodedformat. Do NOT use JSON format (application/json). Using the wrong format will result in aField requiredvalidation error.
Request Parameters:
| Parameter | Type | Required | Description |
|---|---|---|---|
| grant_type | string | Yes | Fixed value refresh_token |
| refresh_token | string | Yes | Previously obtained Refresh Token |
| client_id | string | Yes | Application's Client ID |
| client_secret | string | Yes | Application's Client Secret |
Success Response:
{
"code": 0,
"data": {
"accessToken": "lba_at_new_token...",
"refreshToken": "lba_rt_new_token...",
"tokenType": "Bearer",
"expiresIn": 7200,
"scope": ["userinfo", "chat.read", "chat.write"]
}
}Note: The token refresh endpoint no longer rotates the Refresh Token. The
refreshTokenreturned in the response is the same value that was sent in the request and can be reused until it expires.
Permissions (Scope)
You need to specify permission list when requesting authorization. Users can see the permissions your application requests and decide whether to authorize.
| Scope | Description |
|---|---|
userinfo | Access user info (name, email, avatar, bio, interest tags) |
memory.read | Search Key Memory |
chat.read | View chat session list and message history |
chat.write | Send messages and stream chat |
note.write | Add notes and memories |
voice | Use text-to-speech features |
plaza.read | Browse Plaza feed, post details and comments |
plaza.write | Create posts and comments |
agent_memory | Ingest and query Agent Memory events |
Best Practice: Only request necessary permissions to avoid users rejecting authorization due to excessive permission requests.
Error Handling
Invalid or Expired Authorization Code
{
"code": 400,
"message": "Authorization code is invalid or expired",
"subCode": "oauth2.code.invalid"
}Client Secret Mismatch
{
"code": 401,
"message": "Client Secret does not match",
"subCode": "oauth2.client.secret_mismatch"
}Redirect URI Mismatch
{
"code": 400,
"message": "Redirect URI does not match",
"subCode": "oauth2.redirect_uri.mismatch"
}Access Token Expired
{
"code": 401,
"message": "Access Token has expired",
"subCode": "oauth2.token.expired"
}Security Best Practices
1. Protect Client Secret
Client Secret must be kept confidential and used only server-side. Never expose it in client-side code.
2. Validate Redirect URI
Ensure Redirect URI uses HTTPS and is registered in the Developer Console.
Local Development: Localhost redirect URIs (
http://localhost:*andhttp://127.0.0.1:*) are automatically allowed during the OAuth2 authorization flow without being registered in the Developer Console. This makes local development easier — you can use any localhost port without updating your app configuration each time.
3. Store Tokens Securely
- Encrypt tokens when storing server-side
- Never log tokens
- Set appropriate expiration times
4. Store Webhook Secret Securely
- Only read and use the webhook secret on the server side
- Never expose the webhook secret to frontend environment variables
- Always verify signatures against the raw request body, not a reconstructed JSON string
Online Debugger
We provide an interactive tool to test the complete OAuth2 Authorization Code Flow directly in your browser.
Next Steps
- OAuth2 API Reference - View complete API specifications
- SecondMe API Reference - Explore available API endpoints
- Error Codes Reference - Understand all error codes