Super Qi uses RSA256 digital signatures to sign all webhook notifications and API responses. Validating these signatures is critical to ensure that:
Always validate signatures for webhook notifications including:
If your infrastructure requires IP whitelisting, webhook calls from Super Qi will originate from the following IP addresses:
8.209.108.8047.254.141.23847.254.132.7Super Qi sends signatures in the Signature HTTP header with this format:
Signature: algorithm=RSA256, keyVersion=2, signature=<base64_encoded_signature>
Components:
algorithm - Always RSA256 (SHA256withRSA)keyVersion - Version number of the public key to usesignature - Base64-encoded RSA signaturekeyVersion indicates which Super Qi public key to use for verification. Make sure you're using the correct public key version that matches the signatures you receive.Extract these headers from the webhook request:
clientID := request.Header.Get("Client-Id")
requestTime := request.Header.Get("Request-Time")
signatureHeader := request.Header.Get("Signature")
bodyBytes, err := io.ReadAll(request.Body)
if err != nil {
return err
}
bodyString := string(bodyBytes)
Build the content string that was signed by Super Qi:
Format:
<HTTP_METHOD> <HTTP_URI>
<Client-Id>.<Request-Time>.<HTTP_BODY>
Example:
httpMethod := "POST"
path := "/api/payment/webhook"
signContent := fmt.Sprintf("%s %s\n%s.%s.%s",
httpMethod,
path,
clientID,
requestTime,
bodyString,
)
Result:
POST /api/payment/webhook
<Client-Id>.<Request-Time>.<JSON_BODY>
Extract the base64-encoded signature from the header:
func parseSignature(signatureHeader string) (string, error) {
parts := strings.Split(signatureHeader, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "signature=") {
return strings.TrimPrefix(part, "signature="), nil
}
}
return "", errors.New("signature not found in header")
}
signatureBase64, err := parseSignature(signatureHeader)
if err != nil {
return err
}
signature, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return fmt.Errorf("failed to decode signature: %v", err)
}
Use the Super Qi public key to verify:
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
)
// Hash the sign content
hash := sha256.Sum256([]byte(signContent))
// Verify using Super Qi's public key
err = rsa.VerifyPKCS1v15(publicKey, crypto.SHA256, hash[:], signature)
if err != nil {
return fmt.Errorf("signature verification failed: %v", err)
}
// Signature is valid!
Here's a complete Go implementation:
package webhook
import (
"crypto"
"crypto/rsa"
"crypto/sha256"
"crypto/x509"
"encoding/base64"
"encoding/pem"
"errors"
"fmt"
"io"
"net/http"
"os"
"strings"
)
type SignatureValidator struct {
publicKey *rsa.PublicKey
}
func NewSignatureValidator(publicKeyPath string) (*SignatureValidator, error) {
publicKey, err := loadPublicKey(publicKeyPath)
if err != nil {
return nil, err
}
return &SignatureValidator{publicKey: publicKey}, nil
}
func loadPublicKey(path string) (*rsa.PublicKey, error) {
pemData, err := os.ReadFile(path)
if err != nil {
return nil, err
}
block, _ := pem.Decode(pemData)
if block == nil {
return nil, errors.New("failed to parse PEM block")
}
pub, err := x509.ParsePKIXPublicKey(block.Bytes)
if err != nil {
return nil, err
}
rsaPub, ok := pub.(*rsa.PublicKey)
if !ok {
return nil, errors.New("not an RSA public key")
}
return rsaPub, nil
}
func (v *SignatureValidator) ValidateWebhook(
r *http.Request,
path string,
) error {
// Extract headers
clientID := r.Header.Get("Client-Id")
requestTime := r.Header.Get("Request-Time")
signatureHeader := r.Header.Get("Signature")
if clientID == "" || requestTime == "" || signatureHeader == "" {
return errors.New("missing required headers")
}
// Read body
bodyBytes, err := io.ReadAll(r.Body)
if err != nil {
return err
}
// Construct sign content
signContent := fmt.Sprintf("POST %s\n%s.%s.%s",
path,
clientID,
requestTime,
string(bodyBytes),
)
// Parse signature
signatureBase64, err := parseSignatureHeader(signatureHeader)
if err != nil {
return err
}
// Decode signature
signature, err := base64.StdEncoding.DecodeString(signatureBase64)
if err != nil {
return fmt.Errorf("failed to decode signature: %v", err)
}
// Hash sign content
hash := sha256.Sum256([]byte(signContent))
// Verify signature
err = rsa.VerifyPKCS1v15(v.publicKey, crypto.SHA256, hash[:], signature)
if err != nil {
return fmt.Errorf("signature verification failed: %v", err)
}
return nil
}
func parseSignatureHeader(header string) (string, error) {
parts := strings.Split(header, ",")
for _, part := range parts {
part = strings.TrimSpace(part)
if strings.HasPrefix(part, "signature=") {
return strings.TrimPrefix(part, "signature="), nil
}
}
return "", errors.New("signature not found in header")
}
Usage:
func handleWebhook(w http.ResponseWriter, r *http.Request) {
validator, err := NewSignatureValidator("path/to/superqi_public_key.pem")
if err != nil {
http.Error(w, "Server configuration error", 500)
return
}
err = validator.ValidateWebhook(r, "/api/payment/webhook")
if err != nil {
log.Printf("Invalid signature: %v", err)
http.Error(w, "Invalid signature", 401)
return
}
// Signature is valid - safe to process webhook
// ... process payment notification
}
Use the RSA public key that matches your environment for signature verification (PEM format). Make sure you use the UAT key when validating signatures from the UAT environment and the Production key when validating signatures from production.
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEA249W/txwtMNVMw2jv5uN
u06atBtlvOon+EYopcBV1WzSKXsllq8k+5GEKaRw2n2A2Ix7IalDLfxf5cbPLLnC
IdqyPEfeoQBN2xBL2m0cjFFoPL0JJIHHHvJs75MVR8bpUFuipGOxK+a4316mMlRv
eX4F+YmpCJFXoHgJzJrQlglRNO/HZYQVaP3JRZg4KNSg1o2esxgaCImhCt8owegw
ou9GgYlEYpQjlQYSAgGill36nIMEoUqUqVYUA0o4sOLYPb2hepUROED8Zuc0mW/q
oGVQBihuZInhM2UOiuuw/tP/cpmNbMR9SeCbEDUt9HHd60KpXciNim96tz1zIZQv
jQIDAQAB
-----END PUBLIC KEY-----
-----BEGIN PUBLIC KEY-----
MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAvTCsasBhpdL0juG1mql4
8efGDM//c/GBcVs/weV0BQKQ6fdBYbgLZ0uR+OSsd4krEw3CltsQKUjE5B3VSjD1
xB2aYNe2uF7xIdqPlYlYGnXiQUTuXtOuNQMc9myFGZlhPPVJwotySOFRgjgv79ua
NLYaqlDYvUHyGaJACOfFAq9z2cn/KYysJPZQXikMToT2WaRzvmT1xjYDodd2jdq7
SDBNRZrp8loM25z4Q/MbcHxFYTssWU7i+DsIgUAsK7T832QXMLtYm45pa4UVPduP
RG02LDRQfzQkyXkrIe/DyjSwBL+AjFTgWiecrmknCdTSiMyUd1CYmYyRfEuKFv0e
CQIDAQAB
-----END PUBLIC KEY-----
If you encounter any issues with signature validation, please contact the Super Qi MiniApp team.
Possible Causes:
keyVersion/api/payment/webhook)Request-Time for webhooks, not Response-TimeDebug Steps:
// Log the exact sign content
log.Printf("Sign Content:\n%s", signContent)
// Verify it matches this format:
// POST /api/payment/webhook
// <Client-Id>.<Request-Time>.<JSON_BODY>
The signature header has spaces after commas. Make sure to trim spaces:
part = strings.TrimSpace(part) // Important!
if strings.HasPrefix(part, "signature=") {
return strings.TrimPrefix(part, "signature=")
}
Possible Causes:
Verification:
# Check if it's a valid public key
openssl rsa -pubin -in superqi_public_key.pem -text -noout
This is normal. RSA signatures include random padding (PKCS#1 v1.5), so the same content will produce different signatures each time. This doesn't affect verification.
Signature validation is essential for secure Super Qi integration:
POST <path>\n<Client-Id>.<Request-Time>.<Body>