Getting started
Five minutes from zero to your first webhook. You'll create an API key, fetch your locations, post an order, then verify a webhook signature on your end.
1. Create an API key
Owners and admins create keys in the admin app: Settings → Integrations → API, Webhooks & AI apps → New API key. A key works across the whole business. Tick only the permissions the integration needs (e.g. orders:read, products:write), and optionally limit it to some locations and set an expiry. The full key (mavos_live_…) is shown once on creation — copy it then. Older pos_live_ keys keep working.
Customer details have their own permissions: names need customers:names, and email addresses and phone numbers need customers:contact. Without them those fields come back null.
export MAVOS_KEY=mavos_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx2. Your first request
List the locations the key has access to:
curl https://mavos.com.au/api/v1/locations \ -H "Authorization: Bearer $MAVOS_KEY"
Every response carries Mavos-Request-Id (quote it to support) and Mavos-Api-Version (currently 2026-10-06), plus X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset so you can pace bursts. Money is integer cents and timestamps are ISO 8601.
Pagination
Lists take ?limit= (1–200, default 50) and ?cursor=. Pass next_cursor back as cursor while has_more is true.
{
"data": [ { "id": "…", "name": "Main St" } ],
"next_cursor": "eyJpZCI6…",
"has_more": true
}Errors
Errors share one shape. Match on code; it never changes meaning.
{
"error": {
"code": "insufficient_scope",
"message": "Missing required permission: orders:write",
"request_id": "…",
"details": { "required": "orders:write" }
}
}- 401 —
unauthenticated,invalid_token,revoked_token,expired_token - 403 —
insufficient_scope,insufficient_role,location_forbidden,module_not_enabled,ai_forbidden,mfa_required,company_not_connected - 400 —
validation_failed,company_required(an AI app connection covering several businesses needs aMavos-Companyheader); 404 —not_found; 429 —rate_limited - 409 —
conflict,idempotency_conflict,idempotency_in_progress
3. Create an order
Writes require an Idempotency-Key header (1–255 characters) — a UUID is fine. Sending the same key with the same body within 24 hours replays the stored response with Idempotent-Replayed: true, so a network retry never creates a second order. The same key with a different body answers 409 idempotency_conflict. Prices and modifier deltas come from the database row; the API ignores client-supplied money.
curl https://mavos.com.au/api/v1/orders \
-X POST \
-H "Authorization: Bearer $MAVOS_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"location_id": "00000000-0000-0000-0000-000000000000",
"items": [
{ "product_id": "11111111-1111-1111-1111-111111111111", "quantity": 2 }
]
}'4. Subscribe to webhooks
There are 84 event kinds; GET /api/v1/event-kinds lists them with the permission each needs. Subscribe to exact kinds (order.paid), prefix wildcards (booking.*) or everything (*). pii_level sets how much customer detail deliveries carry: none (default), names or contact. The signing secret (whsec_…) is returned exactly once on create.
curl https://mavos.com.au/api/v1/webhooks \
-X POST \
-H "Authorization: Bearer $MAVOS_KEY" \
-H "Idempotency-Key: $(uuidgen)" \
-H "Content-Type: application/json" \
-d '{
"name": "stock-control",
"url": "https://stock.example.com/mavos-webhook",
"subscribed_events": ["order.paid", "order.refunded", "product.*"],
"pii_level": "none"
}'Each delivery is a POST with this envelope:
{
"id": "evt_123",
"type": "order.paid",
"api_version": "2026-10-06",
"created_at": "2026-10-06T01:23:45.000Z",
"company_id": "…",
"location_id": "…",
"resource": { "type": "order", "id": "…" },
"actor": { "kind": "user", "id": "…", "label": "…" },
"data": { … }
}Headers: Mavos-Signature, Mavos-Event-Id, Mavos-Event-Type, Mavos-Delivery-Id, Mavos-Api-Version and User-Agent: Mavos-Webhooks/2. Answer with a 2xx quickly. Delivery is at-least-once, so de-duplicate on Mavos-Event-Id. A failed delivery is retried with exponential backoff, starting at 1 minute and capped at 24 hours, for up to 12 attempts in all. Test on the endpoint in the admin (or POST /api/v1/webhooks/{id}/test) sends one webhook.test event to that endpoint only.
5. Verify the signature
Every delivery carries Mavos-Signature: t=<unix>,v1=<hex>. Each v1 is HMAC-SHA256 of <t>.<raw body> keyed with the signing secret. For 24 hours after you rotate the secret the header carries two v1 values (new and previous secret): accept the request if any of them matches. Compare in constant time, use the raw body exactly as received (not re-serialised JSON), and reject timestamps more than 5 minutes old.
Node.js
import { createHmac, timingSafeEqual } from "node:crypto";
const TOLERANCE_SECONDS = 5 * 60;
export function verifyMavosSignature(rawBody: string, header: string, secret: string): boolean {
let t = NaN;
const signatures: Buffer[] = [];
for (const part of header.split(",")) {
const [key, value = ""] = part.trim().split("=");
if (key === "t") t = Number(value);
else if (key === "v1" && /^[0-9a-f]{64}$/i.test(value)) signatures.push(Buffer.from(value, "hex"));
}
if (!Number.isInteger(t) || Math.abs(Date.now() / 1000 - t) > TOLERANCE_SECONDS) return false;
const expected = createHmac("sha256", secret).update(`${t}.${rawBody}`, "utf8").digest();
// Accept if ANY v1 matches: there are two during the 24 hours after a rotation.
return signatures.some((sig) => sig.length === expected.length && timingSafeEqual(sig, expected));
}Python
import hmac, hashlib, time
def verify_mavos_signature(raw_body: bytes, header: str, secret: str) -> bool:
t, signatures = None, []
for part in header.split(","):
key, _, value = part.strip().partition("=")
if key == "t" and value.isdigit():
t = int(value)
elif key == "v1":
signatures.append(value)
if t is None or abs(time.time() - t) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + raw_body, hashlib.sha256).hexdigest()
return any(hmac.compare_digest(expected, sig.lower()) for sig in signatures)What's next
- Browse the full API in the interactive docs.
- Connect Claude, ChatGPT or another AI app through the MCP server.
- Pull historical events via
GET /api/v1/events?since=...if a webhook is missed. - Send a failed delivery again from the endpoint's delivery list in the admin, or via
POST /api/v1/webhooks/{id}/deliveries/{deliveryId}/retry.