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 guidesYou 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"
}id | One 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. |
|---|---|
type | alert, started, updated, ended, or early_all_clear. |
event | rain, lightning or hail. |
decision | What a person should do: STOP, PREPARE, ALL_CLEAR, or RAIN_CLEARED and its kind when one weather type clears while another still runs. |
site | The place: the name you gave it, and its position. |
intensity | mm/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. |
eta | When it reaches the place, in UTC. null on a clear message. |
eta_in_minutes | The same thing counted from the moment we sent, in whole minutes. Never negative: something already past arrives as 0. |
lead_minutes | How 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_km | How far away the nearest cell is, in whole kilometres. null on a clear message, and null where the detector could not measure it. |
from_direction | Which way it comes FROM, as a compass name (SW). null on a clear message. |
observed | true when it is measured at the place now, false when it is still a forecast. |
observed_at | The 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. |
reissued | true when this weather was cleared and came back inside half an hour. A person who read the all-clear needs telling. |
cycle | Which ten-minute run this came from. Use it, not the clock, to order two messages. |
region | Which forecast region the place sits in: europe or us. |
sent_at | When we built the message, in UTC. Not when it reached you. |
test | Present 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_since | When the place actually became clear — the first clean cycle — not when we confirmed it. null when nothing was ever measured at the place. |
|---|---|
other_warnings | Which 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-id | The message identifier, the same value as id in the body. |
|---|---|
webhook-timestamp | Whole seconds since 1970, UTC, at the moment we signed. |
webhook-signature | v1, 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
Answer 2xx within five seconds. We wait five seconds. Anything else — a slow answer, a 4xx, a 5xx — counts as a failed delivery.
Accept the same message twice. We wait at least sixty seconds and try again after a failure. Use
webhook-idto recognise the repeat.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.
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
httpsand 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.
