Skip to main content
reference 5 min read

Webhooks

Signed event push for flight.uploaded / flight.deleted — registration, verification, retries, and SSRF protection.

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

  1. 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).
  2. Expand Show webhooks on the app's card, click Add endpoint.
  3. Enter:
    • URLhttps://your-backend.example.com/webhooks/we-fly. http://localhost is 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.
  4. 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:

HeaderValue
Content-Typeapplication/json
User-Agentwe-fly-webhooks/1.0
X-We-Fly-EventEvent name (flight.uploaded, etc.)
X-We-Fly-DeliveryUnique delivery ID (ObjectId hex). Use it to dedupe.
X-We-Fly-SignatureHMAC-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://localhost is 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@host is 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):

AttemptTime after first try
10 (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-Delivery and 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_SECONDS above).
  • 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.