Webhooks

Receive real-time notifications when events happen in your account

How Webhooks Work

1

Configure

Register a webhook URL in your dashboard

2

Receive

MOR sends a POST request with event data

3

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.

Example: receipt.created
{
"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

EventDescription
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

EventDescription
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

EventDescription
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

EventDescription
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)

Python
from flask import Flask, request, jsonify
import 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}), 200

JavaScript (Express)

JavaScript
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).