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

EnvironmentBase URL
Sandboxhttp://qa.backend.pexx.com:8606
Productionhttps://saas.backend.pexx.com

Merchant Onboarding Materials

Before integration, key exchange and merchant configuration must be completed.

  1. The merchant generates its own RSA key pair.

  2. The merchant provides the RSA public key to the Pexx service for storage.

  3. The RSA private key must be securely stored by the merchant. The Pexx service does not store the merchant's private key.

  4. After merchant configuration is completed, the Pexx service provides the following information:

    • merchantCode: merchant identifier
    • PexxApiKey: 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.pem

Notes:

  • merchant_private_key_pkcs8.pem is the recommended merchant private key file
  • merchant_public_key.pem is the merchant public key file to be provided to the Pexx service
  • If the Pexx service requires the public key in Base64 format, remove the PEM header, footer, and line breaks before submission

Glossary

TermDescription
merchantCodeThe unique merchant identifier assigned by the Pexx service
PexxApiKeyThe API key assigned by the Pexx service and used to identify the merchant
accessTokenThe access token used for business API calls and passed in PexxAuthorization
refreshTokenThe token used to refresh access credentials
secretKeyThe signing key used to generate HMAC signatures for business API requests
grantTypeThe credential application type; for the current integration, client_credentials is recommended
businessIdOptional business user identifier; if omitted, the merchant's primary business user will be used by default
X-TIMESTAMPUnix timestamp in seconds; should use the current time when the request is sent
X-NONCEA unique random string for each request to prevent replay attacks; recommended maximum length is 32 characters, and a UUID without hyphens is recommended
X-SIGNATUREThe request signature value, generated according to the signing rules in this document and then Base64-encoded
pathThe 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, and secretKey required 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 accessToken to access business APIs
  • Use secretKey to 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 accessToken expires by using refreshToken

API Integration Details

Header Requirements

Common Headers

HeaderRequiredDescription
Content-TypeYesMust be application/json
PexxApiKeyYesMerchant API key
X-TIMESTAMPYesUnix timestamp in seconds
X-NONCEYesUnique random string for each request; recommended maximum length is 32 characters
X-SIGNATUREYesRequest signature, Base64-encoded

Additional Header for Business APIs

HeaderRequiredDescription
PexxAuthorizationYesFormat: Bearer {accessToken}

Signing Rules

1. Signature for access-token / refresh-token Requests

Applicable APIs:

  • POST /apis/v1/access-token
  • POST /apis/v1/refresh-token

Signature algorithm:

  • SHA256withRSA

String to sign:

METHOD:path:sha256(minifyJson(body)):apiKey:merchantCode:timestamp:nonce

Example:

POST:/apis/v1/access-token:sha256(body):your-api-key:your-merchant-code:1714291200:6f2e7c1a4d9b4c2f9c7d1e3a5b6f8a0c

Notes:

  • METHOD must be uppercase, for example POST
  • path must use the actual request path, for example /apis/v1/access-token
  • body must 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-token and /apis/v1/refresh-token

Signature algorithm:

  • HmacSHA512

String to sign:

METHOD:path:accessToken:sha256(minifyJson(body)):timestamp:nonce

Example:

POST:/apis/v1/user/balance/list:your-access-token:sha256(body):1714291200:6f2e7c1a4d9b4c2f9c7d1e3a5b6f8a0c

Notes:

  • The secretKey returned by /apis/v1/access-token is the HMAC key
  • The accessToken carried in PexxAuthorization must match the accessToken used in the string to sign
  • An empty request body participates in signing as an empty string

API Specifications

1. POST /apis/v1/access-token

Purpose:

  • Request access credentials

Request body parameters:

ParameterRequiredTypeDescription
merchantCodeYesstringMerchant identifier
grantTypeYesstringFor the current integration, client_credentials is recommended
businessIdNostringSpecify the business user identifier; if omitted, the merchant's primary business user will be used by default

Response parameters:

ParameterTypeDescription
merchantCodestringMerchant identifier
businessUserIdstringThe actual business user identifier for which the credentials are issued
accessTokenstringAccess token for business API calls
refreshTokenstringRefresh token for renewing access credentials
secretKeystringSigning key for business API requests
accessTokenExpiresInlongRemaining validity period of accessToken, in seconds
refreshTokenExpiresInlongRemaining validity period of refreshToken, in seconds
timestamplongCredential issuance time, Unix timestamp in seconds

2. POST /apis/v1/refresh-token

Purpose:

  • Refresh access credentials

Request body parameters:

ParameterRequiredTypeDescription
merchantCodeYesstringMerchant identifier
refreshTokenYesstringRefresh token for renewing access credentials

Response parameters:

ParameterTypeDescription
merchantCodestringMerchant identifier
businessUserIdstringThe actual business user identifier for which the credentials are issued
accessTokenstringNew access token for business API calls
refreshTokenstringNew refresh token for renewing access credentials
secretKeystringNew signing key for business API requests
accessTokenExpiresInlongRemaining validity period of accessToken, in seconds
refreshTokenExpiresInlongRemaining validity period of refreshToken, in seconds
timestamplongCredential 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-SIGNATURE according 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:

FieldDescription
codeResponse code
msgError description
dataBusiness data; usually empty on failure

Error Codes

TypeValueDescription
HTTP status code401Returned when required headers are missing, the signature is invalid, the token is invalid, or authentication fails
Business error code1004INVALID_ACCESS, invalid access, for example merchant mismatch or invalid credentials
Business error code1009ACCESS_TOKEN_EXPIRED, accessToken has expired
Business error code1010REFRESH_TOKEN_EXPIRED, refreshToken has expired