Click Pay ApiClick Pay Api
  • en-US
  • 简体中文
  • en-US
  • 简体中文
  • Go Signing And Verification

Go Signing And Verification

Runtime

Go 1.18+; standard library. Put both files in the same paymentsign package.

Shared Contract

These examples implement the default merchant API and default platform callback, not separately agreed custom signature formats. Both languages of the documentation include the same executable source files.

API request with a body, signed with the merchant private key:

path + lowercaseHex(SHA512(rawBody)) + Timestamp

API request without a body, signed with the merchant private key:

path + Timestamp

Default asynchronous callback, verified with the platform public key:

lowercaseHex(SHA512(rawBody)) + Timestamp

Synchronous API response envelope, verified with the platform public key:

lowercaseHex(SHA512(platform-serialized data bytes)) + timestamp
  • Use RSA PKCS#1 v1.5 with SHA-512 and standard Base64 signatures, not RSA-PSS or RSA encryption.
  • Timestamps are Unix seconds. Verification uses the received timestamp, not a newly generated value. Header names are case-insensitive; Base64 signature values are case-sensitive.
  • The API path excludes scheme, host and query string. Callback signing content has no URL, path or separators.
  • Serialize JSON once. Sign and transmit exactly the same UTF-8 bytes. No sorting is required, but field order, whitespace, escaping, newlines and numeric representation must match the transmitted bytes.
  • Use null / nil / None for no request body. A present zero-byte body is different. Callback verification always hashes the complete received body.
  • Supported keys: unencrypted PKCS#1 / PKCS#8 private keys and X.509 SPKI / PKCS#1 public keys, as PEM or plain Base64, including escaped newlines. Certificates, encrypted private keys, SSH keys and non-RSA keys are not supported by these samples.
  • Sign requests with your merchant private key. Verify with the configured platform public key, not your merchant public key or a key supplied in an incoming request. Never share private keys with support; rotate exposed keys.

Failure Handling

Signing failures throw an exception or return an error. Stop the request; never send an empty signature. Verification returns normally on success and throws/returns an error on failure, logging [payment-verify] FAILED. Never ignore verification failures or continue processing financial effects.

Logs contain the stage, byte lengths and diagnostic reason, not payloads, complete signatures or keys. Keep diagnostics internal, disable HTTP body logging and rate-limit failed requests and logs.

Keep HTTPS certificate and hostname verification enabled. A timeout does not prove order creation failed. Do not automatically resubmit an order; query by merchant order number or reconcile manually.

Callback Integration

Limit body size, read raw bytes and the Timestamp / Signature headers, then verify before parsing JSON. Validate order ownership, amount, currency and allowed state transitions. Apply changes transactionally and idempotently. Return the required SUCCESS acknowledgement only after durable processing. Repeated deliveries must not apply financial effects twice.

Cryptographic verification alone does not prevent replay. Choose timestamp tolerance compatible with platform redelivery rules; implement durable idempotency, replay controls and rate limits separately. An overly short freshness window can reject legitimate retries.

Synchronous Responses Are Not Callbacks

For API response envelopes, read timestamp and sign from JSON, and verify the platform-serialized data bytes, not the entire response. For callbacks, read headers and verify the entire HTTP body.

Do not parse and re-serialize data: large integers, 1.00, escaping and field ordering may change. Use a parser that preserves the original JSON value text (for example Go json.RawMessage or C# JsonElement.GetRawText()), then pass its UTF-8 bytes to verifyPlatform / VerifyPlatform / verify_platform. Preserve JSON quotes/escapes for string values and the literal null for null values. Do not add whitespace. Missing signatures, malformed fields and failed verification are not trusted business results.

API Paths

Use the same request helper for the paths below. Refer to the relevant currency documentation for payload fields.

MethodPathPurpose
POST/v1/merchant/payCollection order
POST/v1/merchant/pay/order/infoCollection query
POST/v1/merchant/payoutPayout order
POST/v1/merchant/payout/order/infoPayout query
GET/v1/merchant/accountBalance query, no body

Signing And Verification Source

sign.go

package paymentsign

import (
	"crypto"
	"crypto/rand"
	"crypto/rsa"
	"crypto/sha512"
	"crypto/x509"
	"encoding/base64"
	"encoding/hex"
	"encoding/pem"
	"fmt"
	"log"
	"strconv"
	"strings"
)

func keyDER(input string, private bool) ([]byte, error) {
	if len(input) > 16384 {
		return nil, fmt.Errorf("key: exceeds 16 KiB")
	}
	input = strings.TrimSpace(input)
	input = strings.ReplaceAll(input, `\r\n`, "\n")
	input = strings.ReplaceAll(input, `\n`, "\n")
	if strings.HasPrefix(input, "-----BEGIN") {
		block, rest := pem.Decode([]byte(input))
		if block == nil || strings.TrimSpace(string(rest)) != "" {
			return nil, fmt.Errorf("key: expected one complete PEM block")
		}
		allowed := block.Type == "PUBLIC KEY" || block.Type == "RSA PUBLIC KEY"
		if private {
			allowed = block.Type == "PRIVATE KEY" || block.Type == "RSA PRIVATE KEY"
		}
		if !allowed || len(block.Headers) != 0 {
			return nil, fmt.Errorf("key: expected unencrypted RSA PEM of the correct kind")
		}
		return block.Bytes, nil
	}
	return decodeBase64(strings.Join(strings.Fields(input), ""), "key")
}

func decodeBase64(text, label string) ([]byte, error) {
	value, err := base64.StdEncoding.DecodeString(text)
	if err != nil || len(value) == 0 || base64.StdEncoding.EncodeToString(value) != text {
		return nil, fmt.Errorf("%s: invalid Base64", label)
	}
	return value, nil
}

func privateKey(input string) (*rsa.PrivateKey, error) {
	der, err := keyDER(input, true)
	if err != nil {
		return nil, err
	}
	if key, err := x509.ParsePKCS1PrivateKey(der); err == nil {
		return key, nil
	}
	key, err := x509.ParsePKCS8PrivateKey(der)
	if err == nil {
		if rsaKey, ok := key.(*rsa.PrivateKey); ok {
			return rsaKey, nil
		}
	}
	return nil, fmt.Errorf("key: invalid/truncated, encrypted or non-RSA private key; DER bytes=%d", len(der))
}

func publicKey(input string) (*rsa.PublicKey, error) {
	der, err := keyDER(input, false)
	if err != nil {
		return nil, err
	}
	if key, err := x509.ParsePKCS1PublicKey(der); err == nil {
		return key, nil
	}
	key, err := x509.ParsePKIXPublicKey(der)
	if err == nil {
		if rsaKey, ok := key.(*rsa.PublicKey); ok {
			return rsaKey, nil
		}
	}
	return nil, fmt.Errorf("key: invalid/truncated or non-RSA public key; DER bytes=%d", len(der))
}

func validTimestamp(text string) error {
	value, err := strconv.ParseInt(text, 10, 64)
	if err != nil || value <= 0 || strconv.FormatInt(value, 10) != text {
		return fmt.Errorf("invalid Timestamp: use Unix seconds")
	}
	return nil
}

func bodyHash(body []byte) string {
	digest := sha512.Sum512(body)
	return hex.EncodeToString(digest[:])
}

// nil means no request body; []byte{} means a present, empty body.
func SignRequest(path string, body []byte, timestamp, merchantPrivateKey string) (signature string, err error) {
	defer func() {
		if err != nil {
			log.Printf("[payment-sign] FAILED body_bytes=%d error=%q", len(body), err.Error())
		}
	}()
	if !strings.HasPrefix(path, "/") {
		return "", fmt.Errorf("path: expected API path")
	}
	if err := validTimestamp(timestamp); err != nil {
		return "", err
	}
	key, err := privateKey(merchantPrivateKey)
	if err != nil {
		return "", err
	}
	hash := ""
	if body != nil {
		hash = bodyHash(body)
	}
	digest := sha512.Sum512([]byte(path + hash + timestamp))
	sig, err := rsa.SignPKCS1v15(rand.Reader, key, crypto.SHA512, digest[:])
	if err != nil {
		return "", fmt.Errorf("RSA SHA512 signing failed: %w", err)
	}
	return base64.StdEncoding.EncodeToString(sig), nil
}

// Exact callback bytes, or exact signed data bytes for API envelopes. NO path.
func VerifyPlatform(body []byte, timestamp, signature, platformPublicKey string) (err error) {
	defer func() {
		if err != nil {
			log.Printf("[payment-verify] FAILED body_bytes=%d error=%q", len(body), err.Error())
		}
	}()
	if err := validTimestamp(timestamp); err != nil {
		return err
	}
	key, err := publicKey(platformPublicKey)
	if err != nil {
		return err
	}
	sig, err := decodeBase64(signature, "Signature")
	if err != nil {
		return err
	}
	if len(sig) != key.Size() {
		return fmt.Errorf("Signature length: got %d, expected %d bytes", len(sig), key.Size())
	}
	digest := sha512.Sum512([]byte(bodyHash(body) + timestamp))
	if err := rsa.VerifyPKCS1v15(key, crypto.SHA512, digest[:], sig); err != nil {
		return fmt.Errorf("RSA SHA512 verification failed: check platform key, raw bytes, timestamp and formula: %w", err)
	}
	return nil
}

HTTP Helper

http.go

package paymentsign

import (
	"bytes"
	"context"
	"fmt"
	"io"
	"net/http"
	"strconv"
	"strings"
	"time"
)

// rawBody is the exact serialized bytes, or nil for a bodyless GET.
func Send(ctx context.Context, baseURL, path, merchantCode string, rawBody []byte, merchantPrivateKey string) ([]byte, error) {
	if !strings.HasPrefix(baseURL, "https://") {
		return nil, fmt.Errorf("HTTPS is required")
	}
	timestamp := strconv.FormatInt(time.Now().Unix(), 10)
	signature, err := SignRequest(path, rawBody, timestamp, merchantPrivateKey)
	if err != nil {
		return nil, err
	}
	method := http.MethodPost
	var body io.Reader = bytes.NewReader(rawBody)
	if rawBody == nil {
		method = http.MethodGet
		body = nil
	}
	req, err := http.NewRequestWithContext(ctx, method, baseURL+path, body)
	if err != nil {
		return nil, err
	}
	req.Header.Set("Content-Type", "application/json")
	req.Header.Set("Merchant-Code", merchantCode)
	req.Header.Set("Timestamp", timestamp)
	req.Header.Set("Signature", signature)
	client := &http.Client{Timeout: 60 * time.Second, CheckRedirect: func(_ *http.Request, _ []*http.Request) error { return http.ErrUseLastResponse }}
	response, err := client.Do(req)
	if err != nil {
		return nil, err
	}
	defer response.Body.Close()
	if response.StatusCode < 200 || response.StatusCode >= 300 {
		return nil, fmt.Errorf("HTTP %d; reconcile before retrying", response.StatusCode)
	}
	result, err := io.ReadAll(io.LimitReader(response.Body, (8<<20)+1))
	if err != nil {
		return nil, err
	}
	if len(result) > 8<<20 {
		return nil, fmt.Errorf("response exceeds 8 MiB")
	}
	return result, nil // Preserve raw response for envelope verification.
}

Integration Examples

The following snippets run inside your application; payload, trusted configuration and transactional business handlers must be supplied by your integration. They do not automatically submit real orders.

// Imports: encoding/json, strconv, time, io, net/http.
body, err := json.Marshal(payload)
if err != nil { return err }
timestamp := strconv.FormatInt(time.Now().Unix(), 10)
signature, err := paymentsign.SignRequest("/v1/merchant/pay", body, timestamp, merchantPrivateKey)
if err != nil { return err }
// Transmit exactly body; use timestamp/signature as headers. Do not marshal again.
// No body: paymentsign.SignRequest("/v1/merchant/account", nil, timestamp, merchantPrivateKey)
// Inside an http.Handler; configuredPlatformPublicKey is loaded from trusted configuration.
r.Body = http.MaxBytesReader(w, r.Body, 1<<20)
defer r.Body.Close()
rawBody, err := io.ReadAll(r.Body)
if err != nil { http.Error(w, "invalid body", 400); return }
if err := paymentsign.VerifyPlatform(rawBody, r.Header.Get("Timestamp"),
    r.Header.Get("Signature"), configuredPlatformPublicKey); err != nil {
    http.Error(w, "invalid signature", 401); return
}
// Only now decode JSON, validate the order and commit an idempotent update.
// Write SUCCESS only after durable processing.
Last Updated: 9/22/26, 2:41 PM
Contributors: aa, aaa