Skip to main content

Using Webhooks and API's

A guide for developers and administrators on how to use webhooks to send real-time Mero event data to external applications.

Updated over 2 weeks ago

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:

Verifying Webhook Signatures

All webhook deliveries include standard Svix signature headers for verification:

Header

Description

svix-id

Unique message ID

svix-timestamp

Unix timestamp of the attempt

svix-signature

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:

  1. A dispatch job runs every hour and checks each organization that has webhooks enabled.

  2. For each organization, it compares the current hour (in the organization's timezone) against the configured delivery_hour (0–23).

  3. If the current hour matches delivery_hour and the reports haven't already been sent today, it dispatches the enabled report jobs.

  4. Each report job aggregates data for the previous calendar day (based on the organization's timezone) — covering all shifts whose starts_at fell on that date.

  5. The system records last_executed_at to 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

id

string (UUID)

Entity identifier

name

string

Human-readable name

Shift Reference

Field

Type

Description

id

string (UUID)

Shift identifier

name

string

Shift name

started_at

string (ISO 8601)

Shift start time

ended_at

string (ISO 8601)

Shift end time

completion_percent

number

Completion % (only in shift.finalized)

Visit Record

Field

Type

Description

cleaner

object {id, name}

Cleaner who made the visit

location

object {id, name}

Location visited

floor

object {id, name}

Floor of the location

started_at

string (ISO 8601)

Visit start time

ended_at

string (ISO 8601)

Visit end time

duration_minutes

integer

Duration in minutes

visit_type

string

"VERIFIED" or "TRANSIENT" (only in cleaner.visit_log)


Quick Reference

Event Type

Category

Trigger

Frequency

notification.traffic_alert

Notification

Traffic exceeds threshold

Real-time

notification.consumable_empty

Notification

Consumable sensor empty

Real-time

notification.consumable_full

Notification

Consumable sensor refilled

Real-time

notification.sensor_offline

Notification

Sensor goes offline

Real-time

notification.basestation_offline

Notification

Basestation goes offline

Real-time

shift.first_visit

Shift

First verified visit in shift

Real-time

shift.ended

Shift

Shift time window closes

Real-time

shift.finalized

Shift

Shift completion calculated

~60 min after shift end

cleaner.visit_log

Cleaner

Periodic visit batch

Every 60 minutes

report.daily_report

Report

Daily at delivery_hour

Once per day (previous day's data)

report.time_worked

Report

Daily at delivery_hour

Once per day (previous day's data)

report.cleaning_report

Report

Daily at delivery_hour

Once per day (previous day's data)

Did this answer your question?