API Authentication Guide
To ensure secure communication, all requests to this platform's API must undergo strict authentication. This document details how to generate a request signature and pass verification.
Preparation: Obtain API Credentials
Before you begin, please log in to your merchant dashboard and obtain the following two key credentials from the API settings:
API_KEY
: Your unique identifier.SECRET_KEY
: The private key used for calculating signatures. Please keep it safe and do not disclose it.
Core Authentication Flow
The core of the authentication is that the client uses the SECRET_KEY
to generate a signature (signature
) for the key information of the request. Then, the API_KEY
, signature
, and other auxiliary information are sent to the server via HTTP headers. The server will recalculate the signature locally in the exact same way and compare it with the signature sent by the client. If they match, the verification passes.
Building the String to Sign
The first step in the signing process is to combine several parts of the request into a single, standardized string according to the following rules. We call this stringToSign
.
The structure of stringToSign
is as follows, with each part separated by a newline character \n
:
REQUEST_BODY\n
TIMESTAMP\n
NONCE
Detailed rules for each component:
Variable | Description | Rules and Explanations |
---|---|---|
REQUEST_BODY | Request Body | The original content of the HTTP request body. Notes: 1. For requests without a body, such as GET , this part is an empty string. 2. For POST and PUT requests, this part is the raw byte stream of the request body. Ensure that you do not modify it in any way (e.g., adding/removing spaces, changing the order of JSON fields). |
TIMESTAMP | Timestamp | The current Unix timestamp (in seconds), represented as a string. The server will reject requests with a time difference of more than ±5 minutes from the server time to prevent replay attacks. |
NONCE | Random Number | A single-use random string used to prevent replay attacks. Requirements: 1. It is recommended to use a UUID or other high-strength random string generator. 2. For the same API_KEY , the NONCE must be unique within the valid time window defined by TIMESTAMP . The server will reject processing a NONCE that has already been received. |
Important
When constructing stringToSign
, ensure that all text content is UTF-8 encoded.
Calculating the Signature
- Use the
HMAC-SHA256
algorithm with yourSECRET_KEY
as the key to hash thestringToSign
constructed in the previous step. - Hex-encode the resulting binary hash value.
- Convert the encoded hexadecimal string to lowercase to get the final
signature
.
Pseudocode Example:
signature = hmac_sha256(secret_key, stringToSign).hex().lower()
Sending the API Request
Place the following authentication information in the HTTP request header:
Header Field | Value | Source |
---|---|---|
X-Api-Key | Your API_KEY | From the merchant dashboard |
X-Timestamp | The timestamp used | Must be consistent with the TIMESTAMP in stringToSign |
X-Nonce | The nonce used | Must be consistent with the NONCE in stringToSign |
X-Signature | The calculated signature | signature |
Complete Example
Suppose we have the following credentials and request information:
- API_KEY:
3AUpfeK573UH5vVe
- SECRET_KEY:
5ShtY7nXAT8Wm2RBeKLv7iPakVyxjddU
- Request:
POST /openapi/v1/payment HTTP/1.1
Content-Type: application/json
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}
Building stringToSign
- REQUEST_BODY:
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}
- TIMESTAMP: Assume it is
1754574105
- NONCE: Assume it is
random_nonce_str
Concatenate the above parts with \n
to get stringToSign
(the \n
here is for illustration purposes):
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}\n1754574105\nrandom_nonce_str
Calculating signature
Use the HMAC-SHA256
algorithm with 5ShtY7nXAT8Wm2RBeKLv7iPakVyxjddU
as the key to calculate the signature for the stringToSign
above. The resulting signature is ce4f73fcc17722e053f7315bfa48384bc50e579ec760e71fa91a6f7cf0d24bfa
.
Assembling and Sending the Request
POST /openapi/v1/payment HTTP/1.1
Host: api.zaepe.com
Content-Type: application/json
X-Api-Key: 3AUpfeK573UH5vVe
X-Nonce: random_nonce_str
X-Signature: ce4f73fcc17722e053f7315bfa48384bc50e579ec760e71fa91a6f7cf0d24bfa
X-Timestamp: 1754574105
{"order_no":"Pay1754574105","chain_type":"bsc","order_amount":"1","product_name":"Test product name","notify_url":"http://api.example.com/my-notify-url","redirect_url":"","meta":""}
Reference Code
Golang
package apisign
import (
"bytes"
"crypto/hmac"
"crypto/sha256"
"encoding/hex"
"io"
"net/http"
"strconv"
"strings"
"time"
"github.com/cockroachdb/errors"
)
const (
HeaderAPIKey = "X-Api-Key"
HeaderTimestamp = "X-Timestamp"
HeaderNonce = "X-Nonce"
HeaderSignature = "X-Signature"
// DefaultExpiration defines the default expiration time for a signature.
DefaultExpiration = 30 * time.Second
)
type SignError struct {
Field string
Err error
}
func (e *SignError) Error() string {
return e.Field + ": " + e.Err.Error()
}
func Verify(req *http.Request, secret string) error {
if key := req.Header.Get(HeaderAPIKey); key == "" {
return &SignError{Field: HeaderAPIKey, Err: errors.New("empty api key")}
}
timestamp := req.Header.Get(HeaderTimestamp)
if timestamp == "" {
return &SignError{Field: HeaderTimestamp, Err: errors.New("empty timestamp")}
}
unix, err := strconv.ParseInt(timestamp, 10, 64)
if err != nil {
return &SignError{Field: HeaderTimestamp, Err: errors.New("invalid timestamp")}
}
t := time.Unix(unix, 0)
if time.Since(t).Abs() > DefaultExpiration {
return &SignError{Field: HeaderTimestamp, Err: errors.New("timestamp expired")}
}
nonce := req.Header.Get(HeaderNonce)
if nonce == "" {
return &SignError{Field: HeaderNonce, Err: errors.New("empty nonce")}
}
signature := strings.ToLower(req.Header.Get(HeaderSignature))
if signature == "" {
return &SignError{Field: HeaderSignature, Err: errors.New("empty signature")}
}
b, err := io.ReadAll(req.Body)
if err != nil {
return &SignError{Field: HeaderSignature, Err: errors.Wrap(err, "fail to read body")}
}
req.Body = io.NopCloser(bytes.NewReader(b))
sign := Sign(secret, b, t, nonce)
if !hmac.Equal([]byte(sign), []byte(signature)) {
return &SignError{Field: HeaderSignature, Err: errors.New("invalid signature")}
}
return nil
}
// Sign generates a signature for the given request body, timestamp, and nonce.
// The signature is an HMAC-SHA256 hash of the request body, timestamp, and nonce, separated by newlines.
func Sign(secretKey string, requestBody []byte, t time.Time, nonce string) string {
// For debugging, you can easily print the content to be signed.
// log.Printf("signing content: %s", string(content))
content := bytes.Join(
[][]byte{
requestBody,
[]byte(strconv.FormatInt(t.Unix(), 10)),
[]byte(nonce),
},
[]byte("\n"),
)
mac := hmac.New(sha256.New, []byte(secretKey))
mac.Write(content)
return hex.EncodeToString(mac.Sum(nil))
}
PHP
class SignError extends \Exception
{
/**
* @var string
*/
protected $field;
public function __construct(string $field, string $message = "", int $code = 0, \Throwable $previous = null)
{
parent::__construct($message, $code, $previous);
$this->field = $field;
}
public function getField(): string
{
return $this->field;
}
public function __toString(): string
{
return $this->field . ": " . $this->getMessage();
}
}
class ApiSigner
{
public static function verify(\Psr\Http\Message\ServerRequestInterface $request, string $secret): bool
{
$key = $request->getHeaderLine('X-Api-Key');
if (empty($key)) {
throw new SignError("X-Api-Key", "empty api key");
}
$timestamp = $request->getHeaderLine('X-Timestamp');
if (empty($timestamp)) {
throw new SignError("X-Timestamp", "empty timestamp");
}
$unixTimestamp = (int)$timestamp;
if ((string)$unixTimestamp !== $timestamp) {
throw new SignError("X-Timestamp", "invalid timestamp");
}
$nonce = $request->getHeaderLine('X-Nonce');
if (empty($nonce)) {
throw new SignError("X-Nonce", "empty nonce");
}
$signature = strtolower($request->getHeaderLine('X-Signature'));
if (empty($signature)) {
throw new SignError("X-Signature", "empty signature");
}
$body = (string)$request->getBody();
$request->getBody()->rewind();
$sign = self::sign(
$secret,
$body,
$unixTimestamp,
$nonce
);
if ($sign !== $signature) {
throw new SignError("X-Signature", "invalid signature");
}
return true;
}
public static function sign(
string $secretKey,
string $requestBody,
int $timestamp,
string $nonce
): string {
$data = implode("\n", [
$requestBody,
(string)$timestamp,
$nonce,
]);
return hash_hmac('sha256', $data, $secretKey);
}
}
FAQ
Please troubleshoot in the following order:
- Is the
SECRET_KEY
correct? - Is the concatenation order of
stringToSign
completely correct? - Is the
REQUEST_BODY
byte-for-byte identical to what is actually sent? (Check for extra spaces or newlines) - Is the
TIMESTAMP
a Unix timestamp in seconds and within the server's allowed time window? - Are all strings involved in the concatenation UTF-8 encoded?