Authorization Introduction
Scope
This document describes the authorization flow for merchants integrating with the Pexx service B2B APIs. It only covers authorization-related content and does not include detailed business API specifications.
Basic Integration Information
API Base URLs
| Environment | Base URL |
|---|---|
| Sandbox | http://qa.backend.pexx.com:8606 |
| Production | https://saas.backend.pexx.com |
Merchant Onboarding Materials
Before integration, key exchange and merchant configuration must be completed.
-
The merchant generates its own RSA key pair.
-
The merchant provides the RSA public key to the
Pexx servicefor storage. -
The RSA private key must be securely stored by the merchant. The
Pexx servicedoes not store the merchant's private key. -
After merchant configuration is completed, the
Pexx serviceprovides the following information:merchantCode: merchant identifierPexxApiKey: merchant API key
A 2048-bit RSA key is recommended.
Example: Generate RSA Key Pair
Use OpenSSL to generate the RSA key pair:
openssl genrsa -out merchant_private_key.pem 2048
openssl pkcs8 -topk8 -inform PEM -outform PEM -in merchant_private_key.pem -out merchant_private_key_pkcs8.pem -nocrypt
openssl rsa -in merchant_private_key.pem -pubout -out merchant_public_key.pemNotes:
merchant_private_key_pkcs8.pemis the recommended merchant private key filemerchant_public_key.pemis the merchant public key file to be provided to thePexx service- If the
Pexx servicerequires the public key in Base64 format, remove the PEM header, footer, and line breaks before submission
Glossary
| Term | Description |
|---|---|
merchantCode | The unique merchant identifier assigned by the Pexx service |
PexxApiKey | The API key assigned by the Pexx service and used to identify the merchant |
accessToken | The access token used for business API calls and passed in PexxAuthorization |
refreshToken | The token used to refresh access credentials |
secretKey | The signing key used to generate HMAC signatures for business API requests |
grantType | The credential application type; for the current integration, client_credentials is recommended |
businessId | Optional business user identifier; if omitted, the merchant's primary business user will be used by default |
X-TIMESTAMP | Unix timestamp in seconds; should use the current time when the request is sent |
X-NONCE | A unique random string for each request to prevent replay attacks; recommended maximum length is 32 characters, and a UUID without hyphens is recommended |
X-SIGNATURE | The request signature value, generated according to the signing rules in this document and then Base64-encoded |
path | The actual request path, for example /apis/v1/access-token |
Authorization Flow
1. Request Access Credentials
Call:
POST /apis/v1/access-token
Purpose:
- Request business access credentials
- Obtain the
accessToken,refreshToken, andsecretKeyrequired for subsequent business API calls
Optional: Follow the cURL walkthrough in Recipes
Open the Request an access token with cURL Recipe to see the request body, required headers, cURL command, and example success and failure responses side by side.
Use this page for the signing rules and parameter definitions, then use the Recipe when you want a request flow you can copy and adapt.
2. Call Business APIs
Applicable range:
GET /apis/v1/**POST /apis/v1/**
Purpose:
- Use
accessTokento access business APIs - Use
secretKeyto generate the request signature for business APIs
3. Refresh Access Credentials
Call:
POST /apis/v1/refresh-token
Purpose:
- Request a new set of access credentials after
accessTokenexpires by usingrefreshToken
API Integration Details
Header Requirements
Common Headers
| Header | Required | Description |
|---|---|---|
Content-Type | Yes | Must be application/json |
PexxApiKey | Yes | Merchant API key |
X-TIMESTAMP | Yes | Unix timestamp in seconds |
X-NONCE | Yes | Unique random string for each request; recommended maximum length is 32 characters |
X-SIGNATURE | Yes | Request signature, Base64-encoded |
Additional Header for Business APIs
| Header | Required | Description |
|---|---|---|
PexxAuthorization | Yes | Format: Bearer {accessToken} |
Signing Rules
1. Signature for access-token / refresh-token Requests
access-token / refresh-token RequestsApplicable APIs:
POST /apis/v1/access-tokenPOST /apis/v1/refresh-token
Signature algorithm:
SHA256withRSA
String to sign:
METHOD:path:sha256(minifyJson(body)):apiKey:merchantCode:timestamp:nonceExample:
POST:/apis/v1/access-token:sha256(body):your-api-key:your-merchant-code:1714291200:6f2e7c1a4d9b4c2f9c7d1e3a5b6f8a0cNotes:
METHODmust be uppercase, for examplePOSTpathmust use the actual request path, for example/apis/v1/access-tokenbodymust be serialized in a stable format before hashing- The signature result must be Base64-encoded and passed in
X-SIGNATURE
package com.pexx.core.utils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import org.apache.commons.codec.digest.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
/**
* @ClassName GetAccessTokenTest
* @Author yichun.gao
* @Description
* @Date 2026/5/29 16:29
* @Version v1.0
**/
public class GetAccessTokenTest {
private static final String DEFAULT_BASE_URL = "http://qa.backend.pexx.com:8606";
private static final String DEFAULT_ACCESS_TOKEN_PATH = "/pexx/business/auth/access-token";
public static void main(String[] args) throws Exception {
// configuration (please modify according to actual situation)
String merchantCode = "7244dac7-fcf9-4d0f-8746-d504862344a1";
String apiKey = "nHbJ9ASE4Olw7AEkuc-8BWK9Ejn47Crcq2rGHfNyeuc";
String privatekey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8UUYBlzlJR33eM+D8JIJvhybVsho3tQNJF/WyhLmqBgn7ylEQUtGzlheXqGL+SjlATC8hg12bMdiQbaG/DQV6fRUwGniUxFrBSyTpZ7a0z66wA2nKU0iT0E9OgmXxseEN3dbzcjdXbeplHwQqMH11u3Vs79FjgRyHX4Fo4e1V7/ot0rzWxyB4a5cJ7PaBqXl33oVFJsmDmbwaR7teZ8iQRfTbVtWXPLy2/+vJq20KrYbRqFnxwmsQnN6CBTIPgaZB80QvJn7j/qdqu1mQQUDqJUjcakTyiJ2X/kkq4Tglr702S65Bbhebmxgs/8gcUAcM2SKew4e09UTuthAsZT6nAgMBAAECggEAfCFacuv6f9oXFqO9tpZeQCOnLo8ihvvTOZgIhW7Fb1Rhuk3m10qwHZ9e18HP1uyYBlDxdDbCOe1GYhVR27w6kz3l/HpGZ1FyvRzKLOwHW/HVpQHq9smk+oIB9K8xgXqN7XUAHiJ4ZjH2okcqmKCz4in5wh/mNp/BbV4/0CG0LsNJ/Z7Emhrsxg/7EI1k7R1RNLz5+a3NZgzaIHxSMxNUihcnl5dakaZHJHOjJ/klsHqfax+lgeVvCSntZAIG0stRwdd1HMW0VZChnIB64IYOxj04AY2owpKBhwDKX5epal9bSV14GRQQvdV1DnfOjMxrD1Heino+iGcL07BUGpMsQQKBgQDditouEncpfhE231l/OLga8mBxHSYSDvW2sZXAa5f/ch1ROvbh9UPZqIM810JotYhnCbOzaYM5h+5J/dnqKu6EyJVFovWgx//CrVu37mErVbjsKAFz96SNIfS4ylfNf1Gikb2Ml8aiOzYp8lo3leKEHhTC87oZMgaUWFzb5L/0oQKBgQDZm4FpsbOPE4vSbG+Ce2/4w8MyNBTY1QPJBrwZEXzdT7PTlOh/xCwkh4KqRIOXZV/pStytBJ4TerPc502/cL44yUjnW4n7rcbrucyyxT4R5FqkSXg9cYLk4LdqJtcmEsGzExt7rqqnWgRCZbxnSz0LA7FDUlrF1AJkgQ+lLYimRwKBgEp9qabcJp0Y+ojMyLbyR1UoMi1Wc7qWtR/czlGI2+7UW+84OFL5uPqyoo4OgxHaGCctJ/MngywQ/Jp8dI08Kj8Tgr2LcbPCC8lVqQVLbfi4NhmRygtINVgPFs4bmzJJoRVck7N2RR+/cRLhnlwaVbO+uZRjhyt5mqS+oVp+q9yBAoGABhst+3hIEJi80K/IRUIPd0yO+qapexgnHgn5Vz69YTxuUF6aU5N+pZvD1+FKTAJFObenD5fUk7lauLUo4llYjSFg0VUpPw22SkERdGbCgiAFRxzkqdy4jpGbs/fZC7F1DABaQhM5qK6G9hICwmdDFD8LR1dVQr3bP1S7yqfHcNsCgYEA0cZIBiNGRSpncvuppe8KSZP12AbUNgclxoowm8XDPZj2I5Ej2JUslWWKNneFatPyTJMZ6tQh/0z+OL5s726eYtlET5Lh8tY5fQwGyjrb0hfvLtVokDLf8ty7R0cHz7IfKahwiROInFsrbyNp95UjcBrLycPBxeaZGp7S+ahDcT0=";
// Build call request body
String body = buildAccessTokenBody(merchantCode);
// current time
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString().replace("-", "");
// Generate fields to be signed
String stringToSign = buildAccessTokenSignString(
"POST",
DEFAULT_ACCESS_TOKEN_PATH,
body,
apiKey,
merchantCode,
timestamp,
nonce
);
// generate signature
String signature = signRsa(privatekey, stringToSign);
Map<String, String> headers = new LinkedHashMap<String, String>();
headers.put("Content-Type", "application/json");
headers.put("PexxApiKey", privatekey);
headers.put("X-TIMESTAMP", timestamp);
headers.put("X-NONCE", nonce);
headers.put("X-SIGNATURE", signature);
SignedRequest signedRequest = new SignedRequest("POST", DEFAULT_ACCESS_TOKEN_PATH, headers, body);
System.out.println("=== ACCESS TOKEN REQUEST ===");
System.out.println(toCurl(DEFAULT_BASE_URL, signedRequest));
}
private static String buildAccessTokenBody(String merchantCode) {
Map<String, Object> body = new LinkedHashMap<String, Object>();
// body.put("businessId", firstNonBlank(config.getBusinessUserId(), DEFAULT_BUSINESS_USER_ID));
body.put("grantType", "client_credentials");
body.put("merchantCode", merchantCode);
return JSON.toJSONString(body);
}
public static String buildAccessTokenSignString(String method, String path, String body, String apiKey, String merchantCode,
String timestamp, String nonce) {
return String.format("%s:%s:%s:%s:%s:%s",
method.toUpperCase(),
path,
sha256Lower(minifyJson(body)),
apiKey,
merchantCode,
timestamp + ":" + nonce);
}
/**
* rsa sign
* @param privateKeyBase64
* @param stringToSign
* @return
* @throws Exception
*/
private static String signRsa(String privateKeyBase64, String stringToSign) throws Exception {
byte[] decoded = Base64.getDecoder().decode(privateKeyBase64);
PrivateKey privateKey = buildPrivateKey(decoded);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(stringToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
/**
* minify json
* @param body
* @return
*/
public static String minifyJson(String body) {
if (body == null || body.trim().isEmpty()) {
return "";
}
try {
Object parsed = JSON.parse(body);
if (!(parsed instanceof JSONObject)) {
return JSON.toJSONString(parsed);
}
JSONObject jsonObject = (JSONObject) parsed;
JSONObject sorted = new JSONObject();
jsonObject.keySet().stream().sorted().forEach(key -> sorted.put(key, jsonObject.get(key)));
return JSON.toJSONString(sorted, JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.WriteBigDecimalAsPlain);
} catch (Exception e) {
return body.trim();
}
}
public static String sha256Lower(String input) {
return DigestUtils.sha256Hex(input == null ? "" : input).toLowerCase();
}
private static PrivateKey buildPrivateKey(byte[] keyBytes) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
try {
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (Exception ex) {
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(wrapPkcs1ToPkcs8(keyBytes)));
}
}
private static byte[] wrapPkcs1ToPkcs8(byte[] pkcs1) {
byte[] version = new byte[]{0x02, 0x01, 0x00};
byte[] algId = new byte[]{0x30, 0x0d, 0x06, 0x09, 0x2a, (byte) 0x86, 0x48, (byte) 0x86,
(byte) 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00};
byte[] octetString = concat(new byte[]{0x04}, derLength(pkcs1.length), pkcs1);
byte[] sequence = concat(version, algId, octetString);
return concat(new byte[]{0x30}, derLength(sequence.length), sequence);
}
private static String toCurl(String baseUrl, SignedRequest request) {
StringBuilder builder = new StringBuilder();
builder.append("curl --location '")
.append(baseUrl)
.append(request.path)
.append("' \\\n");
builder.append(" --request ").append(request.method).append(" \\\n");
for (Map.Entry<String, String> entry : request.headers.entrySet()) {
builder.append(" --header '")
.append(entry.getKey())
.append(": ")
.append(entry.getValue())
.append("' \\\n");
}
builder.append(" --data-raw '").append(request.body).append("'");
return builder.toString();
}
private static byte[] concat(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
totalLength += array.length;
}
byte[] result = new byte[totalLength];
int offset = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
private static byte[] derLength(int length) {
if (length < 128) {
return new byte[]{(byte) length};
}
int temp = length;
int count = 0;
while (temp > 0) {
count++;
temp >>= 8;
}
byte[] result = new byte[1 + count];
result[0] = (byte) (0x80 | count);
for (int i = count; i > 0; i--) {
result[i] = (byte) (length & 0xFF);
length >>= 8;
}
return result;
}
private static class SignedRequest {
private final String method;
private final String path;
private final Map<String, String> headers;
private final String body;
private SignedRequest(String method, String path, Map<String, String> headers, String body) {
this.method = method;
this.path = path;
this.headers = headers;
this.body = body;
}
}
}
package com.pexx.core.utils;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONObject;
import com.alibaba.fastjson2.JSONWriter;
import org.apache.commons.codec.digest.DigestUtils;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.time.Instant;
import java.util.Base64;
import java.util.LinkedHashMap;
import java.util.Map;
import java.util.UUID;
/**
* @ClassName GetAccessTokenTest
* @Author yichun.gao
* @Description
* @Date 2026/5/29 16:29
* @Version v1.0
**/
public class RefreshTokenTest {
private static final String DEFAULT_BASE_URL = "http://qa.backend.pexx.com:8606";
private static final String DEFAULT_REFRESH_TOKEN_PATH = "/pexx/business/auth/refresh-token";
public static void main(String[] args) throws Exception {
// configuration (please modify according to actual situation)
String merchantCode = "7244dac7-fcf9-4d0f-8746-d504862344a1";
String apiKey = "nHbJ9ASE4Olw7AEkuc-8BWK9Ejn47Crcq2rGHfNyeuc";
String privatekey = "MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC8UUYBlzlJR33eM+D8JIJvhybVsho3tQNJF/WyhLmqBgn7ylEQUtGzlheXqGL+SjlATC8hg12bMdiQbaG/DQV6fRUwGniUxFrBSyTpZ7a0z66wA2nKU0iT0E9OgmXxseEN3dbzcjdXbeplHwQqMH11u3Vs79FjgRyHX4Fo4e1V7/ot0rzWxyB4a5cJ7PaBqXl33oVFJsmDmbwaR7teZ8iQRfTbVtWXPLy2/+vJq20KrYbRqFnxwmsQnN6CBTIPgaZB80QvJn7j/qdqu1mQQUDqJUjcakTyiJ2X/kkq4Tglr702S65Bbhebmxgs/8gcUAcM2SKew4e09UTuthAsZT6nAgMBAAECggEAfCFacuv6f9oXFqO9tpZeQCOnLo8ihvvTOZgIhW7Fb1Rhuk3m10qwHZ9e18HP1uyYBlDxdDbCOe1GYhVR27w6kz3l/HpGZ1FyvRzKLOwHW/HVpQHq9smk+oIB9K8xgXqN7XUAHiJ4ZjH2okcqmKCz4in5wh/mNp/BbV4/0CG0LsNJ/Z7Emhrsxg/7EI1k7R1RNLz5+a3NZgzaIHxSMxNUihcnl5dakaZHJHOjJ/klsHqfax+lgeVvCSntZAIG0stRwdd1HMW0VZChnIB64IYOxj04AY2owpKBhwDKX5epal9bSV14GRQQvdV1DnfOjMxrD1Heino+iGcL07BUGpMsQQKBgQDditouEncpfhE231l/OLga8mBxHSYSDvW2sZXAa5f/ch1ROvbh9UPZqIM810JotYhnCbOzaYM5h+5J/dnqKu6EyJVFovWgx//CrVu37mErVbjsKAFz96SNIfS4ylfNf1Gikb2Ml8aiOzYp8lo3leKEHhTC87oZMgaUWFzb5L/0oQKBgQDZm4FpsbOPE4vSbG+Ce2/4w8MyNBTY1QPJBrwZEXzdT7PTlOh/xCwkh4KqRIOXZV/pStytBJ4TerPc502/cL44yUjnW4n7rcbrucyyxT4R5FqkSXg9cYLk4LdqJtcmEsGzExt7rqqnWgRCZbxnSz0LA7FDUlrF1AJkgQ+lLYimRwKBgEp9qabcJp0Y+ojMyLbyR1UoMi1Wc7qWtR/czlGI2+7UW+84OFL5uPqyoo4OgxHaGCctJ/MngywQ/Jp8dI08Kj8Tgr2LcbPCC8lVqQVLbfi4NhmRygtINVgPFs4bmzJJoRVck7N2RR+/cRLhnlwaVbO+uZRjhyt5mqS+oVp+q9yBAoGABhst+3hIEJi80K/IRUIPd0yO+qapexgnHgn5Vz69YTxuUF6aU5N+pZvD1+FKTAJFObenD5fUk7lauLUo4llYjSFg0VUpPw22SkERdGbCgiAFRxzkqdy4jpGbs/fZC7F1DABaQhM5qK6G9hICwmdDFD8LR1dVQr3bP1S7yqfHcNsCgYEA0cZIBiNGRSpncvuppe8KSZP12AbUNgclxoowm8XDPZj2I5Ej2JUslWWKNneFatPyTJMZ6tQh/0z+OL5s726eYtlET5Lh8tY5fQwGyjrb0hfvLtVokDLf8ty7R0cHz7IfKahwiROInFsrbyNp95UjcBrLycPBxeaZGp7S+ahDcT0=";
String refreshToken = "0c56af280e0c47d8bc3a5a4b573ef4ee3ef7627eb7bd4a39b641c7b72a623a57";
// Build call request body
String body = buildRefreshTokenBody(merchantCode,refreshToken);
// current time
String timestamp = String.valueOf(Instant.now().getEpochSecond());
String nonce = UUID.randomUUID().toString().replace("-", "");
// Generate fields to be signed
String stringToSign = buildAccessTokenSignString(
"POST",
DEFAULT_REFRESH_TOKEN_PATH,
body,
apiKey,
merchantCode,
timestamp,
nonce
);
// generate signature
String signature = signRsa(privatekey, stringToSign);
Map<String, String> headers = new LinkedHashMap<String, String>();
headers.put("Content-Type", "application/json");
headers.put("PexxApiKey", privatekey);
headers.put("X-TIMESTAMP", timestamp);
headers.put("X-NONCE", nonce);
headers.put("X-SIGNATURE", signature);
SignedRequest signedRequest = new SignedRequest("POST", DEFAULT_REFRESH_TOKEN_PATH, headers, body);
System.out.println("=== REFRESH TOKEN REQUEST ===");
System.out.println(toCurl(DEFAULT_BASE_URL, signedRequest));
}
private static String buildRefreshTokenBody(String merchantCode,String refreshToken) {
Map<String, Object> body = new LinkedHashMap<String, Object>();
body.put("merchantCode", merchantCode);
body.put("refreshToken", refreshToken);
return JSON.toJSONString(body);
}
public static String buildAccessTokenSignString(String method, String path, String body, String apiKey, String merchantCode,
String timestamp, String nonce) {
return String.format("%s:%s:%s:%s:%s:%s",
method.toUpperCase(),
path,
sha256Lower(minifyJson(body)),
apiKey,
merchantCode,
timestamp + ":" + nonce);
}
/**
* rsa sign
* @param privateKeyBase64
* @param stringToSign
* @return
* @throws Exception
*/
private static String signRsa(String privateKeyBase64, String stringToSign) throws Exception {
byte[] decoded = Base64.getDecoder().decode(privateKeyBase64);
PrivateKey privateKey = buildPrivateKey(decoded);
Signature signature = Signature.getInstance("SHA256withRSA");
signature.initSign(privateKey);
signature.update(stringToSign.getBytes(StandardCharsets.UTF_8));
return Base64.getEncoder().encodeToString(signature.sign());
}
/**
* minify json
* @param body
* @return
*/
public static String minifyJson(String body) {
if (body == null || body.trim().isEmpty()) {
return "";
}
try {
Object parsed = JSON.parse(body);
if (!(parsed instanceof JSONObject)) {
return JSON.toJSONString(parsed);
}
JSONObject jsonObject = (JSONObject) parsed;
JSONObject sorted = new JSONObject();
jsonObject.keySet().stream().sorted().forEach(key -> sorted.put(key, jsonObject.get(key)));
return JSON.toJSONString(sorted, JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.WriteBigDecimalAsPlain);
} catch (Exception e) {
return body.trim();
}
}
public static String sha256Lower(String input) {
return DigestUtils.sha256Hex(input == null ? "" : input).toLowerCase();
}
private static PrivateKey buildPrivateKey(byte[] keyBytes) throws Exception {
KeyFactory keyFactory = KeyFactory.getInstance("RSA");
try {
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(keyBytes));
} catch (Exception ex) {
return keyFactory.generatePrivate(new PKCS8EncodedKeySpec(wrapPkcs1ToPkcs8(keyBytes)));
}
}
private static byte[] wrapPkcs1ToPkcs8(byte[] pkcs1) {
byte[] version = new byte[]{0x02, 0x01, 0x00};
byte[] algId = new byte[]{0x30, 0x0d, 0x06, 0x09, 0x2a, (byte) 0x86, 0x48, (byte) 0x86,
(byte) 0xf7, 0x0d, 0x01, 0x01, 0x01, 0x05, 0x00};
byte[] octetString = concat(new byte[]{0x04}, derLength(pkcs1.length), pkcs1);
byte[] sequence = concat(version, algId, octetString);
return concat(new byte[]{0x30}, derLength(sequence.length), sequence);
}
private static String toCurl(String baseUrl, SignedRequest request) {
StringBuilder builder = new StringBuilder();
builder.append("curl --location '")
.append(baseUrl)
.append(request.path)
.append("' \\\n");
builder.append(" --request ").append(request.method).append(" \\\n");
for (Map.Entry<String, String> entry : request.headers.entrySet()) {
builder.append(" --header '")
.append(entry.getKey())
.append(": ")
.append(entry.getValue())
.append("' \\\n");
}
builder.append(" --data-raw '").append(request.body).append("'");
return builder.toString();
}
private static byte[] concat(byte[]... arrays) {
int totalLength = 0;
for (byte[] array : arrays) {
totalLength += array.length;
}
byte[] result = new byte[totalLength];
int offset = 0;
for (byte[] array : arrays) {
System.arraycopy(array, 0, result, offset, array.length);
offset += array.length;
}
return result;
}
private static byte[] derLength(int length) {
if (length < 128) {
return new byte[]{(byte) length};
}
int temp = length;
int count = 0;
while (temp > 0) {
count++;
temp >>= 8;
}
byte[] result = new byte[1 + count];
result[0] = (byte) (0x80 | count);
for (int i = count; i > 0; i--) {
result[i] = (byte) (length & 0xFF);
length >>= 8;
}
return result;
}
private static class SignedRequest {
private final String method;
private final String path;
private final Map<String, String> headers;
private final String body;
private SignedRequest(String method, String path, Map<String, String> headers, String body) {
this.method = method;
this.path = path;
this.headers = headers;
this.body = body;
}
}
}
2. Signature for Business API Requests
Applicable APIs:
- All other
/apis/v1/**business APIs except/apis/v1/access-tokenand/apis/v1/refresh-token
Signature algorithm:
HmacSHA512
String to sign:
METHOD:path:accessToken:sha256(minifyJson(body)):timestamp:nonceExample:
POST:/apis/v1/user/balance/list:your-access-token:sha256(body):1714291200:6f2e7c1a4d9b4c2f9c7d1e3a5b6f8a0cNotes:
- The
secretKeyreturned by/apis/v1/access-tokenis the HMAC key - The
accessTokencarried inPexxAuthorizationmust match theaccessTokenused in the string to sign - An empty request body participates in signing as an empty string
API Specifications
1. POST /apis/v1/access-token
POST /apis/v1/access-tokenPurpose:
- Request access credentials
Request body parameters:
| Parameter | Required | Type | Description |
|---|---|---|---|
merchantCode | Yes | string | Merchant identifier |
grantType | Yes | string | For the current integration, client_credentials is recommended |
businessId | No | string | Specify the business user identifier; if omitted, the merchant's primary business user will be used by default |
Response parameters:
| Parameter | Type | Description |
|---|---|---|
merchantCode | string | Merchant identifier |
businessUserId | string | The actual business user identifier for which the credentials are issued |
accessToken | string | Access token for business API calls |
refreshToken | string | Refresh token for renewing access credentials |
secretKey | string | Signing key for business API requests |
accessTokenExpiresIn | long | Remaining validity period of accessToken, in seconds |
refreshTokenExpiresIn | long | Remaining validity period of refreshToken, in seconds |
timestamp | long | Credential issuance time, Unix timestamp in seconds |
2. POST /apis/v1/refresh-token
POST /apis/v1/refresh-tokenPurpose:
- Refresh access credentials
Request body parameters:
| Parameter | Required | Type | Description |
|---|---|---|---|
merchantCode | Yes | string | Merchant identifier |
refreshToken | Yes | string | Refresh token for renewing access credentials |
Response parameters:
| Parameter | Type | Description |
|---|---|---|
merchantCode | string | Merchant identifier |
businessUserId | string | The actual business user identifier for which the credentials are issued |
accessToken | string | New access token for business API calls |
refreshToken | string | New refresh token for renewing access credentials |
secretKey | string | New signing key for business API requests |
accessTokenExpiresIn | long | Remaining validity period of accessToken, in seconds |
refreshTokenExpiresIn | long | Remaining validity period of refreshToken, in seconds |
timestamp | long | Credential issuance time, Unix timestamp in seconds |
3. General Requirements for Business APIs
Purpose:
- Use the access credentials to call specific business APIs
General requirements:
PexxAuthorization: Bearer {accessToken}must be included in the request header- The request must generate
X-SIGNATUREaccording to the business API signing rules - The request and response parameters of each specific business API are subject to its corresponding API documentation
Authentication Failure Response
Response Structure
When authentication fails, the response body uses the following common fields:
| Field | Description |
|---|---|
code | Response code |
msg | Error description |
data | Business data; usually empty on failure |
Error Codes
| Type | Value | Description |
|---|---|---|
| HTTP status code | 401 | Returned when required headers are missing, the signature is invalid, the token is invalid, or authentication fails |
| Business error code | 1004 | INVALID_ACCESS, invalid access, for example merchant mismatch or invalid credentials |
| Business error code | 1009 | ACCESS_TOKEN_EXPIRED, accessToken has expired |
| Business error code | 1010 | REFRESH_TOKEN_EXPIRED, refreshToken has expired |