Building
Webhooks
For a POS that runs in the cloud: we POST every order event to your HTTPS address as it happens, signed so you can prove it came from us.
Delivery
The shop owner sets the webhook address in the Business app and receives a webhook secret (whsec_…), shown once, to enter in your POS. Each event is a POST with this body:
Body
{ "event_id": 1042, "type": "order.confirmed", "created_at": "2026-11-03T10:15:22+03:00", "order": { ... } }
The order is the same object the event feed returns.
Headers
| Header | Contains |
|---|---|
X-Nunua-Event | The event type, for example order.confirmed. |
X-Nunua-Event-Id | A unique id. De-duplicate on it. |
X-Nunua-Timestamp | Unix seconds when it was sent. |
X-Nunua-Signature | sha256= followed by the hex HMAC-SHA256 of "{timestamp}.{raw body}", keyed with the shop's webhook secret. |
Verifying the signature
Verify against the raw request body, before any JSON parsing, and reject timestamps older than a few minutes so a captured request can't be replayed.
const crypto = require('crypto');
// Verify a Nunua Pro webhook. `rawBody` must be the RAW request body (a Buffer or string),
// not JSON.parse'd and re-stringified. (Express: express.raw({ type: 'application/json' }).)
function verifyNunuaWebhook(rawBody, timestamp, signatureHeader, secret, tolerance = 300) {
// Reject old requests so a captured one cannot be replayed later.
if (Math.abs(Date.now() / 1000 - Number(timestamp)) > tolerance) return false;
const expected = 'sha256=' + crypto.createHmac('sha256', secret).update(`${timestamp}.`).update(rawBody).digest('hex');
const a = Buffer.from(expected);
const b = Buffer.from(signatureHeader || '');
return a.length === b.length && crypto.timingSafeEqual(a, b);
}
module.exports = { verifyNunuaWebhook };
// In your endpoint:
// const ok = verifyNunuaWebhook(req.body, req.get('X-Nunua-Timestamp'), req.get('X-Nunua-Signature'), process.env.NUNUA_WEBHOOK_SECRET);
// if (!ok) return res.sendStatus(401);
// ...record the sale, then answer 2xx quickly.import hashlib
import hmac
import time
def verify_nunua_webhook(raw_body: bytes, timestamp: str, signature_header: str, secret: str, tolerance: int = 300) -> bool:
"""Verify a Nunua Pro webhook. Pass the RAW request body (bytes), not a re-encoded dict."""
# Reject old requests so a captured one cannot be replayed later.
if abs(time.time() - int(timestamp)) > tolerance:
return False
signed = timestamp.encode() + b"." + raw_body
expected = "sha256=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, signature_header)
# In your endpoint (Flask example):
# ok = verify_nunua_webhook(
# request.get_data(),
# request.headers.get("X-Nunua-Timestamp", ""),
# request.headers.get("X-Nunua-Signature", ""),
# os.environ["NUNUA_WEBHOOK_SECRET"],
# )
# if not ok: abort(401)
# ...record the sale, then answer 2xx quickly.<?php
// Verify a Nunua Pro webhook. Use the RAW request body (do not decode and re-encode it).
function verifyNunuaWebhook(string $rawBody, string $timestamp, string $signatureHeader, string $secret, int $tolerance = 300): bool
{
// Reject old requests so a captured one cannot be replayed later.
if (abs(time() - (int) $timestamp) > $tolerance) {
return false;
}
$expected = 'sha256=' . hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
return hash_equals($expected, $signatureHeader);
}
// In your endpoint:
// $ok = verifyNunuaWebhook(
// file_get_contents('php://input'),
// $_SERVER['HTTP_X_NUNUA_TIMESTAMP'] ?? '',
// $_SERVER['HTTP_X_NUNUA_SIGNATURE'] ?? '',
// getenv('NUNUA_WEBHOOK_SECRET'),
// );
// if (! $ok) { http_response_code(401); exit; }
// ...record the sale, then answer 2xx quickly.Retries and timeouts
- Answer with any
2xxwithin 10 seconds. If recording the sale is slow, accept the webhook and do the work in the background. - Anything else, or a timeout, is retried with growing delays for 24 hours. The event feed has every event regardless.
- Addresses must be
httpson a public host. Private networks are refused, and redirects aren't followed.