Skip to main content

HTML form signature

The customer can change any form field right in the browser—for example, set a lower amount. That is why the invoice fields travel in a signed container: the service recomputes the signature and, if it does not match, does not create the invoice.

The store's server computes the signature. Only the finished data and signature pair goes to the browser.

What is signed

data = base64url(json)
signature = HMAC-SHA256(data, formSecret)

The HMAC is computed over the data string, not over the JSON bytes. The receiver verifies the signature against the submitted string as is, and only then decodes it.

It follows that there is no canonicalization: key order, JSON indentation, and Unicode escaping style do not affect the signature. Any standard JSON encoder works.

Reference set

Plug these values into your code and compare the output.

The example secret:

dvc3khto5WpjEPojZfYJsbJzFEPPVnRa

The amount and payment deadline as numbers; strings are accepted too:

{"projectId":"a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d","amountFiat":10.5,"currencyFiat":"USD","timeToPay":1,"description":"Order #7, delivery","serviceData":"order-7"}

The data string is base64url of those bytes. The padding is stripped here; the receiver also accepts it with padding:

eyJwcm9qZWN0SWQiOiJhMWIyYzNkNC01ZTZmLTRhN2ItOGM5ZC0wZTFmMmEzYjRjNWQiLCJhbW91bnRGaWF0IjoxMC41LCJjdXJyZW5jeUZpYXQiOiJVU0QiLCJ0aW1lVG9QYXkiOjEsImRlc2NyaXB0aW9uIjoiT3JkZXIgIzcsIGRlbGl2ZXJ5Iiwic2VydmljZURhdGEiOiJvcmRlci03In0

The signature of this string with the secret above:

09628020c47400c9231420450af642adfdb3c246f33e3e12cb8e28f96c2a785e

With these values, the examples below print exactly this data and signature. The same JSON with the "output":"errors" key at the end produces a different data string and a different signature.

If data does not match while the JSON looks the same, the JSON encoder is the cause: a different key order or spaces after separators. The receiver will accept such a container, but it will not match the reference verbatim.

Examples

<?php

function signedContainer(array $fields, string $formSecret): array {
$data = rtrim(strtr(base64_encode(json_encode($fields)), '+/', '-_'), '=');

return [
'data' => $data,
'signature' => hash_hmac('sha256', $data, $formSecret),
];
}

// Test values from the reference set. Before going live,
// replace the secret with the form secret from your project settings
$container = signedContainer([
'projectId' => 'a1b2c3d4-5e6f-4a7b-8c9d-0e1f2a3b4c5d',
'amountFiat' => 10.5,
'currencyFiat' => 'USD',
'timeToPay' => 1,
'description' => 'Order #7, delivery',
'serviceData' => 'order-7',
], 'dvc3khto5WpjEPojZfYJsbJzFEPPVnRa');

echo $container['data'], "\n";
echo $container['signature'], "\n";

The example values are ASCII, but the code works with any Unicode unchanged. PHP and Python escape non-ASCII as \uXXXX by default, JavaScript sends raw UTF-8—the receiver accepts both.

Two common mistakes

The JSON is signed instead of the container. The HMAC is computed over the data string, that is, over the base64url. The mistake looks like “everything matches, but the signature does not.”

The container is rebuilt after signing. If even one byte of data changes, the signature is invalid. Build the pair in one place in your code.

If the signature does not match

Walk the chain JSON → data → signature; the first link that diverges from the reference is the cause. Check data first: it does not depend on the secret.