Seller Authorization
Last Updated: 2026-07-03
Overview
MallPlus uses an OAuth-style seller authorization flow when a partner app needs to act on behalf of a specific seller. This is the flow you use for seller-scoped routes such as orders, inventory, fulfillments, and other operational APIs.
The partner generates a signed authorization URL, the seller grants consent, MallPlus returns a one-time authorization code, and the partner exchanges that code for an access token and refresh token tied to that seller-app pair.
What this page covers:
- How to generate and exchange seller authorization credentials
- How access tokens and refresh tokens rotate over time
- How revocation works from both the partner and seller side
- When the seller must re-authorize your app
Authorization Flow
The seller authorization flow consists of 6 steps:
Step 1: Generate Signed Authorization URL
Your backend generates an HMAC-signed URL that the seller will visit. The signature proves the link came from your app and prevents tampering.
GET https://sandbox.open.mallplus.ph/open/v1/auth/authorize ?client_id=mp_your_client_id &redirect_uri=https://yourapp.com/callback ×tamp=1713254400 &sign=a1b2c3d4e5f6...
The sign parameter is an HMAC-SHA256 signature. See HMAC for Auth URL below.
redirect_uri must exactly match the redirect domain registered for your app. Subdomains are rejected. A mismatch fails with REDIRECT_URL_MISMATCH and the seller is never redirected — set your app's redirect domain before starting the flow. Step 2: Seller Visits URL and Consents
MallPlus validates the signature and shows a consent page with your app's name, category, and requested scopes. The seller signs in with their seller account and clicks Authorize to grant access.
error=access_denied query parameter — for example https://yourapp.com/callback?error=access_denied. Handle this as a normal user cancellation, not a failure. Step 3: Receive Authorization Code
After consent, MallPlus redirects back to your redirect_uri with a one-time authorization code and the seller ID:
https://yourapp.com/callback?code=abc123xyz&seller_id=seller_nike_ph
Step 4: Exchange Code for Tokens
Your backend exchanges the authorization code for an access token and refresh token. This request requires standard HMAC authentication headers.
POST /open/v1/auth/token
Headers:
X-MallPlus-Partner-Id: mp_your_client_id
X-MallPlus-Timestamp: 1713254400
X-MallPlus-Signature: <hmac_signature>
Content-Type: application/json
Body:
{
"code": "abc123xyz",
"client_id": "mp_your_client_id",
"seller_id": "seller_nike_ph"
}
Response:
{
"success": true,
"data": {
"access_token": "at_xxxxxxxxxxxx",
"refresh_token": "rt_xxxxxxxxxxxx",
"expires_in": 14400,
"seller_id": "seller_nike_ph",
"seller_name": "Nike Philippines",
"scopes": ["catalog:read", "orders:read"]
}
}Step 5: Make Seller-Scoped API Calls
For seller-scoped routes, send the normal HMAC headers plus the seller token headers. Calls without the seller headers return 401 SELLER_TOKEN_REQUIRED.
| Header | Description |
|---|---|
| X-MallPlus-Partner-Id | Your app's client ID |
| X-MallPlus-Timestamp | Current Unix timestamp in seconds |
| X-MallPlus-Signature | HMAC-SHA256 signature |
| X-MallPlus-Access-Token | Seller-scoped access token |
| X-MallPlus-Seller-Id | Seller ID paired with the access token |
Step 6: Refresh Tokens Before They Expire
Access tokens expire after 4 hours. Use the refresh token to request a new token pair. Refresh tokens are single-use: once a refresh succeeds, the old refresh token is invalid immediately.
POST /open/v1/auth/token/refresh
Headers:
X-MallPlus-Partner-Id: mp_your_client_id
X-MallPlus-Timestamp: 1713254400
X-MallPlus-Signature: <hmac_signature>
Content-Type: application/json
Body:
{
"refresh_token": "rt_xxxxxxxxxxxx",
"client_id": "mp_your_client_id",
"seller_id": "seller_nike_ph"
}
Response:
{
"success": true,
"data": {
"access_token": "at_new_token",
"refresh_token": "rt_new_token",
"expires_in": 14400
}
}REFRESH_TOKEN_REUSED and revokes the entire authorization chain for that seller-app pair. HMAC Signature for Auth URL
The authorization URL uses this signature base string:
{timestamp}:{clientId}:/open/v1/auth/authorize This is the same format as the standard API authentication signature, using the fixed path /open/v1/auth/authorize.
const baseString = `${timestamp}:${clientId}:/open/v1/auth/authorize`;
const sign = crypto.createHmac('sha256', clientSecret).update(baseString).digest('hex');HMAC for Token Calls
The token exchange, refresh, and revoke endpoints all use the standard HMAC base string format:
{timestamp}:{clientId}:{requestPath} For example, when calling POST /open/v1/auth/token/refresh:
const baseString = `${timestamp}:${clientId}:/open/v1/auth/token/refresh`;
const signature = crypto.createHmac('sha256', clientSecret).update(baseString).digest('hex');X-MallPlus-Access-Token / X-MallPlus-Seller-Id headers and are not part of the signature base string — the base string is identical to a public-mode request (MP-7894). Token Lifecycle
| Credential | Lifetime | Rotation / End | Notes |
|---|---|---|---|
| authorization_code | 10 minutes | Single-use | Expires quickly and is consumed immediately on token exchange |
| access_token | 4 hours | Replaced on refresh | Used on seller-scoped API calls |
| refresh_token | 30 days | Single-use rotation | Old token is invalid immediately after a successful refresh |
| authorization grant | 365 days | Seller must re-authorize | Refresh cannot extend past the overall 365-day authorization wall |
Manage Authorizations
Partners can review connected sellers from the app detail area in the partner console. Open your app and go to the Authorized Sellers page to inspect current authorizations.
The authorization list shows:
- Shop Name and Shop ID for the connected seller
- Scopes granted to your app for that seller
- Authorized Date so you know when access was first granted
- Expiry Date — the end of the 365-day authorization window
- Status — Active, Expiring Soon (within 7 days of the authorization expiry), or Expired
Revocation
Authorizations can be revoked from either side. Once revoked, the seller-app pair stops working immediately and subsequent seller-scoped API calls fail until the seller completes the authorization flow again.
Partner-side revocation
Partners can revoke access from the console or programmatically with POST /open/v1/auth/revoke. This endpoint requires HMAC headers because only the owning partner can revoke its own authorizations.
POST /open/v1/auth/revoke
Headers:
X-MallPlus-Partner-Id: mp_your_client_id
X-MallPlus-Timestamp: 1713254400
X-MallPlus-Signature: <hmac_signature>
Content-Type: application/json
Body:
{
"client_id": "mp_your_client_id",
"seller_id": "seller_nike_ph"
}Seller-side revocation
Sellers manage your app from Shop Settings → Partner Management in their MallPlus seller center. The page lists every app they have authorized across three tabs — Using (active), Expired (past the 365-day window), and Unlink (revoked) — and lets the shop owner unlink an app after a confirmation step.
When a seller unlinks your app, both the access token and the refresh token for that seller-app pair stop working immediately, and the seller must grant consent again before you can continue calling seller-scoped APIs.
Endpoints Reference
All token and authorization failures should use the standardized error envelope below so partner systems can handle refresh, expiry, and revocation failures consistently.
{
"success": false,
"error": {
"code": "ERROR_CODE",
"message": "Human-readable message"
}
}| Method | Endpoint | Auth | Description |
|---|---|---|---|
| GET | /open/v1/auth/authorize | Query params (HMAC signed) | Initiate seller authorization |
| POST | /open/v1/auth/seller-verify | Consent form | Validate seller login and create auth code |
| POST | /open/v1/auth/deny | Consent form | Seller declines consent; returns the validated redirect target |
| POST | /open/v1/auth/token | HMAC headers | Exchange auth code for access + refresh tokens |
| POST | /open/v1/auth/token/refresh | HMAC headers | Rotate the refresh token for a fresh token pair |
| POST | /open/v1/auth/revoke | HMAC headers | Revoke a seller authorization for your app |
Full Flow Example (Node.js)
import crypto from 'crypto';
const CLIENT_ID = 'mp_your_client_id';
const CLIENT_SECRET = 'sk_live_your_secret';
const REDIRECT_URI = 'https://yourapp.com/callback';
function sign(path, timestamp) {
const baseString = `${timestamp}:${CLIENT_ID}:${path}`;
return crypto.createHmac('sha256', CLIENT_SECRET).update(baseString).digest('hex');
}
function generateAuthUrl() {
const timestamp = Math.floor(Date.now() / 1000);
const baseString = `${timestamp}:${CLIENT_ID}:/open/v1/auth/authorize`;
const signValue = crypto.createHmac('sha256', CLIENT_SECRET).update(baseString).digest('hex');
const url = new URL('https://sandbox.open.mallplus.ph/open/v1/auth/authorize');
url.searchParams.set('client_id', CLIENT_ID);
url.searchParams.set('redirect_uri', REDIRECT_URI);
url.searchParams.set('timestamp', String(timestamp));
url.searchParams.set('sign', signValue);
return url.toString();
}
async function exchangeCode(code, sellerId) {
const timestamp = String(Math.floor(Date.now() / 1000));
const path = '/open/v1/auth/token';
const res = await fetch(`https://sandbox.open.mallplus.ph${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-MallPlus-Partner-Id': CLIENT_ID,
'X-MallPlus-Timestamp': timestamp,
'X-MallPlus-Signature': sign(path, timestamp),
},
body: JSON.stringify({
code,
client_id: CLIENT_ID,
seller_id: sellerId,
}),
});
return res.json();
}
async function refreshTokens(refreshToken, sellerId) {
const timestamp = String(Math.floor(Date.now() / 1000));
const path = '/open/v1/auth/token/refresh';
const res = await fetch(`https://sandbox.open.mallplus.ph${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-MallPlus-Partner-Id': CLIENT_ID,
'X-MallPlus-Timestamp': timestamp,
'X-MallPlus-Signature': sign(path, timestamp),
},
body: JSON.stringify({
refresh_token: refreshToken,
client_id: CLIENT_ID,
seller_id: sellerId,
}),
});
return res.json();
}
async function revokeAuthorization(sellerId) {
const timestamp = String(Math.floor(Date.now() / 1000));
const path = '/open/v1/auth/revoke';
const res = await fetch(`https://sandbox.open.mallplus.ph${path}`, {
method: 'POST',
headers: {
'Content-Type': 'application/json',
'X-MallPlus-Partner-Id': CLIENT_ID,
'X-MallPlus-Timestamp': timestamp,
'X-MallPlus-Signature': sign(path, timestamp),
},
body: JSON.stringify({
client_id: CLIENT_ID,
seller_id: sellerId,
}),
});
return res.json();
}Error Handling
| Error Code | HTTP | Cause | What to do |
|---|---|---|---|
| BAD_REQUEST | 400 | Authorization code is malformed or does not match the authenticated partner request | Restart the seller consent flow and exchange the new code immediately |
| AUTH_CODE_USED | 401 | The authorization code has already been exchanged once | Codes are single-use — restart the seller consent flow to get a fresh code |
| AUTH_CODE_EXPIRED | 401 | The authorization code is past its 10-minute window | Restart the seller consent flow and exchange the new code immediately |
| REDIRECT_URL_MISMATCH | 400 | The redirect_uri domain does not match the redirect domain registered for the app | Use a redirect URI on the exact registered domain; update the app's redirect domain if it changed |
| UNAUTHORIZED | 401 | Refresh token is invalid for the authenticated app, expired, rotated out, or no longer usable | If you no longer have a valid token pair, ask the seller to re-authorize |
| REFRESH_TOKEN_REUSED | 401 | A previously-rotated refresh token was presented again | Stop retrying. Treat this as a hard revoke and drive the seller through consent again |
| RE_AUTHORIZATION_REQUIRED | 401 | The 365-day seller authorization validity has expired | Ask the seller to grant consent again; refresh cannot extend past this wall |
| SELLER_TOKEN_REQUIRED | 401 | A seller-scoped route was called without seller token headers | Send X-MallPlus-Access-Token and X-MallPlus-Seller-Id with the HMAC headers |
| TIMESTAMP_EXPIRED | 401 | The HMAC timestamp is outside the allowed window | Sync your server clock and regenerate the request |