跳至主要內容

请求签名

大约 4 分钟

请求签名

适用于 API v2.0(RSA)。调代收 / 代付 / 查询时,用商户私钥生成 X-SIGNATURE

最常见的对接错误

不要拿这套公式(或商户私钥)去验回调。回调验签见 回调验签tradeNo|X-TIMESTAMP + Platform Public Key)。

必填 Header

Header必填说明
Content-Typeapplication/json
X-TIMESTAMP格式 yyyy-MM-dd'T'HH:mm:ssXXX,如 2024-12-30T18:30:36+07:00
X-SIGNATUREBase64(SHA256withRSA(...))
X-PARTNER-ID你的 Merchant ID

代收 / 代付:X-PARTNER-ID 必须与 Body 里的 merchant.merchantId 完全一致

签名公式

网关 v2 校验:

stringToSign = X-TIMESTAMP + "|" + merchant_secret + "|" + minify(requestBody)
X-SIGNATURE  = Base64( SHA256withRSA(stringToSign, merchantPrivateKey) )
部分来源
X-TIMESTAMP与请求头 X-TIMESTAMP 同一值
merchant_secret商户后台 → Configuration Info
minify(requestBody)实际发送的 JSON Body(去掉字符串外空白)
私钥商户 RSA 私钥(PKCS#8,Base64)

压缩 Body

签名前去掉 JSON 字符串外的空白。签名字符串必须与 HTTP Body 完全一致

格式化 JSON(示例)
{
    "area": 10,
    "callbackUrl": "https://docs.smilepayz.com/en/",
    "merchant": {
        "merchantId": "20019"
    },
    "money": {
        "amount": 10000,
        "currency": "IDR"
    },
    "orderNo": "20019c5b63c4b-e34a-4855-9b20-d4b",
    "paymentMethod": "QRIS",
    "purpose": "Purpose For Transaction from Java SDK"
}

压缩后(实际签名并发送):

{"area":10,"callbackUrl":"https://docs.smilepayz.com/en/","merchant":{"merchantId":"20019"},"money":{"amount":10000,"currency":"IDR"},"orderNo":"20019c5b63c4b-e34a-4855-9b20-d4b","paymentMethod":"QRIS","purpose":"Purpose For Transaction from Java SDK"}

签名后不要再改 Body。

待签名字符串示例

{merchant_secret} 换成你在 Configuration Info 里的真实密钥:

2024-12-30T18:30:36+07:00|{merchant_secret}|{"area":10,"callbackUrl":"https://docs.smilepayz.com/en/","merchant":{"merchantId":"20019"},"money":{"amount":10000,"currency":"IDR"},"orderNo":"20019c5b63c4b-e34a-4855-9b20-d4b","paymentMethod":"QRIS","purpose":"Purpose For Transaction from Java SDK"}

请求头示例

Content-Type: application/json
X-TIMESTAMP: 2024-12-30T18:30:36+07:00
X-SIGNATURE: {Base64 签名}
X-PARTNER-ID: {merchantId}

X-TIMESTAMP 须符合上述格式,且与 Smilepayz 服务器时间(Asia/Jakarta)相差在 24 小时 内。请用 NTP 同步时钟。

示例代码

点开你的语言。门户密钥是裸 Base64(私钥 PKCS#8,公钥 X.509/SPKI),无 PEM 头。传入前请先压缩 Body。

Java
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.PrivateKey;
import java.security.Signature;
import java.security.spec.PKCS8EncodedKeySpec;
import java.util.Base64;

/** Build X-SIGNATURE for Smilepayz API v2. */
public class RequestSigner {
    public static String sign(
            String timestamp,
            String merchantSecret,
            String minifiedBody,
            String privateKeyBase64) throws Exception {
        // timestamp|merchant_secret|minify(body)
        String stringToSign = timestamp + "|" + merchantSecret + "|" + minifiedBody;

        byte[] keyBytes = Base64.getDecoder().decode(privateKeyBase64);
        PrivateKey privateKey = KeyFactory.getInstance("RSA")
                .generatePrivate(new PKCS8EncodedKeySpec(keyBytes));

        Signature signature = Signature.getInstance("SHA256withRSA");
        signature.initSign(privateKey);
        signature.update(stringToSign.getBytes(StandardCharsets.UTF_8));
        return Base64.getEncoder().encodeToString(signature.sign());
    }
}
PHP
<?php
/** Build X-SIGNATURE for Smilepayz API v2. $privateKeyBase64 = raw Base64 from portal (no PEM). */
function sign(string $timestamp, string $merchantSecret, string $minifiedBody, string $privateKeyBase64): string {
    // timestamp|merchant_secret|minify(body)
    $stringToSign = $timestamp . '|' . $merchantSecret . '|' . $minifiedBody;

    $keyPem = "-----BEGIN PRIVATE KEY-----
"
        . chunk_split($privateKeyBase64, 64, "
")
        . "-----END PRIVATE KEY-----
";
    $privateKey = openssl_pkey_get_private($keyPem);
    if ($privateKey === false) {
        throw new RuntimeException('Invalid private key');
    }

    openssl_sign($stringToSign, $sig, $privateKey, OPENSSL_ALGO_SHA256);
    return base64_encode($sig);
}
Python
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding

def sign(timestamp: str, merchant_secret: str, minified_body: str, private_key_base64: str) -> str:
    """Build X-SIGNATURE for Smilepayz API v2. private_key_base64 = raw Base64 from portal."""
    # timestamp|merchant_secret|minify(body)
    string_to_sign = f"{timestamp}|{merchant_secret}|{minified_body}"

    private_key = serialization.load_der_private_key(
        base64.b64decode(private_key_base64),
        password=None,
    )
    sig = private_key.sign(
        string_to_sign.encode("utf-8"),
        padding.PKCS1v15(),
        hashes.SHA256(),
    )
    return base64.b64encode(sig).decode("utf-8")
Node.js
const crypto = require("crypto");

/** Build X-SIGNATURE for Smilepayz API v2. privateKeyBase64 = raw Base64 from portal. */
function sign(timestamp, merchantSecret, minifiedBody, privateKeyBase64) {
  // timestamp|merchant_secret|minify(body)
  const stringToSign = `${timestamp}|${merchantSecret}|${minifiedBody}`;

  const privateKey = crypto.createPrivateKey({
    key: Buffer.from(privateKeyBase64, "base64"),
    format: "der",
    type: "pkcs8",
  });
  const signer = crypto.createSign("RSA-SHA256");
  signer.update(stringToSign, "utf8");
  return signer.sign(privateKey, "base64");
}
Golang
package signature

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha256"
	"crypto/x509"
	"encoding/base64"
	"fmt"
)

// Sign builds X-SIGNATURE for Smilepayz API v2. privateKeyBase64 = raw Base64 from portal.
func Sign(timestamp, merchantSecret, minifiedBody, privateKeyBase64 string) (string, error) {
	// timestamp|merchant_secret|minify(body)
	stringToSign := timestamp + "|" + merchantSecret + "|" + minifiedBody

	keyBytes, err := base64.StdEncoding.DecodeString(privateKeyBase64)
	if err != nil {
		return "", err
	}
	key, err := x509.ParsePKCS8PrivateKey(keyBytes)
	if err != nil {
		return "", err
	}
	rsaKey, ok := key.(*rsa.PrivateKey)
	if !ok {
		return "", fmt.Errorf("not an RSA private key")
	}

	hash := sha256.Sum256([]byte(stringToSign))
	sig, err := rsa.SignPKCS1v15(rand.Reader, rsaKey, crypto.SHA256, hash[:])
	if err != nil {
		return "", err
	}
	return base64.StdEncoding.EncodeToString(sig), nil
}

Signature Test(商户后台)

  1. 登录 商户后台open in new window
  2. 打开 Configuration(沙盒:左侧菜单;正式:多为 Settings → Configuration)。
  3. 进入 Signature Test
  4. 填入 X-PARTNER-IDMerchant SecretX-TIMESTAMP、压缩后的 Body JSON、商户私钥 → Generate Signature

凭证在哪拿

Merchant Secret / X-PARTNER-ID / Platform Public Key:见 Configuration Info。商户私钥:见 API Setting → Generate。详见 集成信息

商户后台 — Signature Test

快速排查

问题先查什么
请求签名对不上Body 是否压缩且签完未改;顺序是否 timestamp|secret|body;私钥是否 PKCS#8 Base64
X-PARTNER-ID 被拒是否与 Body 里 merchant.merchantId 一致(代收/代付)
时间戳被拒格式是否 yyyy-MM-dd'T'HH:mm:ssXXX;是否在 Asia/Jakarta 时间 ±24h;是否 NTP