One Order Built in Python, JavaScript and by Hand Gives Three JSON Hashes and One Canon Hash
A checkout service written in Python signs each order before it hands it on. A fulfilment service written in JavaScript checks the signature before it ships anything. Both have the same order in memory. The question is whether they agree on its bytes.
We built one order three ways. Python filled a dict in the order its code reads, and json.dumps wrote it with its defaults: a space after every comma and colon, and “Zürich” escaped as Z\u00fcrich. JavaScript built the same object with its keys in another order, and JSON.stringify wrote it compactly with the ü left as it is. The third copy is a file written by hand, pretty-printed, with yet another key order, hashed with the bxm-canon command.

Same data, three plain JSON hashes. Python’s text was 226 bytes and JavaScript’s 193, so they were never going to match. Through Canon, all three came out as the same 166 bytes and one hash, c40aa605751c57bc for the first 16 hex digits.
Then the signature. Python signed with HMAC-SHA256 and a shared key, once over its JSON text and once over the Canon bytes. JavaScript rebuilt both from its own copy of the order and checked them.
| What Python signed | Checked in JavaScript |
|---|---|
| Its JSON text | failed |
| The Canon bytes | passed |
The JSON signature failed although nothing changed. That’s the bug people usually fix by sending the exact signed text alongside the data, and then hoping nobody re-serializes it on the way.
The one thing JavaScript has to do
JavaScript has a single number type, so 249.0 and 249 are the same number to it, and JSON.stringify writes 249. Canon keeps ints and floats apart, so 249 would hash as an int and the signatures would differ. The prices in this order are floats, so the JavaScript side writes them with a decimal point when it builds the Canon text. Python needed nothing: its float and int are already different types.
That’s a schema decision, and Canon makes you take it once instead of finding out at the signature check. Prices with cents, like 19.95, come through either way.
Run it yourself
The script is examples/case-studies/01-three-languages in the repository, and the recorded output is test/results/case-studies/01-three-languages.txt. It uses the released packages: the Python wheel, the JavaScript package and the command from the download page.
More examples are on the use cases page.