You receive signed POST notifications when something happens in your store — a paid order, a new contact, a confirmed booking. Wire them to Zapier, Make, or your own server.
Verify the signature
- The signature header carries the timestamp and the v1 HMAC:
t=<unix>,v1=<hex>. - The signed message is
{timestamp}.{rawBody} — the exact bytes received, with no extra whitespace. - Reject requests whose timestamp is outside the tolerance shown below (replay protection).
- After you rotate the secret, the old secret keeps signing for 24 hours. In that window the header carries two
v1 values; accept the request if either one matches.
Use the raw request body — never re-serialize JSON.
Node.js
import crypto from 'node:crypto';
const SECRET = process.env.MIA_WEBHOOK_SECRET!; // whsec_… from Mia dashboard
const TOLERANCE_SEC = 300;
export function verifyMiaWebhook(rawBody: string, signatureHeader: string | null) {
if (!signatureHeader) return { ok: false, error: 'missing_header' };
// During a secret rotation the header carries one v1 per valid secret.
let timestamp: number | null = null;
const signatures: string[] = [];
for (const part of signatureHeader.split(',')) {
const [k, v] = part.trim().split('=');
if (k === 't') timestamp = Number(v);
if (k === 'v1' && v) signatures.push(v);
}
if (!Number.isFinite(timestamp!) || signatures.length === 0) return { ok: false, error: 'invalid_header' };
const age = Math.abs(Math.floor(Date.now() / 1000) - timestamp!);
if (age > TOLERANCE_SEC) return { ok: false, error: 'timestamp_outside_tolerance' };
const expected = crypto
.createHmac('sha256', SECRET)
.update(`${timestamp}.${rawBody}`)
.digest('hex');
const a = Buffer.from(expected, 'utf8');
const matched = signatures.some((sig) => {
const b = Buffer.from(sig, 'utf8');
return a.length === b.length && crypto.timingSafeEqual(a, b);
});
if (!matched) return { ok: false, error: 'signature_mismatch' };
return { ok: true, body: JSON.parse(rawBody) };
}
// Express: app.post('/hooks/mia', express.raw({ type: 'application/json' }), (req, res) => {
// const result = verifyMiaWebhook(req.body.toString('utf8'), req.get('X-Mia-Signature'));
// if (!result.ok) return res.status(400).send(result.error);
// res.sendStatus(200);
// });Python
import hashlib
import hmac
import json
import os
import time
SECRET = os.environ["MIA_WEBHOOK_SECRET"].encode("utf-8") # whsec_… from Mia dashboard
TOLERANCE_SEC = 300
HEADER = "X-Mia-Signature"
def verify_mia_webhook(raw_body: bytes, signature_header: str | None):
if not signature_header:
return False, "missing_header"
# During a secret rotation the header carries one v1 per valid secret.
timestamp = None
signatures = []
for part in signature_header.split(","):
key, _, value = part.strip().partition("=")
if key == "t":
timestamp = int(value)
if key == "v1" and value:
signatures.append(value)
if timestamp is None or not signatures:
return False, "invalid_header"
if abs(int(time.time()) - timestamp) > TOLERANCE_SEC:
return False, "timestamp_outside_tolerance"
signed = f"{timestamp}.".encode("utf-8") + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
if not any(hmac.compare_digest(expected, sig) for sig in signatures):
return False, "signature_mismatch"
return True, json.loads(raw_body)
# Flask example:
# @app.post("/hooks/mia")
# def mia_hook():
# raw = request.get_data()
# ok, payload = verify_mia_webhook(raw, request.headers.get(HEADER))
# if not ok:
# return payload, 400
# return "", 200PHP
<?php
declare(strict_types=1);
$secret = getenv('MIA_WEBHOOK_SECRET'); // whsec_… from Mia dashboard
$toleranceSec = 300;
function verify_mia_webhook(string $rawBody, ?string $header): array {
global $secret, $toleranceSec;
if ($header === null || $header === '') {
return ['ok' => false, 'error' => 'missing_header'];
}
// During a secret rotation the header carries one v1 per valid secret.
$timestamp = null;
$signatures = [];
foreach (explode(',', $header) as $part) {
[$k, $v] = array_pad(explode('=', trim($part), 2), 2, null);
if ($k === 't') $timestamp = (int) $v;
if ($k === 'v1' && $v !== null && $v !== '') $signatures[] = $v;
}
if ($timestamp === null || count($signatures) === 0) {
return ['ok' => false, 'error' => 'invalid_header'];
}
if (abs(time() - $timestamp) > $toleranceSec) {
return ['ok' => false, 'error' => 'timestamp_outside_tolerance'];
}
$expected = hash_hmac('sha256', $timestamp . '.' . $rawBody, $secret);
$matched = false;
foreach ($signatures as $sig) {
if (hash_equals($expected, $sig)) $matched = true;
}
if (!$matched) {
return ['ok' => false, 'error' => 'signature_mismatch'];
}
return ['ok' => true, 'body' => json_decode($rawBody, true, 512, JSON_THROW_ON_ERROR)];
}
// Read php://input once; header: $_SERVER['HTTP_X_MIA_SIGNATURE'] ?? nullEvents and sample payloads
order.completed
Order completed — A creator sale was paid. Emitted once when the order row is inserted.
| Field | Type | Personal data | Description |
|---|
| order_id | uuid | No | Order id |
| product_id | uuid | No | Product id |
| product_title | string | No | Product title at purchase |
| amount_cents | integer | No | Charge amount in minor units (bani or cents) |
| currency | enum | No | ISO 4217 currency |
| status | enum | No | Order status |
| buyer_email | email | null | Yes | Buyer email. Null when checkout collected none. |
Sample payload
{
"id": "6c9e923c-6547-5a9c-bfbe-7d83e5e19b54",
"type": "order.completed",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"order_id": "22222222-2222-4222-8222-222222222222",
"product_id": "33333333-3333-4333-8333-333333333333",
"product_title": "Ghid de start",
"amount_cents": 4500,
"currency": "RON",
"status": "paid",
"buyer_email": "buyer@example.com"
}
}order.refunded
Order refunded — A paid order was refunded in full. Emitted once on the paid → refunded transition.
| Field | Type | Personal data | Description |
|---|
| order_id | uuid | No | Order id |
| product_id | uuid | No | Product id |
| product_title | string | No | Product title at purchase |
| amount_cents | integer | No | Charge amount in minor units (bani or cents) |
| currency | enum | No | ISO 4217 currency |
| status | enum | No | Order status |
| buyer_email | email | null | Yes | Buyer email. Null when checkout collected none. |
Sample payload
{
"id": "2578302e-094d-579b-a553-6af647838d02",
"type": "order.refunded",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"order_id": "22222222-2222-4222-8222-222222222222",
"product_id": "33333333-3333-4333-8333-333333333333",
"product_title": "Ghid de start",
"amount_cents": 4500,
"currency": "RON",
"status": "refunded",
"buyer_email": "buyer@example.com"
}
}contact.created
Contact created — A contact was first stored for this creator. Updates do not emit again.
| Field | Type | Personal data | Description |
|---|
| contact_id | uuid | No | Contact id |
| email | email | Yes | Contact email |
| name | string | null | Yes | Contact name. Null when unknown. |
| source | enum | No | How the contact was captured |
| locale | enum | No | Contact locale |
Sample payload
{
"id": "1121e58f-1f8a-54f0-9ad9-3174aa5eb3e8",
"type": "contact.created",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"contact_id": "44444444-4444-4444-8444-444444444444",
"email": "buyer@example.com",
"name": "Sample Buyer",
"source": "lead_magnet",
"locale": "ro"
}
}contact.unsubscribed
Contact unsubscribed — A contact unsubscribed. Emitted once when unsubscribed_at is set.
| Field | Type | Personal data | Description |
|---|
| contact_id | uuid | No | Contact id |
| email | email | Yes | Contact email, so the receiver can suppress them |
| unsubscribed_at | datetime | No | When the unsubscribe was stored, ISO-8601 |
Sample payload
{
"id": "a94e5322-2d16-5a73-b452-58dfd5e234ae",
"type": "contact.unsubscribed",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"contact_id": "44444444-4444-4444-8444-444444444444",
"email": "buyer@example.com",
"unsubscribed_at": "2026-11-03T12:00:00.000Z"
}
}membership.started
Membership started — A member subscription became active. One event per subscription id.
| Field | Type | Personal data | Description |
|---|
| subscription_id | uuid | No | Mia member subscription id |
| membership_id | uuid | No | Membership id |
| product_id | uuid | No | Product id |
| interval | enum | No | Billing interval |
| status | enum | No | Subscription status |
| buyer_email | email | null | Yes | Member email. Null when checkout had no address. |
| current_period_end | datetime | null | No | End of the current paid period, ISO-8601 |
Sample payload
{
"id": "fa277bf8-37fa-5470-8634-631284f9e8f1",
"type": "membership.started",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"subscription_id": "55555555-5555-4555-8555-555555555555",
"membership_id": "66666666-6666-4666-8666-666666666666",
"product_id": "33333333-3333-4333-8333-333333333333",
"interval": "monthly",
"status": "active",
"buyer_email": "member@example.com",
"current_period_end": "2026-12-03T12:00:00.000Z"
}
}membership.cancelled
Membership cancelled — A member subscription was canceled. Period-end scheduling does not emit this.
| Field | Type | Personal data | Description |
|---|
| subscription_id | uuid | No | Mia member subscription id |
| membership_id | uuid | No | Membership id |
| product_id | uuid | No | Product id |
| interval | enum | No | Billing interval |
| status | enum | No | Subscription status |
| buyer_email | email | null | Yes | Member email. Null when checkout had no address. |
| current_period_end | datetime | null | No | End of the current paid period, ISO-8601 |
Sample payload
{
"id": "fccac8ad-d4fa-5a9c-8999-eac07199cee0",
"type": "membership.cancelled",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"subscription_id": "55555555-5555-4555-8555-555555555555",
"membership_id": "66666666-6666-4666-8666-666666666666",
"product_id": "33333333-3333-4333-8333-333333333333",
"interval": "monthly",
"status": "canceled",
"buyer_email": "member@example.com",
"current_period_end": "2026-12-03T12:00:00.000Z"
}
}booking.confirmed
Booking confirmed — A held slot was confirmed after payment.
| Field | Type | Personal data | Description |
|---|
| booking_id | uuid | No | Booking id |
| product_id | uuid | No | Booking product id |
| order_id | uuid | null | No | Paid order id. Null when the booking has no order yet. |
| starts_at | datetime | No | Slot start, ISO-8601 |
| ends_at | datetime | No | Slot end, ISO-8601 |
| timezone | string | No | Buyer timezone (IANA) |
| status | enum | No | Booking status |
| buyer_email | email | null | Yes | Buyer email. Null when unknown. |
Sample payload
{
"id": "14bed3db-2b9e-56ba-8f50-113ba3e20bac",
"type": "booking.confirmed",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"booking_id": "77777777-7777-4777-8777-777777777777",
"product_id": "33333333-3333-4333-8333-333333333333",
"order_id": "22222222-2222-4222-8222-222222222222",
"starts_at": "2026-11-10T09:00:00.000Z",
"ends_at": "2026-11-10T10:00:00.000Z",
"timezone": "Europe/Bucharest",
"status": "confirmed",
"buyer_email": "buyer@example.com"
}
}booking.cancelled
Booking cancelled — A pending or confirmed booking was cancelled.
| Field | Type | Personal data | Description |
|---|
| booking_id | uuid | No | Booking id |
| product_id | uuid | No | Booking product id |
| order_id | uuid | null | No | Paid order id. Null when the booking has no order yet. |
| starts_at | datetime | No | Slot start, ISO-8601 |
| ends_at | datetime | No | Slot end, ISO-8601 |
| timezone | string | No | Buyer timezone (IANA) |
| status | enum | No | Booking status |
| buyer_email | email | null | Yes | Buyer email. Null when unknown. |
Sample payload
{
"id": "49171d11-fe43-579d-ab90-e7af37e265a4",
"type": "booking.cancelled",
"api_version": "2026-11-03",
"created_at": "2026-11-03T12:00:00.000Z",
"store_id": "11111111-1111-4111-8111-111111111111",
"data": {
"booking_id": "77777777-7777-4777-8777-777777777777",
"product_id": "33333333-3333-4333-8333-333333333333",
"order_id": "22222222-2222-4222-8222-222222222222",
"starts_at": "2026-11-10T09:00:00.000Z",
"ends_at": "2026-11-10T10:00:00.000Z",
"timezone": "Europe/Bucharest",
"status": "cancelled",
"buyer_email": "buyer@example.com"
}
}If anything in this guide is unclear, email support@mia.store.