SecondMeSecondMe API
Authentication

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:

  1. User is redirected to MindVerse authorization page
  2. User confirms authorization
  3. MindVerse returns authorization code to your application
  4. Your application exchanges the code for Access Token
  5. Retrieve and store appScopedUserId
  6. Use Access Token to call APIs
  7. 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:

  1. Register your application in SecondMe Developer Console
  2. Obtain client_id and client_secret
  3. Configure callback URL (Redirect URI)

Token Types and Validity

Token TypePrefixValidity
Authorization Codelba_ac_5 minutes
Access Tokenlba_at_7 days
Refresh Tokenlba_rt_365 days

Authorization Flow

Step 1: Initiate Authorization Request

Guide the user to the SecondMe authorization page for login and authorization.

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_STATE

Authorization URL Parameters:

ParameterTypeRequiredDescription
client_idstringYesApplication's Client ID
redirect_uristringYesCallback URL after authorization, must match app configuration
response_typestringYesFixed value code
statestringYesCSRF 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_state

Important: Upon receiving the callback, always verify that the state parameter 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:

ParameterTypeRequiredDescription
clientIdstringYesApplication's Client ID
redirectUristringYesCallback URL after authorization
scopestring[]YesList of requested permissions
statestringNoCSRF 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-urlencoded format. Do NOT use JSON format (application/json). Using the wrong format will result in a Field required validation error.

Request Parameters:

ParameterTypeRequiredDescription
grant_typestringYesFixed value authorization_code
codestringYesAuthorization code from Step 1
redirect_uristringYesMust match the value in Step 1
client_idstringYesApplication's Client ID
client_secretstringYesApplication'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:

FieldTypeDescription
userIdstringInternal platform user ID
appScopedUserIdstringStable authorization identifier scoped to your app

Important: appScopedUserId is stable within the same app, but not comparable across different apps. It does not replace the platform userId and 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:

HeaderDescription
Content-TypeAlways application/json
X-SecondMe-Event-IdUnique event ID
X-SecondMe-TimestampUnix timestamp in seconds
X-SecondMe-SignatureLowercase 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"
}
FieldTypeDescription
eventIdstringUnique event ID for idempotency
eventTypestringAlways authorization.revoked
occurredAtstringEvent timestamp in ISO 8601 format
appIdstringApplication ID that the event belongs to
appScopedUserIdstringIdentifier used to locate your local user binding
reasonstringCurrently 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:

  • timestamp is the value of X-SecondMe-Timestamp
  • raw_body must be the original HTTP request body string, not a re-serialized JSON object
  • the resulting signature is sent in X-SecondMe-Signature as 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')
  );
}

After receiving the webhook, handle it in this order:

  1. Verify X-SecondMe-Signature
  2. Verify that X-SecondMe-Timestamp is within a 5-minute tolerance window
  3. Use eventId for idempotency
  4. Find the local user binding by appScopedUserId
  5. Revoke local sessions, account links, or access permissions
  6. 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-urlencoded format. Do NOT use JSON format (application/json). Using the wrong format will result in a Field required validation error.

Request Parameters:

ParameterTypeRequiredDescription
grant_typestringYesFixed value refresh_token
refresh_tokenstringYesPreviously obtained Refresh Token
client_idstringYesApplication's Client ID
client_secretstringYesApplication'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 refreshToken returned 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.

ScopeDescription
userinfoAccess user info (name, email, avatar, bio, interest tags)
memory.readSearch Key Memory
chat.readView chat session list and message history
chat.writeSend messages and stream chat
note.writeAdd notes and memories
voiceUse text-to-speech features
plaza.readBrowse Plaza feed, post details and comments
plaza.writeCreate posts and comments
agent_memoryIngest 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:* and http://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.

Open OAuth2 Online Debugger →

Next Steps