Skip to main content
CONNECT GUIDES

Send WeatherX alerts to your own server

For a place whose alerts have to reach a program, not a person. It needs somebody who writes code; every other destination we offer does not.

All connect guides

You give us an address on your own server; we send one signed message to it every time something happens at your place. Most of this page is for whoever writes that server. Everything on it is what we already send — there is nothing to negotiate.

Is this the one you want?

Only if somebody on your side writes code. Every other destination we offer — email, Slack, Discord, Telegram, an alert on your screen — is one press and needs nobody technical. Start at all connect guides if that is what you are after.

This one is worth the work when the alert has to reach a program rather than a person: to open a barrier, hold a crane, page whoever is on duty, or write a row in your own records. Instead of the sentence a person reads, your server gets the numbers behind it — how far away the storm is, which way it is coming from, how far ahead the forecast was.

To set it up you need two things from your side: an address on a server of yours that is reachable from the internet over https, and somebody to write what happens when a message arrives. A place can carry this and an ordinary destination at the same time, so the crew still gets a message they can read.

If that is what you want, hand the rest of this page to whoever will build it. Nothing below needs you.

How to stop everything, without asking anybody

Your WeatherX email carries a Stop link at the bottom. Open it, and press the button on the page it shows. It stops every alert for that place and turns off every destination on it, at any hour. It needs no key and no reply.

Write to support@weatherx.earth, and ask us to take one destination off and leave the rest running.

That is the whole job

If you have connected the destination and you know how to stop it, you are done. You can stop reading here.

Everything below this line is for a developer — somebody who makes calls to our API from a program. It is a different way to do some of the same things, and one thing more. Nobody needs it to receive alerts.

Everything here needs your API key

Send it as an X-API-Key header. It is the key from your welcome message, and it never travels in a connect link — the two are separate on purpose.

Send yourself a real alert, now

You do not have to wait for weather. One call sends a real alert through the real senders:

POST /portal/sites/<place id>/test
X-API-Key: wx_live_…

It arrives as light rain half an hour away, and it says on its face that it is a test — in the first line of the message — so nobody can mistake it for weather. One a minute, twenty a day.

It goes to every destination on that place, not only to the one this page is about. If the place already writes into your team's chat, your team sees the test too. Say so first, or test on a place that has only one destination on it.

Take one destination off, and leave the rest

The Stop link above turns off everything at once. This takes off one and leaves the place watched. Ask for the list, then delete the one you mean:

GET /portal/sites/<place id>/channels
DELETE /portal/channels/<id>
X-API-Key: wx_live_…

Everything else on that place carries on receiving.

Add your endpoint

There is no approval screen and no connect link for this one. You register the address:

POST /portal/sites/<place id>/channels/webhook
X-API-Key: wx_live_…
Content-Type: application/json

{"url": "https://alerts.example.com/weatherx"}

The answer carries the signing secret once. It is never readable again, so store it as you would any other credential. The address must be https on port 443, must resolve to a public host, and must not redirect: we check it again at the moment of every send, not only when you register it.

What we send

One POST, content-type: application/json, one message per event per place. The body:

{
  "id": "evt_01K3XQ0R7YB2N4V8HJ5TZCDEFG",
  "type": "started",
  "event": "lightning",
  "decision": "STOP",
  "site": {
    "name": "Acme depot",
    "lat": 48.14,
    "lon": 11.58
  },
  "intensity": {
    "category": "high",
    "value": 3.0,
    "unit": "tier"
  },
  "eta": "2026-08-24T14:30:00Z",
  "eta_in_minutes": 18,
  "lead_minutes": 20,
  "distance_km": 12,
  "from_direction": "SW",
  "observed": true,
  "reissued": false,
  "cycle": "20260824_1410",
  "region": "europe",
  "sent_at": "2026-08-24T14:12:03Z",
  "observed_at": "2026-08-24T14:10:00Z"
}
idOne identifier per message, 26 characters after the evt_. The same value comes back on a retry, so it is what you use to drop a duplicate.
typealert, started, updated, ended, or early_all_clear.
eventrain, lightning or hail.
decisionWhat a person should do: STOP, PREPARE, ALL_CLEAR, or RAIN_CLEARED and its kind when one weather type clears while another still runs.
siteThe place: the name you gave it, and its position.
intensitymm/h for rain; a tier number for lightning and hail. On a clear message the category is peak and the value is the strongest the event reached.
etaWhen it reaches the place, in UTC. null on a clear message.
eta_in_minutesThe same thing counted from the moment we sent, in whole minutes. Never negative: something already past arrives as 0.
lead_minutesHow far ahead the FORECAST was, counted from the cycle rather than from the moment the message left. Use this to score us, not eta_in_minutes.
distance_kmHow far away the nearest cell is, in whole kilometres. null on a clear message, and null where the detector could not measure it.
from_directionWhich way it comes FROM, as a compass name (SW). null on a clear message.
observedtrue when it is measured at the place now, false when it is still a forecast.
observed_atThe valid time of the observation this event started from. This is the exact answer to "how fresh is this"; the time inside cycle can be a cycle older.
reissuedtrue when this weather was cleared and came back inside half an hour. A person who read the all-clear needs telling.
cycleWhich ten-minute run this came from. Use it, not the clock, to order two messages.
regionWhich forecast region the place sits in: europe or us.
sent_atWhen we built the message, in UTC. Not when it reached you.
testPresent and true ONLY on a message a customer asked us to send to prove the destination works (POST /portal/sites/<id>/test). Its cycle is the word test. Treat it as proof of delivery, never as weather.

Two more fields, on a clear message only

When the weather has passed, the same shape carries two fields the others do not:

{
  "id": "evt_01K3XQ0R7YB2N4V8HJ5TZCDEFG",
  "type": "ended",
  "decision": "LIGHTNING_CLEARED",
  "intensity": {
    "category": "peak",
    "value": 0.0,
    "unit": "tier"
  },
  "clear_since": "2026-08-24T15:00:00Z",
  "other_warnings": [
    "rain"
  ]
}
clear_sinceWhen the place actually became clear — the first clean cycle — not when we confirmed it. null when nothing was ever measured at the place.
other_warningsWhich other weather is still warned at this place, by name. When it is not empty the decision is RAIN_CLEARED (or its kind) and not ALL_CLEAR: one weather type has passed and another has not.

How it is signed

We follow the Standard Webhooks scheme, so a library you already have may verify it for you. Three headers travel with every message:

webhook-idThe message identifier, the same value as id in the body.
webhook-timestampWhole seconds since 1970, UTC, at the moment we signed.
webhook-signaturev1, followed by the base64 of an HMAC, SHA-256, over the three parts joined by dots: the id, the timestamp, and the raw body.

The signing secret is yours alone. You get it once, when the destination is made; if you lose it, write to support@weatherx.earth and we issue a new one. It begins with whsec_, and the part after that prefix is base64 — decode it to bytes before you use it as the HMAC key.

Sign the raw body, not your parsed object. Read the bytes exactly as they arrived, before any JSON decoding. A reserialised body has different spacing and the signature will never match.

How to check it

import base64, hashlib, hmac

def is_ours(secret, headers, raw_body):
    """raw_body is the exact text we POSTed, not a re-dumped dict."""
    msg_id = headers["webhook-id"]
    stamp  = headers["webhook-timestamp"]

    # Refuse anything old: it is a replay, not an alert.
    import time
    if abs(time.time() - int(stamp)) > 300:
        return False

    key = base64.b64decode(secret.split("_", 1)[1])
    signed = f"{msg_id}.{stamp}.{raw_body}".encode()
    digest = hmac.new(key, signed, hashlib.sha256).digest()
    mine = "v1," + base64.b64encode(digest).decode()

    # Constant time, and accept any one of the space-separated
    # signatures the scheme allows.
    return any(hmac.compare_digest(mine, one)
               for one in headers["webhook-signature"].split(" "))

What your endpoint must do

  1. Answer 2xx within five seconds. We wait five seconds. Anything else — a slow answer, a 4xx, a 5xx — counts as a failed delivery.

  2. Accept the same message twice. We wait at least sixty seconds and try again after a failure. Use webhook-id to recognise the repeat.

  3. Do not expect a late message. We stop trying after ten minutes and drop it. A weather warning that arrives after the weather is worse than none.

  4. Do the work after you answer. Take the message, answer 200, then act on it. Work done inside the five seconds is work we are waiting for.

Rules for the address you give us

  • It must be https and reachable from the public internet.
  • It must be the final address. We do not follow redirects: a 307 answer is a failed delivery, not a new destination.
  • Addresses that resolve to a private, loopback or link-local network are refused, at the moment of every send and not only when the destination is made.
  • The path may carry your own token if you want a second gate. It never appears in our logs.

What arrives, and when

We look at your place every ten minutes. You get one message when weather is coming or has started, and one when it has passed. A quiet week is a quiet channel — nothing arrives to say that nothing is happening.

WeatherX

STOP — lightning — Acme depot

The lightning is at the place now.

This is measured at the place, not a forecast.

How strong: high (level 3 on our scale).

We measured this at 14:10 UTC.

The first line is the decision: STOP means it is at the place now or within twenty minutes, PREPARE means it is coming, ALL CLEAR means it has passed.

The picture above is the same event as it reaches a person on Slack or in an inbox. Your endpoint gets the fields, not the sentences, and nothing is added to the payload: no stop line, no footer, nothing but the JSON above.

How to stop it

Refusing our POST does not stop it — we keep trying the next event, and the one after. Use the DELETE call above to take this endpoint off, or the Stop link to end every alert for the place at once.

The steps on this page were checked on . The example message is not a copy — it is drawn by the same code that writes your alerts, so it is current on every visit. If the messages you receive no longer look like the ones on this page, write to support@weatherx.earth and tell us. A person reads it.

This page holds no key, sets no cookie and runs nothing that follows you.