Skip to content

Signing & Auth (RSA2)

The platform uses asymmetric signing: you sign with your own private key and the platform verifies with your public key — the platform stores only your public key. Callbacks go the other way: the platform signs with its private key and you verify with the platform public key (see collection callbacks / payout callbacks).

Common parameters

Every request must carry these common parameters (signed together with business parameters):

FieldDescription
merchant_noMerchant number
app_idApplication ID
key_versionYour public-key version (supports smooth rotation)
timestampUnix seconds; must be within 5 minutes of server time
nonceRandom string ≥16 chars, unique within the validity window
sign_typeFixed RSA2 (SHA256withRSA, RSA-2048, PKCS#1 v1.5)
signSignature, standard Base64, no line breaks

Signing algorithm (4 steps)

  1. Take all common + business parameters, excluding sign itself and any field whose value is null or an empty string (sign_type is included to prevent downgrade).
  2. Sort by parameter name in ASCII ascending order.
  3. Percent-encode each value per RFC3986, then join as k1=v1&k2=v2&… (keys themselves are not encoded). This is the message to sign (UTF-8 bytes).
  4. sign = Base64( RSA_SHA256_Sign(your private key, message) ). The platform verifies with your registered public key.

Percent-encoding must be RFC3986

Space → %20 (not +); A-Za-z0-9-_.~ stay unencoded; hex uppercase; over UTF-8 bytes. In Java do NOT use URLEncoder.encode (form encoding turns spaces into +); in PHP use rawurlencode; in Go do not use url.QueryEscape directly. All languages must produce byte-identical messages.

Signed values must be scalars

Values must be strings or integers only — never JSON objects/arrays; amounts are integers in minor units. Pass-through fields such as attach must be strings (serialize complex content to a JSON string yourself first).

Example:

text
message = amount=10000&app_id=app_10001&currency=PHP&key_version=1&mch_order_no=T20260528001
         &merchant_no=M100001&nonce=a1b2c3d4e5f6a1b2&product_code=QRPH&sign_type=RSA2&timestamp=1769990400
sign    = Base64( SHA256withRSA( merchant_private_key, message ) )

Key & encoding formats

  • Private key: PKCS#8 PEM (-----BEGIN PRIVATE KEY-----). Generate your own RSA-2048 key pair; the private key never leaves your server.
  • Public key: X.509/SPKI PEM (-----BEGIN PUBLIC KEY-----), registered with the platform (bound to app_id + key_version).
  • Signature output and key transport: standard Base64 (not URL-safe), no line breaks.

Generate keys:

bash
openssl genpkey -algorithm RSA -pkeyopt rsa_keygen_bits:2048 -out merchant_private.pem
openssl pkey -in merchant_private.pem -pubout -out merchant_public.pem

Security check order

Requests are checked in this order; any failure rejects:

  1. Rate limit (1007)
  2. All common parameters present
  3. Look up your public key by merchant_no / app_id / key_version
  4. Timestamp within 5 minutes of server time (1003)
  5. RSA signature verification (1002)
  6. Merchant / app / key status active (1006)
  7. Source IP in your allowlist; an empty allowlist rejects everything (1005)
  8. Anti-replay: (app_id, nonce) is consumed only after a valid signature (1004)