MyMeter — Event webhooks

Event webhooks

A workspace can have this platform POST a signed JSON notification to a URL of yours whenever something happens, so your system does not have to poll the API to find out. Subscriptions are configured under Settings → Event webhooks.

The event catalog

Each subscription chooses which of these it wants. The value is what arrives in the envelope and in the X-MyMeter-Event header.

Event Fires when
meter.created A meter was added, by hand or by auto-provisioning from an incoming report.
meter.status_changed A device reported a different set of status flags than it had before.
alert.opened An alert rule opened a new alert for a meter.
alert.resolved An open alert was resolved, either automatically or by a member.
reading.rejected One delivery of readings was partly refused. Sent once per delivery with counts, never once per reading.
import.completed An import of historical readings finished or failed.
integration.transfer_failed A scheduled transfer to an external portal failed.

Why there is no event per reading

Readings are by far the highest-volume object here — a quarter-hourly fleet of 500 meters produces about 48,000 a day — and one request per reading would make your endpoint, not ours, the capacity limit of the whole ingestion path. So readings are not subscribable individually. Rejections are reported instead, aggregated: one delivery per incoming batch, carrying exact counts by reason and a bounded sample of the meters affected. Nothing is dropped to achieve that — the counts are complete.

What a delivery looks like

A POST with a JSON body. Every event uses the same envelope; only data differs.

POST /your/endpoint HTTP/1.1
Content-Type: application/json
X-MyMeter-Signature: t=1787961600,v1=ca65efb0dd0c21333afae93f8b50ef1973ada79a93492db8106aafa3575e97a9
X-MyMeter-Event: meter.created
X-MyMeter-Delivery: 9b1c1a2e-0f3a-4a5b-8c7d-6e5f4a3b2c1d

{"id":"9b1c1a2e-0f3a-4a5b-8c7d-6e5f4a3b2c1d","event":"meter.created","occurred_at":"2026-08-28T12:00:00+00:00","workspace":{"id":42},"data":{"meter":{"id":1234,"identifier":"WATER-CELLAR","name":"Cellar water","unit":"m3","group_id":7}}}

Answer quickly

Reply 2xx as soon as you have the body; do your own work afterwards. Anything else is treated as a failure and retried four times with a growing delay, and a target that keeps refusing is switched off — with an email to the workspace owners, never silently.

Deduplicate on the delivery id

Retries are at-least-once, so the same event can arrive more than once. X-MyMeter-Delivery is stable across every retry of one event — use it as your idempotency key. A manual retry from the delivery log reuses it too, so re-sending a delivery will never look to you like the event happening twice.

Verifying the signature

The header carries a timestamp and an HMAC-SHA256 digest, taken over the timestamp, a dot, and the raw request body:

X-MyMeter-Signature: t=<unix timestamp>,v1=<hex digest>

signed_payload = "{t}." + raw_request_body
v1             = hex(hmac_sha256(secret, signed_payload))

Check the timestamp as well as the digest. The digest alone proves the body came from us but stays valid for ever, so a captured request could be replayed at any later moment. Because the timestamp is inside the signed string it cannot be altered — rejecting an old one is what makes a replay fail. Five minutes either way is a reasonable window; allow both directions, because your clock can be ahead of ours.

Hash the bytes you received. Do not parse the JSON and re-encode it before verifying — key order and escaping can differ, and the digest is over the exact bytes.

Worked example

These values are produced by the same code that signs real deliveries, so you can check your implementation against them exactly.

secret         = whsec_example_0123456789abcdef0123456789abcdef
t              = 1787961600
signed_payload = 1787961600.{"id":"9b1c1a2e-0f3a-4a5b-8c7d-6e5f4a3b2c1d","event":"meter.created","occurred_at":"2026-08-28T12:00:00+00:00","workspace":{"id":42},"data":{"meter":{"id":1234,"identifier":"WATER-CELLAR","name":"Cellar water","unit":"m3","group_id":7}}}

X-MyMeter-Signature: t=1787961600,v1=ca65efb0dd0c21333afae93f8b50ef1973ada79a93492db8106aafa3575e97a9

Example verifier

import hashlib, hmac, time

def verify(secret: str, body: bytes, header: str, tolerance: int = 300) -> bool:
    parts = dict(p.split("=", 1) for p in header.split(",") if "=" in p)
    t, v1 = parts.get("t"), parts.get("v1")
    if not t or not v1 or not t.isdigit():
        return False
    # The replay check. Without it a captured delivery verifies for ever.
    if abs(int(time.time()) - int(t)) > tolerance:
        return False
    expected = hmac.new(
        secret.encode(),
        t.encode() + b"." + body,   # the RAW body, not re-encoded JSON
        hashlib.sha256,
    ).hexdigest()
    return hmac.compare_digest(expected, v1)

If the secret leaks

Generate a new one under Settings → Event webhooks. The old secret stops verifying immediately, so update your side first or plan for a short gap.