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.