Put an address on my site where the till sends a message after every sale,
and check that the message really comes from our till.
A MplusKASSA webhook is a message that the till sends to your server, for example after every completed receipt. Such a receiver runs fine on ordinary web hosting with PHP. The most important work is in one check: does this message really come from your till? This article walks through the steps, with code we use ourselves on our test till.
Why you should prefer a non-blocking webhook, you can read in a webhook that can block your till.
1. An address on your server
You need an HTTPS address that accepts POST requests, for example https://yourdomain.com/hook. Mplus appends the name of the event: a completed receipt arrives at /hook/completeSession. So your receiver must handle everything under /hook/, or have a path of its own per event.
Make sure the certificate is valid and stays valid. With a blocking setting, a webhook to an address that does not answer is a till that cannot check out.
2. The webhook in the back office
In the Mplus back office you go to Beheer > Instellingen > Webhooks (management > settings > webhooks). There you fill in:
- the address from step 1;
- a secret key (base64), which you also keep on your server;
- the events you want to receive, for example
completeSession; - whether the webhook is blocking. Start with non-blocking.
Through the Mplus API you can read out the configured webhooks, but not create or change them. So setting up always goes through the back office.
3. Verifying the signature
Every call has the X-Mplus-Signature header. That is an HMAC-SHA256 over the raw body of the message, with your secret key after base64 decoding as the key, and the result in base64 again.
In PHP:
<?php
$key = base64_decode(getenv('MPLUS_WEBHOOK_SECRET'));
$body = file_get_contents('php://input');
$expected = base64_encode(hash_hmac('sha256', $body, $key, true));
$received = $_SERVER['HTTP_X_MPLUS_SIGNATURE'] ?? '';
$valid = $received !== '' && hash_equals($expected, $received);
In Python:
import base64, hashlib, hmac
def signature(body: bytes, secret_b64: str) -> str:
key = base64.b64decode(secret_b64)
return base64.b64encode(hmac.new(key, body, hashlib.sha256).digest()).decode()
valid = hmac.compare_digest(received, signature(raw_body, SECRET))
Three things that often go wrong:
- Not decoding the key. The key is base64. If you use the text as it appears in the back office, the signature never matches.
- Calculating over the JSON instead of the raw body. If you first parse the message and then turn it back into text, the order or the formatting changes and the signature no longer matches. Always calculate over the bytes as they came in.
- Just using
==. Compare withhash_equalsorcompare_digest, so the comparison gives away nothing about how far a forged signature matched.
4. Answering, and signing your response
Answer with status 200 and JSON. For most events an empty object {} is enough. You sign your response in the same way, with the X-Mplus-Signature header:
<?php
$response = '{}';
header('Content-Type: application/json');
header('X-Mplus-Signature: ' . base64_encode(hash_hmac('sha256', $response, $key, true)));
echo $response;
With completeSession, a blocking webhook may also return a text for the bottom of the receipt, in the receiptFooter field. With a non-blocking webhook the till does not wait for your response and such a text does nothing.
5. Store first, then process
Store the message, answer straight away, and process it afterwards. That keeps your response time short, even if the processing (updating stock in your accounting, sending a notification) takes a while.
With a wrong or missing signature you do not process the message. You do answer, so the till is not bothered by it. Keep such messages apart, then you can see if someone tries to abuse your address.
Testing
Check out a receipt at the till and see whether the message comes in. With us it arrived within a second. Check in your log whether the signature was right and whether the lines you expect are in it. Watch for fields that are missing: an item without an own item code or barcode does not have those fields in the message.
Common mistakes
Starting with blocking. Always set a new webhook to non-blocking first. Only when everything has run stably for months and your response to the till adds something can you consider blocking.
Putting the key in your code. Keep it in an environment variable or a file outside your web folder, not in the code that sits in your repository.
Accepting every path. Only accept the events you have switched on, and refuse the rest with a short 404.
What you can do with a connected till is on connecting MplusKASSA.