Docs

This is a push integration: you register an HTTPS endpoint, we deliver signed JSON to it. There is no polling/pull API in v1 — you receive alerts as they happen.

1. Setup

Open Settings → Notification channels in the dashboard.

  1. Add a Webhook channel and paste your HTTPS receiver URL.

  2. We generate a signing secret and show it once — copy it now; it is never displayed again (rotate it later if you lose it).

  3. Click Send test to deliver a synthetic payload and confirm your receiver parses + verifies it before any real alert fires.

Requirements for your URL:

  • Must be https:// (plain http is rejected — payloads carry account cost data and must not cross the wire in clear text).

  • Should be a stable URL: we send with allow_redirects=false, so a 3xx is treated as a failure, not followed (this prevents us from being bounced into an internal endpoint).

2. The request

We POST a JSON body to your URL with these headers:

Header

Example

Meaning

Content-Type

application/json

Body is JSON, UTF-8.

User-Agent

watchmy.cloud-notifier/1

Identifies our notifier.

X-Watchmycloud-Api-Version

v1

Contract version (see §6). Mirrors schema_version in the body.

X-Watchmycloud-Timestamp

1748674800

Unix seconds when we signed the request. Use it to reject replays.

X-Watchmycloud-Signature

v1=9f86d08...

HMAC-SHA256 signature (see §4).

Body — schema_version: 1

Request body

Copy lines below

{
  "schema_version": 1,
  "alert_event_id": "5f0c8b2e-1c3d-4a5b-9e7f-2a1b3c4d5e6f",
  "channel_id": "a1b2c3d4-5e6f-7a8b-9c0d-1e2f3a4b5c6d",
  "alert": {
    "rule_type": "daily_anomaly",
    "current_value": "142.50",
    "threshold_value": "100.00",
    "currency": "USD",
    "triggered_at": "2026-05-31T07:00:00Z",
    "message": "Daily spend on account Prod is 42% above the 7-day average.",
    "account": {
      "id": "123456789012",
      "label": "Prod"
    }
  }
}

Field reference

Field

Type

Notes

schema_version

integer

Contract major version. 1 today.

alert_event_id

string (UUID)

Unique per triggered alert. Use it for idempotency — retries reuse the same value.

channel_id

string (UUID)

The webhook channel that received this delivery.

alert.rule_type

string

Stable machine code of the rule. One of: daily_anomaly, hourly_spike, daily_threshold, absolute_cap, percent_increase. Branch on this, not on message.

alert.current_value

string

The measured value that tripped the rule. Sent as a string to preserve exact decimals (no float rounding). Parse as a decimal.

alert.threshold_value

string

The configured threshold, same units. String for the same reason.

alert.currency

string

ISO-4217 code. USD today.

alert.triggered_at

string

ISO-8601 UTC (…Z) timestamp of when the alert fired.

alert.message

string

Human-readable, localised to the account owner's language. For display only — do not parse.

alert.account.id

string

The cloud account identifier that tripped the rule (AWS Account ID today).

alert.account.label

string

Display name of that account, or the id if no label was set.

Stability promise: within schema_version: 1 we only ever add fields. Treat unknown fields as forward-compatible and ignore them. We never remove or retype a field without bumping to schema_version: 2 (see §6).

3. How you should respond

Your response

What we do

2xx

Treated as acknowledged. Done.

4xx

Permanent failure — we do not retry (auth/validation is on you). The error surfaces in your Notification History.

5xx, timeout, connection error

Transient — we retry with backoff.

3xx

Treated as failure (we do not follow redirects).

Return 2xx fast. We use a 10-second timeout. If your processing is slow, return 202 immediately and do the work asynchronously on your side.

Retry schedule

Backoff is 1m → 5m → 30m → 2h (4 retries after the first try), up to 5 attempts total / ~2h36m, then the delivery is marked failed and stops. Every attempt reuses the same alert_event_id, so deduplicate on it.

4. Verifying the signature

We sign "<timestamp>.<raw-body>" with HMAC-SHA256 keyed on your channel's signing secret. The header value is v1=<lowercase hex digest>. The format mirrors Stripe/Svix, so off-the-shelf verifiers adapt with one line.

Always verify before trusting a payload, and reject stale timestamps to defeat replays (we recommend a ±5-minute tolerance).

Python

Python

Copy code

import hashlib, hmac, time

def verify(secret: str, headers: dict, raw_body: bytes, tolerance=300) -> bool:
    ts = headers["X-Watchmycloud-Timestamp"]
    sig = headers["X-Watchmycloud-Signature"]          # "v1=<hex>"
    if abs(time.time() - int(ts)) > tolerance:
        return False                                    # replay window exceeded
    signed = f"{ts}.".encode() + raw_body
    expected = "v1=" + hmac.new(secret.encode(), signed, hashlib.sha256).hexdigest()
    return hmac.compare_digest(expected, sig)           # constant-time

Node.js

Node.js

Copy code

const crypto = require("crypto");

function verify(secret, headers, rawBody, tolerance = 300) {
  const ts = headers["x-watchmycloud-timestamp"];
  const sig = headers["x-watchmycloud-signature"];      // "v1=<hex>"
  if (Math.abs(Date.now() / 1000 - Number(ts)) > tolerance) return false;
  const signed = Buffer.concat([Buffer.from(`${ts}.`), rawBody]);
  const expected =
    "v1=" + crypto.createHmac("sha256", secret).update(signed).digest("hex");
  return crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sig));
}

Verify against the raw request body bytes, before any JSON re-encoding — re-serialising changes whitespace/key order and breaks the HMAC.

Rotating the secret

If your secret leaks, rotate it from the channel's settings (or POST /api/v1/notification-channels/{id}/rotate-secret). The new secret is shown once; deliveries sign with it immediately, so update your receiver in the same change window.

5. Security model

HTTPS only, enforced at channel creation.

  • No redirects followed (allow_redirects=false) — publish a stable URL.

  • We never echo your receiver's response body back into the dashboard or logs (only the status code + reason), so a misconfigured receiver can't be turned into a data-exfiltration channel.

  • Egress is from our notifier worker, not from an untrusted browser — so we do not block private-IP / localhost receiver URLs. You can legitimately point a webhook at internal infra reached over VPN or a tunnel (ngrok, etc.).

6. Versioning policy

Two coordinated axes, one number to track:

  • Body: schema_version (integer) + header X-Watchmycloud-Api-Version.

  • Our URLs: the developer API and machine spec are path-versioned (/api/v1/…). The human-readable docs live at /docs/api and always describe the current contract.

Rule: the API path version equals the body schema_version. /api/v1/…"schema_version": 1.

What bumps the version: only breaking changes — removing a field, renaming one, or changing its type. Those ship as schema_version: 2 under a new /api/v2/… path, and v1 keeps working through a published deprecation window.

What does NOT bump it: purely additive changes (new optional fields). Build your receiver to ignore unknown fields and you stay compatible across all minor evolutions of a major version.

The receiver URL itself is yours — we cannot encode our contract version in it. That is exactly why the version travels in the body (schema_version) and the X-Watchmycloud-Api-Version header instead.

Changelog


  • v1 (2026-05) — initial contract: schema_version 1, HMAC-SHA256 signatures, the alert object documented above.

watchmy.cloud
A smoke detector for your AWS bill. Built by engineers who got tired of cost surprises.

watchmy.cloud
A smoke detector for your AWS bill. Built by engineers who got tired of cost surprises.

watchmy.cloud
A smoke detector for your AWS bill. Built by engineers who got tired of cost surprises.