SeloraXDEVELOPERS

OAuth 2.0 Flow

OAuth 2.0 Authorization Code Flow

SeloraX uses the OAuth 2.0 Authorization Code grant to let merchants authorize third-party apps to access their store data. This is the primary authentication method for apps that act on behalf of a merchant.

Step 1: Initiate Authorization

When a merchant clicks "Install" on your app, the SeloraX dashboard redirects them to the platform's authorization endpoint.

GET /api/apps/oauth/authorize?client_id=X&redirect_uri=Y&scope=Z&state=S&store_id=22
ParameterRequiredDescription
client_idYesYour app's client ID (format: sx_app_...)
redirect_uriYesMust match one of the URLs in your app's redirect_urls array
scopeYesComma-separated list of requested scopes (e.g. read:orders,write:products)
stateYesRandom string to prevent CSRF attacks. Verify this on callback.
store_idYesThe store the merchant is authorizing access to

What the platform does:

  1. Validates the merchant is logged in (middleware: [auth, admin])
  2. Checks that client_id exists and the app is active
  3. Confirms redirect_uri is in the app's registered redirect_urls array
  4. Generates an authorization code: sx_ac_<64 hex characters>, SHA256-hashed before storage, with a 60-second TTL
  5. Redirects to: {redirect_uri}?code=sx_ac_...&state={state} (note: store_id is not included in the redirect — it is returned in the token exchange response)

:::warning Authorization codes expire after 60 seconds. Exchange them immediately. :::

Step 2: Exchange Code for Tokens

Once your app receives the authorization code on the callback URL, exchange it for access and refresh tokens.

POST /api/apps/oauth/token

This endpoint is rate limited to 10 requests per minute.

No Authorization header is required. Authentication is done via the request body.

Request body:

{
  "grant_type": "authorization_code",
  "client_id": "sx_app_...",
  "client_secret": "sx_secret_...",
  "code": "sx_ac_...",
  "redirect_uri": "https://app.example.com/oauth/callback"
}

Response:

{
  "access_token": "sx_at_...",
  "refresh_token": "sx_rt_...",
  "token_type": "bearer",
  "expires_in": 86400,
  "scope": "read:orders write:products",
  "store_id": 22,
  "store_name": "My Shop",
  "installation_id": 2
}

Store the access_token, refresh_token, store_id, and installation_id securely on your server.

Step 3: Use Access Token

Include the access token in the Authorization header for all API requests:

curl -X GET https://api.selorax.io/api/apps/v1/orders \
  -H "Authorization: Bearer sx_at_..."
  • Access token TTL: 24 hours
  • Caching: Tokens are cached in Redis for 5 minutes (prefix-based key) for faster validation

Step 4: Refresh Tokens

When an access token expires, use the refresh token to obtain a new token pair.

POST /api/apps/oauth/token

Request body:

{
  "grant_type": "refresh_token",
  "client_id": "sx_app_...",
  "client_secret": "sx_secret_...",
  "refresh_token": "sx_rt_..."
}

Response:

{
  "access_token": "sx_at_...",
  "refresh_token": "sx_rt_...",
  "token_type": "bearer",
  "expires_in": 86400,
  "scope": "read:orders write:products"
}

The refresh token response does not include store_id, store_name, or installation_id — only the initial token exchange returns those fields. Your app should have stored them during the original OAuth flow.

  • Refresh token TTL: 90 days
  • Both the old access token and old refresh token are invalidated
  • Store the new token pair immediately

:::tip Implement proactive token refresh before expiry (e.g. refresh when less than 1 hour remains) to avoid failed requests. :::

Revoke Tokens

Merchants can revoke app access from the dashboard, or apps can revoke their own tokens.

POST /api/apps/oauth/revoke

Requires [auth, admin] middleware (merchant must be logged in).

Request body:

{
  "installation_id": 2,
  "store_id": 22
}

This invalidates all access and refresh tokens for the installation.

Direct Install (First-Party Apps)

For first-party apps built by the SeloraX team, the OAuth redirect flow can be skipped entirely.

POST /api/apps/installations/direct-install

Requires [auth, admin] middleware.

Request body:

{
  "app_id": 1,
  "store_id": 22
}

What happens:

  1. Creates an installation record
  2. Generates access and refresh tokens directly (no auth code step)
  3. Delivers the tokens to the app's webhook_url via an HMAC-signed POST request
  4. Auto-creates webhook subscriptions for all topics the app declares

This is useful for platform-native integrations that don't need merchant-facing consent screens.

Token Storage Best Practices

How you store tokens determines your integration's security and reliability.

What to store

After the token exchange, persist these values per-store:

ValueWhere to StoreNotes
access_tokenEncrypted in databaseUsed for API calls. Rotate on refresh.
refresh_tokenEncrypted in databaseUsed to get new access tokens. Rotate on refresh.
store_idDatabaseIdentifies which merchant this token belongs to
installation_idDatabaseIdentifies the app installation
token_expires_atDatabaseCalculate from Date.now() + expires_in * 1000

Storage recommendations

  • Encrypt tokens at rest. Use AES-256 or your framework's encrypted column feature.
  • Never log tokens. Exclude token fields from application logs and error reports.
  • Never expose tokens to the frontend. Tokens belong on your server only.
  • Use environment variables for client_secret — never commit it to source control.

Proactive refresh pattern

Don't wait for a 401 to refresh. Check expiry before each API call:

async function getValidToken(storeId) {
  const installation = await db.getInstallation(storeId);
  const expiresAt = new Date(installation.token_expires_at);
  const oneHourFromNow = new Date(Date.now() + 60 * 60 * 1000);
 
  if (expiresAt > oneHourFromNow) {
    return installation.access_token; // Still valid
  }
 
  // Refresh proactively
  const response = await fetch("https://api.selorax.io/api/apps/oauth/token", {
    method: "POST",
    headers: { "Content-Type": "application/json" },
    body: JSON.stringify({
      grant_type: "refresh_token",
      client_id: process.env.SELORAX_CLIENT_ID,
      client_secret: process.env.SELORAX_CLIENT_SECRET,
      refresh_token: installation.refresh_token,
    }),
  });
 
  const tokens = await response.json();
  await db.updateInstallation(storeId, {
    access_token: tokens.access_token,
    refresh_token: tokens.refresh_token,
    token_expires_at: new Date(Date.now() + tokens.expires_in * 1000),
  });
 
  return tokens.access_token;
}

Token Reference

TypePrefixLengthStorage HashTTL
Access Tokensx_at_96 hexSHA25624 hours
Refresh Tokensx_rt_96 hexSHA25690 days
Auth Codesx_ac_64 hexSHA25660 seconds

:::note Scope delimiter For the /api/apps/oauth/authorize request, send scope as a comma-separated string (for example read:orders,write:products). :::

All tokens are generated using cryptographically secure random bytes. Only the SHA256 hash is stored in the database; the raw token is returned to the client once and cannot be retrieved again.

Error Codes

Error CodeDescription
invalid_grantAuth code is expired, already used, or does not exist
invalid_clientClient ID or client secret is incorrect
invalid_redirect_uriRedirect URI does not match any registered URL for this app

Error responses follow the standard format:

{
  "message": "Invalid authorization code",
  "status": 400,
  "error": "invalid_grant"
}