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
| Parameter | Required | Description |
|---|---|---|
client_id | Yes | Your app's client ID (format: sx_app_...) |
redirect_uri | Yes | Must match one of the URLs in your app's redirect_urls array |
scope | Yes | Comma-separated list of requested scopes (e.g. read:orders,write:products) |
state | Yes | Random string to prevent CSRF attacks. Verify this on callback. |
store_id | Yes | The store the merchant is authorizing access to |
What the platform does:
- Validates the merchant is logged in (middleware:
[auth, admin]) - Checks that
client_idexists and the app is active - Confirms
redirect_uriis in the app's registeredredirect_urlsarray - Generates an authorization code:
sx_ac_<64 hex characters>, SHA256-hashed before storage, with a 60-second TTL - Redirects to:
{redirect_uri}?code=sx_ac_...&state={state}(note:store_idis 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:
- Creates an installation record
- Generates access and refresh tokens directly (no auth code step)
- Delivers the tokens to the app's
webhook_urlvia an HMAC-signed POST request - 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:
| Value | Where to Store | Notes |
|---|---|---|
access_token | Encrypted in database | Used for API calls. Rotate on refresh. |
refresh_token | Encrypted in database | Used to get new access tokens. Rotate on refresh. |
store_id | Database | Identifies which merchant this token belongs to |
installation_id | Database | Identifies the app installation |
token_expires_at | Database | Calculate 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
| Type | Prefix | Length | Storage Hash | TTL |
|---|---|---|---|---|
| Access Token | sx_at_ | 96 hex | SHA256 | 24 hours |
| Refresh Token | sx_rt_ | 96 hex | SHA256 | 90 days |
| Auth Code | sx_ac_ | 64 hex | SHA256 | 60 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 Code | Description |
|---|---|
invalid_grant | Auth code is expired, already used, or does not exist |
invalid_client | Client ID or client secret is incorrect |
invalid_redirect_uri | Redirect 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"
}