we-fly pushes JSON events to URLs you register, signed with HMAC-SHA256 so you can verify they came from us. You don't need to poll the API to discover changes — register a webhook and we'll tell you when a flight is uploaded, deleted, etc.
Registering an endpoint
- Open https://we-fly.cloud/developer and pick the app whose owner
you want webhooks for (the events that trigger webhooks are
filtered by
userScope— see below). - Expand Show webhooks on the app's card, click Add endpoint.
- Enter:
- URL —
https://your-backend.example.com/webhooks/we-fly.http://localhostis accepted for dev; production must be HTTPS. - Events — pick at least one. Available today:
flight.uploaded,flight.deleted. - User scope:
owner only— only events caused by the app's owner fire. Use this for personal-sync integrations.any user who authorised this app— events from any user with an active OAuth grant for the app. Use this for multi-tenant integrations.
- URL —
- Copy the signing secret (
wf_whsec_…) revealed once. You'll use it to verify the HMAC on every payload.
Event delivery
Each delivery is a POST to your URL with:
| Header | Value |
|---|---|
Content-Type | application/json |
User-Agent | we-fly-webhooks/1.0 |
X-We-Fly-Event | Event name (flight.uploaded, etc.) |
X-We-Fly-Delivery | Unique delivery ID (ObjectId hex). Use it to dedupe. |
X-We-Fly-Signature | HMAC-SHA256 signature — see Verifying signatures |
Body is a JSON object. Example for flight.uploaded:
{
"event": "flight.uploaded",
"occurredAt": "2026-05-27T14:42:11.000Z",
"user": { "id": "65a1bcdeadbeef1234567890" },
"flight": {
"hash": "f4b9a8c...",
"date": "2026-05-27",
"distance": 87.3,
"score": 130.95,
"type": "FAITriangle",
"gliderType": "Ozone Zeolite GT",
"durationSeconds": 14820
}
}
For flight.deleted:
{
"event": "flight.deleted",
"occurredAt": "2026-05-27T14:42:11.000Z",
"user": { "id": "65a1bcdeadbeef1234567890" },
"flight": { "id": "65a4f1bcdeadbeef12345678", "hash": "f4b9a8c..." }
}
Payloads stay under a few KB — they reference the canonical record by
ID/hash; pull the full data via /api/v1/flights/{id} if you need
more.
Verifying signatures
The X-We-Fly-Signature header is:
X-We-Fly-Signature: t=1748426400,v1=d3e4f5...
Where t is the Unix timestamp (seconds) and v1 is the
HMAC-SHA256 hex digest of "${t}.${rawRequestBody}" keyed by your
signing secret.
Always verify before trusting the payload. Otherwise, anyone who guesses your URL can post arbitrary events.
Node.js
import crypto from "node:crypto";
const SECRET = process.env.WEFLY_WEBHOOK_SECRET;
const MAX_AGE_SECONDS = 300; // reject anything older than 5 minutes
export function verifyWebhook(rawBody, sigHeader) {
if (!sigHeader) return false;
const parts = Object.fromEntries(
sigHeader.split(",").map((kv) => kv.split("=")),
);
const t = Number(parts.t);
if (!Number.isFinite(t)) return false;
if (Math.abs(Date.now() / 1000 - t) > MAX_AGE_SECONDS) return false;
const expected = crypto
.createHmac("sha256", SECRET)
.update(`${t}.${rawBody}`)
.digest("hex");
const provided = parts.v1;
if (typeof provided !== "string" || provided.length !== expected.length) {
return false;
}
return crypto.timingSafeEqual(
Buffer.from(provided),
Buffer.from(expected),
);
}
Use the raw body before any JSON parsing — JSON re-serialisation will reorder keys and break the signature.
Python
import hmac, hashlib, time
SECRET = os.environ["WEFLY_WEBHOOK_SECRET"].encode()
MAX_AGE = 300
def verify(raw_body: bytes, sig_header: str) -> bool:
parts = dict(p.split("=", 1) for p in sig_header.split(","))
try:
t = int(parts["t"])
except (KeyError, ValueError):
return False
if abs(time.time() - t) > MAX_AGE:
return False
signed = f"{t}.".encode() + raw_body
expected = hmac.new(SECRET, signed, hashlib.sha256).hexdigest()
return hmac.compare_digest(parts.get("v1", ""), expected)
URL constraints (SSRF protection)
To prevent we-fly being used as a tool to probe internal networks, the URL you register must:
- Use
https://. (http://localhostis accepted in development environments only.) - Resolve to a public IP address. We reject hostnames that resolve to private, loopback, link-local, or other internal ranges (IPv4 and IPv6) — at registration and again immediately before every delivery attempt.
- Not contain a userinfo segment (
https://user:pass@hostis rejected; HMAC-SHA256 is our authentication channel, not Basic auth). - Not redirect us to a different host. Your endpoint must respond directly with 2xx; any 3xx is treated as a delivery failure.
The address we screen is the address we connect to: each delivery is pinned to the IP we just validated, so a hostname that flips from public to private between the check and the connection (DNS rebinding / fast flux) cannot be used to reach an internal target. TLS is still verified against your hostname.
Retry policy
If your endpoint:
- returns 2xx → delivery succeeds.
- returns anything else, or times out (10s), or DNS/TLS fails → we retry.
Retry schedule (approximate):
| Attempt | Time after first try |
|---|---|
| 1 | 0 (immediate) |
| 2 | +1 minute |
| 3 | +5 minutes |
| 4 | +25 minutes |
| 5 | +2 hours |
| 6 | +12 hours |
| 7 | +24 hours |
After attempt 7 the delivery is moved to a failed status. It
remains visible in your /developer dashboard for 30 days for
post-mortem before being purged automatically.
Best practices
- Respond quickly — 200 OK with an empty body, then process asynchronously. We time out after 10 seconds.
- Use the delivery ID for idempotency. Network glitches or retries
can deliver the same event more than once. Store
X-We-Fly-Deliveryand skip duplicates. - Verify the signature on every request. Don't skip it for "trusted" deployments — a typo in your firewall rules is exactly the kind of mistake signature verification catches.
- Reject replays by checking the timestamp is within the last few
minutes (
MAX_AGE_SECONDSabove). - Pin your TLS chain if you're paranoid. We use Let's Encrypt and rotate quarterly.
Rotating the secret
Click Rotate secret on the endpoint row. We mint a new secret immediately; the old one stops being valid the moment you click. Deploy the new secret to your server before clicking, or do it during a maintenance window.
If you suspect a leak, rotate immediately and inspect your delivery history for unusual receivers.
Pausing an endpoint
Click Pause on the row. New events won't be queued; existing pending deliveries continue trying. Click Resume to re-enable.
What we don't do (yet)
- Multiple endpoints per event — register as many endpoints as you like, but consider that each adds load to the cron processor. Practical limit: a few hundred per app.
- Filters beyond event + userScope — if you want only certain flight types, filter in your handler.
- Native fan-out to message queues — your endpoint can republish to SQS / Pub/Sub / Kafka if needed.
These are likely candidates for a future v2 of the webhook API.