Authentication
Last Updated: 2026-04-16
Overview
MallPlus Open Platform uses HMAC-SHA256 signature-based authentication for all API requests. Unlike token-based auth (OAuth, JWT), every request must be independently signed using your app's client secret. This ensures that requests cannot be replayed or tampered with in transit.
Required Headers
Every API request must include the following three headers:
| Header | Type | Description |
|---|---|---|
| X-MallPlus-Partner-Id | String | Your app's client ID (found in the App Details page in the console) |
| X-MallPlus-Timestamp | Integer | Current Unix timestamp in seconds (e.g., 1713254400) |
| X-MallPlus-Signature | String | HMAC-SHA256 hex digest of the signature base string |
Signature Generation
Follow these steps to generate a valid request signature:
Get the current Unix timestamp (seconds)
Use your language's standard time library. The timestamp must be in seconds, not milliseconds.
Construct the signature base string
Five fields joined with colons:
{timestamp}:{clientId}:{METHOD}:{requestPath}:{sha256(body)} Example (GET, empty body): 1713254400:app_ck9x2mf3w0001:GET:/open/v1/products:e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855
METHODis the uppercase HTTP verb (GET,POST, …).requestPathis the path only (e.g./open/v1/products) — leading slash, no query parameters, and no URL-encoding. Send the path exactly as it appears in the endpoint reference.sha256(body)is the hex SHA-256 of the raw request body; for empty bodies use SHA-256 of"".- For seller-scoped endpoints, send the seller token as the
X-MallPlus-Access-Token/X-MallPlus-Seller-Idheaders — they are not part of the base string.
Compute the HMAC-SHA256 hash
Use your app's client secret as the HMAC key and the base string as the message.
Hex-encode the result
Convert the binary HMAC output to a lowercase hexadecimal string and send it as the X-MallPlus-Signature header.
Public vs Shop Mode
The Open Platform has two signing contexts, and both use the identical base string — {timestamp}:{clientId}:{METHOD}:{requestPath}:{sha256(body)}:
- Public mode — calls that act on your app's own resources. Only the three signing headers (
X-MallPlus-Partner-Id,X-MallPlus-Timestamp,X-MallPlus-Signature) are sent. - Shop mode — seller-scoped calls made on behalf of a connected seller. You additionally send the seller's
access_tokenandshop_idas theX-MallPlus-Access-Token/X-MallPlus-Seller-Idheaders.
access_token and shop_id are transmitted as HTTP headers and are never part of the base string. A Shop-mode signature is computed exactly like a Public-mode signature for the same method, path, and body. Signature Format
The X-MallPlus-Signature header must be the HMAC-SHA256 digest encoded as a 64-character lowercase hexadecimal string. Other encodings are rejected with INVALID_SIGNATURE.
| Format | Example | Accepted? |
|---|---|---|
| Lowercase hex (64 chars) | e8864c9a…5b855 | Accepted |
| Uppercase hex | E8864C9A…5B855 | Rejected |
| Base64 | 6IZMmq…W4VQ== | Rejected |
Most standard crypto libraries emit lowercase hex by default (Node's .digest('hex'), Python's .hexdigest(), PHP's hash_hmac()). If you build the hex string yourself, do not upper-case it.
Code Examples
Node.js
import crypto from 'crypto';
const clientId = 'app_ck9x2mf3w0001';
const clientSecret = 'sk_live_abc123...';
const method = 'GET';
const requestPath = '/open/v1/products';
const body = ''; // raw request body string; '' for GET
const timestamp = Math.floor(Date.now() / 1000);
const bodyHash = crypto.createHash('sha256').update(body).digest('hex');
const baseString = `${timestamp}:${clientId}:${method}:${requestPath}:${bodyHash}`;
const signature = crypto
.createHmac('sha256', clientSecret)
.update(baseString)
.digest('hex');
const response = await fetch('https://sandbox.open.mallplus.ph/open/v1/products', {
headers: {
'X-MallPlus-Partner-Id': clientId,
'X-MallPlus-Timestamp': String(timestamp),
'X-MallPlus-Signature': signature,
},
});Python
import hmac
import hashlib
import time
import requests
client_id = "app_ck9x2mf3w0001"
client_secret = "sk_live_abc123..."
method = "GET"
request_path = "/open/v1/products"
body = "" # raw request body string; "" for GET
timestamp = str(int(time.time()))
body_hash = hashlib.sha256(body.encode()).hexdigest()
base_string = f"{timestamp}:{client_id}:{method}:{request_path}:{body_hash}"
signature = hmac.new(
client_secret.encode(),
base_string.encode(),
hashlib.sha256
).hexdigest()
response = requests.get("https://sandbox.open.mallplus.ph/open/v1/products", headers={
"X-MallPlus-Partner-Id": client_id,
"X-MallPlus-Timestamp": timestamp,
"X-MallPlus-Signature": signature,
})PHP
<?php
$clientId = 'app_ck9x2mf3w0001';
$clientSecret = 'sk_live_abc123...';
$method = 'GET';
$requestPath = '/open/v1/products';
$body = ''; // raw request body string; '' for GET
$timestamp = time();
$bodyHash = hash('sha256', $body);
$baseString = "{$timestamp}:{$clientId}:{$method}:{$requestPath}:{$bodyHash}";
$signature = hash_hmac('sha256', $baseString, $clientSecret);
$ch = curl_init('https://sandbox.open.mallplus.ph/open/v1/products');
curl_setopt($ch, CURLOPT_HTTPHEADER, [
"X-MallPlus-Partner-Id: {$clientId}",
"X-MallPlus-Timestamp: {$timestamp}",
"X-MallPlus-Signature: {$signature}",
]);
curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);
$response = curl_exec($ch);
curl_close($ch);Java
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import java.security.MessageDigest;
import java.net.http.*;
import java.net.URI;
String clientId = "app_ck9x2mf3w0001";
String clientSecret = "sk_live_abc123...";
String method = "GET";
String requestPath = "/open/v1/products";
String body = ""; // raw request body string; "" for GET
long timestamp = System.currentTimeMillis() / 1000;
MessageDigest sha = MessageDigest.getInstance("SHA-256");
String bodyHash = bytesToHex(sha.digest(body.getBytes()));
String baseString = timestamp + ":" + clientId + ":" + method + ":" + requestPath + ":" + bodyHash;
Mac mac = Mac.getInstance("HmacSHA256");
mac.init(new SecretKeySpec(clientSecret.getBytes(), "HmacSHA256"));
byte[] hash = mac.doFinal(baseString.getBytes());
String signature = bytesToHex(hash);
HttpRequest request = HttpRequest.newBuilder()
.uri(URI.create("https://sandbox.open.mallplus.ph/open/v1/products"))
.header("X-MallPlus-Partner-Id", clientId)
.header("X-MallPlus-Timestamp", String.valueOf(timestamp))
.header("X-MallPlus-Signature", signature)
.build();Timestamp Validation
The X-MallPlus-Timestamp header must be a Unix epoch integer in seconds (e.g. 1716451200), not milliseconds. The MallPlus API server validates that it is within 90 seconds of the server's current time.
Requests with timestamps outside this window are rejected with a TIMESTAMP_EXPIRED error. This prevents replay attacks where a captured request is resent at a later time.
Seller-Scoped API Calls
For seller-scoped API calls where a partner app acts on behalf of a specific seller, MallPlus uses an OAuth-style authorization flow. The seller grants your app access through a consent page, and your app receives scoped access tokens tied to that seller.
Common Errors
| Error Code | HTTP Status | Description | Solution |
|---|---|---|---|
| INVALID_SIGNATURE | 401 | The computed signature does not match | Verify your base string format and client secret. Check for extra whitespace or encoding issues. |
| TIMESTAMP_EXPIRED | 401 | Timestamp is more than 90 seconds from server time | Sync your server clock via NTP. Ensure you're sending seconds, not milliseconds. |
| INACTIVE_APP | 403 | The app associated with the client ID is not active | Check the app status in the console. Apps must be approved and active to make API calls. |
| MISSING_HEADER | 400 | One or more required authentication headers are missing | Include all three required headers in every request. |
| SCOPE_DENIED | 403 | The app does not have the required scope for this endpoint | Request the necessary scope in the App Management page, then wait for approval. |