Docs · API reference

Signature verification

Every E-comm callback from miaPOS carries an RSA signature over the payment result. Verify it before you fulfil the order. This page is the authoritative algorithm — Java, Python, PHP and Node.js reference implementations.

AudienceDeveloper
DifficultyIntermediate
Updated2026-07-07

Overview

After a payment reaches a terminal state (SUCCESS, FAILED, refunded), the miaPOS E-comm service POSTs a JSON body to the callbackUrl the merchant supplied on POST /ecomm/api/v1/pay. The body has two top-level fields:

  • result — the payment outcome object (terminal id, order id, payment id, status, amount, currency, payment date, etc.).
  • signature — an SHA256withRSA signature, produced by miaPOS with its private key over a canonical string built from the result object. Base64-encoded.

Verification is a merchant-side responsibility. Anyone can POST to your callbackUrl; the signature is the only proof the payload came from miaPOS. Do not rely on IP allow-listing — rely on this verification.

Algorithm

  1. Parse the JSON body. Extract the result object and the signature string.
  2. Sort the fields of result alphabetically by key.
  3. Concatenate the values (in that sorted order) using ; as separator. This is the canonical string.
  4. Base64-decode signature. Fetch the public key from GET /api/v1/public-key, decode it from Base64 into an RSA key.
  5. Verify: SHA256withRSA (a.k.a. RSASSA-PKCS1-v1_5 with SHA-256) with the public key, over the UTF-8 bytes of the canonical string, against the decoded signature bytes.

The canonical string is deterministic. Given the example payload below, the string prepared for verification is:

145.25;MDL;2024-05-20T16:32:28+03:00;order123;bc340d13-7411-4785-a083-b594b1384eb5;SUCCESS;swift123;SomeBank;123456
Important. The signed string is built from the result values only — keys are used only for sorting and are not included. Fields with numeric or boolean values are stringified with their native toString(). If miaPOS adds a new field to the result object in the future, it automatically joins the canonical string in its alphabetical position — treat every field of result as signed, whether you use it or not.

Callback payload envelope

{
  "result": {
    "terminalId":     "123456",
    "orderId":        "order123",
    "paymentId":      "bc340d13-7411-4785-a083-b594b1384eb5",
    "status":         "SUCCESS",
    "amount":         145.25,
    "currency":       "MDL",
    "paymentDate":    "2024-05-20T16:32:28+03:00",
    "swiftMessageId": "swift123",
    "swiftPayerBank": "SomeBank"
  },
  "signature": "base64_encoded_signature"
}

Fetch the public key

GET/api/v1/public-key
{
  "publicKey": "MIIBIjANBgkqhkiG9w0BAQEFAAOCAQ8AMIIBCgKCAQEAr1f/yhw+UI//z3KdpnJz..."
}

The publicKey field is a Base64-encoded X.509 SubjectPublicKeyInfo (the DER inside the standard PEM -----BEGIN PUBLIC KEY----- block). Cache it on your side; re-fetch after any signature verification failure — the key may have rotated.

Verify (Java)

import com.fasterxml.jackson.databind.ObjectMapper;

import java.security.KeyFactory;
import java.security.PublicKey;
import java.security.Signature;
import java.security.spec.X509EncodedKeySpec;
import java.util.Base64;
import java.util.Map;
import java.util.TreeMap;
import java.util.stream.Collectors;

public class SignatureVerification {

    /**
     * @param jsonString   raw callback body — {"result": {...}, "signature": "..."}
     * @param publicKeyPEM Base64-encoded RSA public key from GET /api/v1/public-key
     */
    public static boolean verifySignature(String jsonString, String publicKeyPEM) {
        try {
            ObjectMapper objectMapper = new ObjectMapper();
            Map<String, Object> jsonMap = objectMapper.readValue(jsonString, Map.class);
            Map<String, Object> resultMap = (Map<String, Object>) jsonMap.get("result");
            String base64Signature = (String) jsonMap.get("signature");

            // Sort keys alphabetically, join VALUES with ";"
            Map<String, Object> sortedMap = new TreeMap<>(resultMap);
            String signatureString = sortedMap.values().stream()
                    .map(Object::toString)
                    .collect(Collectors.joining(";"));

            byte[] decodedKey = Base64.getDecoder().decode(publicKeyPEM);
            X509EncodedKeySpec keySpec = new X509EncodedKeySpec(decodedKey);
            KeyFactory keyFactory = KeyFactory.getInstance("RSA");
            PublicKey publicKey = keyFactory.generatePublic(keySpec);

            Signature signature = Signature.getInstance("SHA256withRSA");
            signature.initVerify(publicKey);
            signature.update(signatureString.getBytes());

            byte[] signatureBytes = Base64.getDecoder().decode(base64Signature);
            return signature.verify(signatureBytes);
        } catch (Exception e) {
            e.printStackTrace();
            return false;
        }
    }
}

Verify (Python)

import json
import base64
from cryptography.hazmat.primitives import hashes, serialization
from cryptography.hazmat.primitives.asymmetric import padding


def verify_signature(json_string: str, public_key_base64: str) -> bool:
    try:
        json_data = json.loads(json_string)
        result = json_data["result"]
        signature_base64 = json_data["signature"]

        # Sort by keys, join VALUES with ";"
        sorted_items = sorted(result.items())
        signature_string = ";".join(str(value) for _, value in sorted_items)

        pem = (
            "-----BEGIN PUBLIC KEY-----\n"
            f"{public_key_base64}\n"
            "-----END PUBLIC KEY-----"
        ).encode()
        public_key = serialization.load_pem_public_key(pem)

        signature = base64.b64decode(signature_base64)
        public_key.verify(
            signature,
            signature_string.encode(),
            padding.PKCS1v15(),
            hashes.SHA256(),
        )
        return True
    except Exception as e:
        print(f"Signature verification failed: {e}")
        return False

Verify (PHP)

<?php
function verifyMiaPosSignature(string $jsonBody, string $publicKeyBase64): bool
{
    $payload = json_decode($jsonBody, true);
    if (!is_array($payload) || !isset($payload['result'], $payload['signature'])) {
        return false;
    }

    // Sort by keys, join VALUES with ";"
    ksort($payload['result']);
    $signatureString = implode(';', array_map(
        static fn($v) => is_bool($v) ? ($v ? 'true' : 'false') : (string)$v,
        $payload['result']
    ));

    $pem = "-----BEGIN PUBLIC KEY-----\n"
         . chunk_split($publicKeyBase64, 64, "\n")
         . "-----END PUBLIC KEY-----\n";
    $publicKey = openssl_pkey_get_public($pem);
    if ($publicKey === false) {
        return false;
    }

    $signatureBytes = base64_decode($payload['signature'], true);
    if ($signatureBytes === false) {
        return false;
    }

    return openssl_verify(
        $signatureString,
        $signatureBytes,
        $publicKey,
        OPENSSL_ALGO_SHA256
    ) === 1;
}

Verify (Node.js)

import crypto from 'node:crypto';

/**
 * @param {string} jsonBody          Raw callback body
 * @param {string} publicKeyBase64   Value of publicKey from GET /api/v1/public-key
 */
export function verifyMiaPosSignature(jsonBody, publicKeyBase64) {
  const payload = JSON.parse(jsonBody);
  const result = payload.result;
  const signatureB64 = payload.signature;
  if (!result || !signatureB64) return false;

  // Sort by keys, join VALUES with ";"
  const signatureString = Object.keys(result)
    .sort()
    .map((k) => String(result[k]))
    .join(';');

  const pem =
    '-----BEGIN PUBLIC KEY-----\n' +
    publicKeyBase64.match(/.{1,64}/g).join('\n') +
    '\n-----END PUBLIC KEY-----\n';

  const verifier = crypto.createVerify('RSA-SHA256');
  verifier.update(signatureString, 'utf8');
  verifier.end();
  return verifier.verify(pem, signatureB64, 'base64');
}

Common pitfalls

  • Wrong input. The signed message is not the raw request body — it is the semicolon-joined values of the sorted result map. Signing the raw body will always fail.
  • Number formatting. Values are stringified with the runtime's default number formatting. Java Object::toString, Python str(), PHP (string) and JavaScript String() all produce the same result for the canonical result fields; still, if you re-serialize the JSON first, do not force locale-dependent formatting on floats.
  • Encoding. The bytes passed to the verifier must be UTF-8. Every value in result is currently ASCII, but do not assume it — treat the canonical string as UTF-8.
  • Signature transport. The signature is in the JSON body, not in an HTTP header. Do not look for X-Signature or similar headers.
  • Failed verification → refetch key, retry once. A verification failure usually means the signing key rotated. Re-fetch GET /api/v1/public-key, verify again with the fresh key; only reject after the second failure.

Key rotation

miaPOS may rotate the signing key at any time — routinely, and immediately on any suspected exposure. Cache the public key on your side, and re-fetch on the first verification failure before deciding the callback is invalid. There is no scheduled rotation calendar; treat rotation as always possible.