Webhooks
Receive real-time notifications when events happen in your account
How Webhooks Work
Configure
Register a webhook URL in your dashboard
Receive
MOR sends a POST request with event data
Verify
Validate the HMAC-SHA256 signature
Event Payload
Every webhook delivery sends a JSON object with id, type, created_at, and a data object specific to the event type.
{ "id": "evt_01HZ3ABC123DEF456", "type": "receipt.created", "created_at": "2026-03-26T14:30:00Z", "data": { "id": "rcp_789xyz", "fiscal_code": "MOR-2026-001234", "total": 745.00, "vat_amount": 97.17, "payment_method": "ETHQR", "device_id": "dev_456abc", "merchant_tin": "0012345678", "status": "completed" }}Event Types
Receipts
| Event | Description |
|---|---|
receipt.created | A new fiscal receipt was successfully issued. Receipt object with `fiscal_code`, `total`, `items`, `payment_method`. |
receipt.voided | A receipt was voided. A credit note was issued. Receipt object with `status: "voided"` and `void_reason`. |
Devices
| Event | Description |
|---|---|
device.activated | A VFD was activated and is ready to issue receipts. Device object with `status: "active"` and `public_key`. |
device.suspended | A VFD was suspended (regulatory or quota reason). Device object with `status: "suspended"` and `suspend_reason`. |
device.revoked | A VFD was permanently revoked. Device object with `status: "revoked"`. |
Payments
| Event | Description |
|---|---|
payment.completed | An ETHQR payment was confirmed by the bank. Payment object with `amount`, `bank_reference`, `settlement_date`. |
payment.failed | An ETHQR payment was rejected or timed out. Payment object with `failure_reason`. |
Billing
| Event | Description |
|---|---|
billing.payment_failed | Your subscription payment failed. Invoice object with `invoice_id`, `amount_due`, `next_retry_at`. |
Signature Verification
Every webhook includes an X-MOR-Signature header containing an HMAC-SHA256 signature. Always verify this before processing the event.
Python (Flask)
from flask import Flask, request, jsonifyimport hmac, hashlib, json
app = Flask(__name__)WEBHOOK_SECRET = "whsec_your_webhook_secret"
@app.route("/webhooks/mor", methods=["POST"])def handle_webhook(): # 1. Get the signature from headers signature = request.headers.get("X-MOR-Signature") timestamp = request.headers.get("X-MOR-Timestamp")
if not signature or not timestamp: return jsonify({"error": "Missing signature"}), 401
# 2. Construct the signed payload payload = f"{timestamp}.{request.get_data(as_text=True)}"
# 3. Compute HMAC-SHA256 expected = hmac.new( WEBHOOK_SECRET.encode(), payload.encode(), hashlib.sha256, ).hexdigest()
# 4. Compare signatures (timing-safe) if not hmac.compare_digest(f"sha256={expected}", signature): return jsonify({"error": "Invalid signature"}), 401
# 5. Parse and handle the event event = request.get_json()
match event["type"]: case "receipt.created": handle_receipt(event["data"]) case "payment.completed": handle_payment(event["data"]) case "device.suspended": alert_ops_team(event["data"])
return jsonify({"received": True}), 200JavaScript (Express)
import { MorClient } from '@mor-api/sdk';import express from 'express';
const app = express();const WEBHOOK_SECRET = process.env.MOR_WEBHOOK_SECRET;
app.post('/webhooks/mor', express.raw({ type: 'application/json' }), (req, res) => { const signature = req.headers['x-mor-signature']; const timestamp = req.headers['x-mor-timestamp'];
// Verify signature using the SDK helper const isValid = MorClient.webhooks.verify( req.body, signature, timestamp, WEBHOOK_SECRET );
if (!isValid) { return res.status(401).json({ error: 'Invalid signature' }); }
const event = JSON.parse(req.body);
switch (event.type) { case 'receipt.created': console.log(`Receipt ${event.data.fiscal_code} created`); break; case 'payment.completed': console.log(`Payment ${event.data.bank_reference} confirmed`); break; case 'device.suspended': console.log(`Device ${event.data.serial_number} suspended`); break; }
res.json({ received: true });});Best Practices
Return 200 quickly
Respond within 5 seconds. Process heavy work asynchronously.
Handle duplicates
Use the event `id` for idempotency — the same event may be delivered more than once.
Verify signatures
Always validate the HMAC signature before trusting the payload.
Use HTTPS
Webhook URLs must use HTTPS. HTTP endpoints will be rejected.
Monitor failures
MOR retries failed deliveries with exponential backoff (1 min, 5 min, 30 min, 2 hrs, 24 hrs).