Skip to content

Request Signing

1. Authentication method

All API requests must use request-level digital signature authentication.

  • Algorithm: ECDSA (ES256, P-256 + SHA-256)
  • Private key generation:openssl ecparam -name prime256v1 -genkey -noout -out priKey.pem
  • Public key generation:openssl ec -in priKey.pem -pubout -out pubKey.pem

2. Required HTTP headers

All requests must include the following headers:

HeaderDescription
X-Merchant-IdMerchant CNPJ (digits only, without ".", ",", "-" or similar symbols)
X-TimestampRequest timestamp (Unix seconds)
X-NonceRandom string (anti-replay)
DigestRequest body digest
AuthorizationSignature information
Authorization.keyIdSigning key identifier used to locate the verification public key

3. Digest calculation rules

Digest includes the request body in the signed scope.

Format

Digest: SHA-256=<Base64(SHA256(body_bytes))>

Rules

  • POST / PUT / PATCH: digest the raw HTTP body bytes
  • GET / DELETE: digest an empty byte string
  • Do not reserialize JSON before computing Digest

4. Signing content (Canonical String)

The signature must cover exactly these 4 items, in fixed order:

  1. (request-target)
  2. x-timestamp
  3. x-nonce
  4. digest

(request-target) Format

(request-target): <http-method小写> <path>[?<规范化query>]

Example:

(request-target): post /v1/orders
(request-target): get /v1/orders?order_id=123

Canonical String formatting rules

  • Each line must follow this format:

    <字段名>:<单个空格><字段值>
  • Use \n (LF) line breaks

  • No extra spaces or blank lines are allowed

Example

(request-target): post /v1/orders
x-timestamp: 1738123456
x-nonce: a9f3c1d47e8b9a2c
digest: SHA-256=Base64DigestValue

5. Authorization Header

Format

Authorization: Signature keyId="<merchant_id>",alg="ES256",headers="(request-target) x-timestamp x-nonce digest",signature="<Base64签名>"

Description

  • Sign the Canonical String with the merchant private key
  • The platform verifies the signature with the corresponding public key

6. Anti-replay rules

The platform performs the following checks:

  • Timestamp check: difference between X-Timestamp and server time must be ≤ **300 seconds
  • Nonce check: (merchant_id, nonce) must not repeat within the validity window

7. Critical constraints (must follow)

  • (request-target) must be included in the signature and cannot be omitted
  • Header names must be lowercase in the signing string
  • There must be exactly one space after the colon (key: value)
  • Canonical String must match byte-for-byte
  • The signature in Authorization is not a standalone Base64 value
  • Correct verification requires keyId / alg / headers together
  • (request-target) must be included in the signature and cannot be omitted
  • Digest and Authorization must both be validated

8. Authorization generation flow

Authorization generation has 4 fixed steps.

Step 1:Prepare request body digest (Digest)

Example request body (raw JSON text actually sent):

json
{"order_no":"A10001","amount":100,"currency":"CNY"}

Generate Digest:

bodyBytes  = UTF8_BYTES('{"order_no":"A10001","amount":100,"currency":"CNY"}')
digestB64  = Base64(SHA256(bodyBytes))
Digest     = "SHA-256=" + digestB64

Step 2:Build Canonical Signing String

(request-target): post /v1/orders
x-timestamp: 1738123456
x-nonce: a9f3c1d47e8b9a2c
digest: SHA-256=Base64DigestValue

Canonical String must match byte-for-byte:

  • Exactly one space after the colon
  • Use \n (LF) line breaks
  • Field order is fixed

Step 3:Generate digital signature (ES256)

hashBytes   = SHA256(UTF8_BYTES(signingString))
signature   = ECDSA_SIGN_DER(privateKey, hashBytes)
signatureB64 = Base64(signature)

Step 4:Generate Authorization

Authorization =
  'Signature ' +
  'keyId="' + merchantId + '",' +
  'alg="ES256",' +
  'headers="(request-target) x-timestamp x-nonce digest",' +
  'signature="' + signatureB64 + '"'

9. Authorization verification flow

After receiving a request, verify using the following flow:

Step 1:Validate Digest

expectedDigest =
  "SHA-256=" + Base64(SHA256(HTTP_RAW_BODY_BYTES))

if Digest != expectedDigest:
    reject(DIGEST_MISMATCH)

Step 2:Rebuild Canonical Signing String

(request-target): post /v1/orders
x-timestamp: <X-Timestamp>
x-nonce: <X-Nonce>
digest: <Digest>

Step 3:Verify Authorization

publicKey  = LOOKUP_PUBLIC_KEY(keyId)
hashBytes  = SHA256(UTF8_BYTES(signingString))
ok = ECDSA_VERIFY_DER(publicKey, hashBytes, Base64Decode(signature))

if ok != true:
    reject(SIGNATURE_INVALID)