Overview
Mero Webhook Platform v2 is a unified webhook infrastructure that consolidates real-time notifications, shift lifecycle events, cleaner visit reporting, and scheduled daily reports into a single platform. All webhooks are delivered via Svix with:
Standardized entity IDs (location, floor, building)
HMAC signature verification
Automatic retry with exponential backoff
Self-service endpoint management via the Svix App Portal
While this documentation is meant as a guide, more details about individual endpoints and webhooks are available in the Mero portal (Settings > Webhooks)
Getting Started
Managing Your Endpoints
All webhook endpoint management is done through the Svix App Portal — a self-service UI where you can:
Add and remove endpoint URLs
Subscribe to specific event types
Browse the Event Catalog to see available events, payload schemas, and example payloads
View delivery logs and retry failed deliveries
Manage signing secrets
You can access the app portal by logging into the Mero instance and going to Settings > Webhooks. If this is not available for you, Webhooks may need to be enabled for your plan. Please contact Mero support.
For detailed instructions, see the Svix documentation:
Event Catalog — browse available event types and their schemas
Adding Endpoints — configure your webhook URLs
Testing Events — send test payloads to your endpoints
Filtering Logs — troubleshoot deliveries
Replaying Messages — re-send failed deliveries
Verifying Webhook Signatures
All webhook deliveries include standard Svix signature headers for verification:
Header | Description |
| Unique message ID |
| Unix timestamp of the attempt |
| HMAC-SHA256 signature(s) |
Use the Svix verification libraries to validate incoming webhooks.
Delivery Guarantees
At-least-once delivery
Automatic retries with exponential backoff on failure (5xx, timeouts)
Full delivery logs and replay available in the App Portal
Webhook Event Types
Notification Events (Real-time)
These fire immediately when the triggering condition occurs.
notification.traffic_alert
What it does:
Notifies you when foot traffic at a specific location has exceeded a configured threshold since the last time a cleaner visited that location. This is the primary signal to dispatch a cleaner to a high-traffic area before conditions deteriorate.
When it fires:
In real-time, the moment the occupancy sensor count crosses the threshold. The threshold is configured per-location in Mero. The counter resets each time a cleaner visit is recorded at that location (including transient visits).
Use cases:
Trigger automated work orders, send alerts to supervisors, or feed into a facilities dashboard to prioritize cleaning in busy areas.
Example payload:
{ "timestamp": "2026-01-14T15:45:30.000000Z", "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Demo Location 42" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Demo Floor 3" }, "building": { "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Demo Building 2" }, "traffic_count": 156, "threshold": 75, "last_cleaned_at": "2026-01-14T13:45:30.000000Z", "message": "More than 75 visitors since last cleaning" }notification.consumable_empty
What it does:
Notifies you when a consumable dispenser (paper towel, toilet paper, soap, etc.) has been detected as empty by its sensor. This means the supply needs immediate replenishment.
When it fires:
In real-time, as soon as the consumable sensor reports an empty reading. Each sensor monitors a specific dispenser at a specific placement position (e.g., "Left" paper towel dispenser in a restroom).
Use cases:
Trigger restocking alerts, create work orders, or track consumable usage patterns over time.
Example payload:
{ "timestamp": "2026-01-14T15:45:30.000000Z", "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Demo Location 17" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Demo Floor 2" }, "building": { "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Demo Building 1" }, "placement": "Left", "sensor_type": "PAPER_TOWEL", "sensor_id": "AB12CD34" }notification.consumable_full
What it does:
Notifies you when a consumable dispenser has been refilled. The sensor detects that the supply level has returned to full. This is the counterpart to notification.consumable_empty and confirms that restocking has been completed.
When it fires:
In real-time, as soon as the consumable sensor detects a refill. The payload structure is identical to notification.consumable_empty.
Use cases:
Confirm that a restocking work order has been fulfilled, track refill response times, or close out open alerts.
Example payload:
{ "timestamp": "2026-01-14T16:00:00.000000Z", "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Demo Location 17" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Demo Floor 2" }, "building": { "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Demo Building 1" }, "placement": "Right", "sensor_type": "TOILET_PAPER", "sensor_id": "EF56GH78" }Payload fields are identical to notification.consumable_empty.
notification.sensor_offline
What it does:
Notifies you when any sensor in your deployment has stopped communicating. This covers all sensor types: occupancy counters, consumable dispensers (paper towel, toilet paper), and presence sensors. An offline sensor means that location is no longer being monitored until the issue is resolved.
When it fires:
In real-time, when the system's device state monitor detects that a sensor has not reported within its expected heartbeat window. The last_seen_at field tells you when the sensor last communicated.
Use cases:
Trigger maintenance tickets, alert facility managers to connectivity issues, or flag gaps in monitoring coverage.
Example payload:
{ "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Demo Location 42" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Demo Floor 3" }, "building": { "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Demo Building 2" }, "sensor_type": "OCCUPANCY", "sensor_id": "XY98ZW76", "last_seen_at": "2026-01-14T15:40:30.000000Z", "placement": "Left" }notification.basestation_offline
What it does:
Notifies you when a basestation (the hub device that receives signals from beacons and sensors) has stopped communicating. A basestation going offline typically means that all sensors and beacons connected through it will also stop reporting data.
When it fires:
In real-time, when the system's basestation state monitor detects that the basestation has not reported within its expected heartbeat window.
Use cases:
Trigger urgent maintenance tickets, alert IT teams to network or power issues, identify locations that have lost monitoring coverage.
Example payload:
{ "location_id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "building_id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "serial_number": "BS001A3F2" }Shift Events (Real-time)
These events track the lifecycle of cleaning shifts.
shift.first_visit
What it does:
Notifies you the moment a cleaner physically begins work on a shift. A shift being scheduled does not mean it has started — this event fires only when the first verified cleaner visit is recorded at one of the shift's assigned locations.
When it fires:
In real-time, when the system records the first valid, verified cleaner visit against any of the shift's task locations during the shift's time window. Only fires once per shift.
Use cases:
Confirm that scheduled cleaning has begun, track shift start punctuality, trigger SLA timers, or update a live operations dashboard.
Example payload:
{ "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "shift": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "started_at": "2026-01-14T06:00:00.000000Z", "ended_at": "2026-01-14T14:00:00.000000Z", "name": "Morning Shift" }, "assignee_type": "cleaner", "cleaners": [{ "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Alice Johnson" }], "buildings": [{ "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Demo Building 2" }], "first_visit": { "cleaner": { "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Alice Johnson" }, "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Demo Restroom 14" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Demo Floor 3" }, "started_at": "2026-01-14T06:12:30.000000Z" } }shift.ended
What it does:
Notifies you that a shift's scheduled time window has closed. This is a pure lifecycle signal — it tells you the shift period is over, but does not include visit data, task completion, or performance metrics. Those come later in shift.finalized.
When it fires:
In real-time, when the shift end cron detects that the current time has passed the shift's ended_at time. This fires regardless of whether any cleaning occurred.
Use cases:
Mark the shift as "ended" in your system, start a countdown for the finalized report, or trigger follow-up workflows for shifts where no shift.first_visit was received (meaning the shift may have been missed).
Example payload:
{ "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "shift": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Morning Shift", "started_at": "2026-01-14T06:00:00.000000Z", "ended_at": "2026-01-14T14:00:00.000000Z" }, "assignee_type": "CLEANER", "cleaners": [{ "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Alice Johnson" }], "buildings": [{ "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Demo Building 2" }] }shift.finalized
What it does:
Delivers the complete, definitive results of a shift after all data has settled. This includes every cleaner visit recorded during the shift, the completion status of every task in the shift's scope, and the overall shift completion percentage. This is the event to use for performance reporting and SLA tracking.
When it fires:
Approximately 60+ minutes after the shift ends. The system waits for visit data to settle (late-arriving sensor pings), then runs a completion calculation job.
Why the delay?
Beacon data can arrive with some lag. The system gives a buffer period for all visit data to be recorded and processed before calculating final results, ensuring accuracy.
Use cases:
Generate shift performance reports, calculate SLA compliance, compare expected vs. actual cleaning time, feed into billing systems, or sync task completion to external CMMS/IWMS platforms.
Example payload:
{ "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "shift": { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Morning Shift", "started_at": "2026-01-14T06:00:00.000000Z", "ended_at": "2026-01-14T14:00:00.000000Z", "completion_percent": 85.5 }, "assignee_type": "CLEANER", "cleaners": [{ "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Alice Johnson" }], "buildings": [{ "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Demo Building 2" }], "tasks": [ { "id": "d4e5f6a7-b8c9-0123-4567-890abcdef012", "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Main Restroom (Central)" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Floor 1" }, "state": "COMPLETE_VERIFIED", "completed_at": "2026-01-14T12:00:00.000000Z", "expected_duration": 20, "reality_duration": 18 }, { "id": "e5f6a7b8-c9d0-1234-5678-90abcdef0123", "location": { "id": "c3d4e5f6-7890-abcd-ef12-345678901234", "name": "North Restroom" }, "floor": { "id": "d4e5f6a7-5e6f-7890-abcd-ef1234567890", "name": "Floor 2" }, "state": "NOT_COMPLETE", "completed_at": null, "expected_duration": 15, "reality_duration": null } ], "visits": [ { "cleaner": { "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Alice Johnson" }, "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Main Restroom (Central)" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Floor 1" }, "started_at": "2026-01-14T11:00:00.000000Z", "ended_at": "2026-01-14T11:20:00.000000Z", "duration_minutes": 20 } ], "total_visits": 5, "total_time_minutes": 245 }Cleaner Events (Scheduled)
cleaner.visit_log
What it does:
Delivers a batch of all cleaner visits from the past 60 minutes for a given cleaner. Unlike shift events, this is independent of shifts — it fires for any cleaner who had visits in the window, whether or not they were on a scheduled shift. Each cleaner with activity gets their own separate webhook delivery.
When it fires:
Every 60 minutes on a cron schedule. One webhook is sent per cleaner per organization that had activity in the window.
Use cases:
Build a near-real-time feed of all cleaning activity, power live dashboards that aren't tied to shift schedules, track ad-hoc or unscheduled cleaning work, or sync visit data to external systems on a regular cadence.
Example payload:
{ "organization_id": "f47ac10b-58cc-4372-a567-0e02b2c3d479", "window": { "from": "2026-01-14T14:00:00.000000Z", "to": "2026-01-14T15:00:00.000000Z" }, "cleaner": { "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Alice Johnson" }, "total_visits": 4, "total_time_minutes": 65, "visits": [ { "location": { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Main Restroom (Central)" }, "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Floor 1" }, "started_at": "2026-01-14T14:15:00.000000Z", "ended_at": "2026-01-14T14:35:00.000000Z", "duration_minutes": 20, "visit_type": "VERIFIED" } ] }Report Events (Scheduled Daily)
These webhooks are dispatched once per day and contain data for the previous day.
How scheduling works:
A dispatch job runs every hour and checks each organization that has webhooks enabled.
For each organization, it compares the current hour (in the organization's timezone) against the configured
delivery_hour(0–23).If the current hour matches
delivery_hourand the reports haven't already been sent today, it dispatches the enabled report jobs.Each report job aggregates data for the previous calendar day (based on the organization's timezone) — covering all shifts whose
starts_atfell on that date.The system records
last_executed_atto prevent duplicate deliveries if the dispatch job runs multiple times in the same hour.
Example:
If an organization's delivery_hour is set to 18 and their timezone is America/Toronto, the reports will fire at 6:00 PM ET and contain all shift data from the previous day.
Only report types that are individually enabled (daily_report_enabled, total_time_worked_enabled, cleaning_report_enabled) will be sent.
An organization's delivery hour is set to 18 by default, but if you need it to change, please contact Support.
report.daily_report
What it does:
Delivers a comprehensive daily performance report for the previous day. For each shift that ran, it breaks down every assigned location showing: how long the cleaner was expected to spend there, how long they actually spent, the variance between the two, and which cleaners visited.
When it fires:
Once per day at the organization's configured delivery_hour. Reports on all shifts from the previous calendar day.
Use cases:
Daily cleaning compliance reviews, variance analysis, cleaner accountability, and client-facing SLA reports.
Example payload:
[ { "id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "name": "Morning Shift", "campus": { "id": "e7f8a9b0-c1d2-3456-7890-abcdef012345", "name": "Demo Campus" }, "locations": [ { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Demo Restroom 14", "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Demo Floor 3" }, "expected_minutes": 45, "variance_message": "5 minutes less than expected.", "in_assigned_area": true, "cleaners": [ { "id": "c1d2e3f4-a5b6-7890-cdef-123456789012", "name": "Alice Johnson", "actual_minutes": 40, "visit_count": 2 } ] } ] } ]report.time_worked
What it does:
Delivers a simple summary of how much time each cleaner spent working in each building on the previous day. Aggregated from all valid cleaner visits — grouped by building and cleaner. Unlike the daily report, this is not tied to specific shifts or task schedules.
When it fires:
Once per day at the organization's configured delivery_hour.
Use cases:
Payroll verification, labor cost allocation per building, contractor billing, and workforce utilization analysis.
Example payload:
[ { "building": "Demo Building 2", "cleaner": "Demo Cleaner Alice Johnson", "total_minutes": 245 } ]report.cleaning_report
What it does:
Delivers a scope-of-work completion summary for the previous day, broken down by location category (e.g., "Restrooms", "General", "Kitchen"). For each category, it shows how many locations were assigned vs. how many had all their tasks completed. A location counts as "cleaned" only if all tasks assigned to it were completed.
When it fires:
Once per day at the organization's configured delivery_hour.
Use cases:
Scope-of-work compliance tracking, client reporting by area type, identifying which categories of spaces are consistently under-serviced, and contract SLA reporting.
Example payload:
[ { "building": "Demo Building 2", "floor": "Demo Floor 3", "category": "General", "total_locations": 12, "locations_cleaned": 10, "percent_cleaned": 83.33 } ]API's
These endpoints are authenticated with the X-Webhook-Secret header — your organization's webhook secret key.
Location Lookup
Bulk lookup endpoint for resolving location IDs to full entity details. Useful for mapping IDs from webhook payloads to location metadata.
GET /api/webhook/v1/locations?ids[]=<uuid1>&ids[]=<uuid2>
Headers:
X-Webhook-Secret: your-secret-key-here
Example request:
curl -X GET \ "https://api.mero.co/api/webhook/v1/locations?ids[]=b2c3d4e5-6f78-90ab-cdef-123456789012" \ -H "X-Webhook-Secret: your-secret-key-here"
Example response:
{ "data": [ { "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Main Restroom (Central)", "floor": { "id": "a1b2c3d4-5e6f-7890-abcd-ef1234567890", "name": "Floor 1" }, "building": { "id": "9a289236-0650-4e1c-8ca3-aa9be2362299", "name": "Building A" } } ] }All requested IDs must belong to the authenticated organization. Returns 403 if any ID belongs to a different organization, 422 if IDs are missing or invalid.
Test Webhook
Send a test webhook with a generated fake payload to validate your endpoint.
POST /api/webhook/v1/test
Headers:
X-Webhook-Secret: your-secret-key-here Content-Type: application/json
Body:
{ "event_type": "shift.finalized", "endpoint_url": "https://your-server.com/webhooks/mero" }Valid event_type values:
notification.basestation_offline, notification.consumable_empty, notification.consumable_full, notification.sensor_offline, notification.traffic_alert, shift.first_visit, shift.ended, shift.finalized, cleaner.visit_log, report.daily_report, report.time_worked, report.cleaning_report
Response:
{ "message": "Test webhook triggered successfully." }Entity ID Conventions
All entity references in webhook payloads use UUIDs and follow a consistent {id, name} pattern:
{ "id": "b2c3d4e5-6f78-90ab-cdef-123456789012", "name": "Main Restroom (Central)" }Use the Location Lookup API to resolve location IDs to full entity details including associated sensors, networks, and device information.
Common Object Schemas
Entity Reference
Used for locations, floors, buildings, cleaners, and campuses:
Field | Type | Description |
| string (UUID) | Entity identifier |
| string | Human-readable name |
Shift Reference
Field | Type | Description |
| string (UUID) | Shift identifier |
| string | Shift name |
| string (ISO 8601) | Shift start time |
| string (ISO 8601) | Shift end time |
| number | Completion % (only in shift.finalized) |
Visit Record
Field | Type | Description |
| object {id, name} | Cleaner who made the visit |
| object {id, name} | Location visited |
| object {id, name} | Floor of the location |
| string (ISO 8601) | Visit start time |
| string (ISO 8601) | Visit end time |
| integer | Duration in minutes |
| string | "VERIFIED" or "TRANSIENT" (only in cleaner.visit_log) |
Quick Reference
Event Type | Category | Trigger | Frequency |
| Notification | Traffic exceeds threshold | Real-time |
| Notification | Consumable sensor empty | Real-time |
| Notification | Consumable sensor refilled | Real-time |
| Notification | Sensor goes offline | Real-time |
| Notification | Basestation goes offline | Real-time |
| Shift | First verified visit in shift | Real-time |
| Shift | Shift time window closes | Real-time |
| Shift | Shift completion calculated | ~60 min after shift end |
| Cleaner | Periodic visit batch | Every 60 minutes |
| Report | Daily at delivery_hour | Once per day (previous day's data) |
| Report | Daily at delivery_hour | Once per day (previous day's data) |
| Report | Daily at delivery_hour | Once per day (previous day's data) |
Further reading: Svix App Portal documentation
