✈
    TravelmodeDevelopers

    Weather Intelligence API · v1

    🔎/
    📖Overview🧪API Reference (Try It)
    Guides
    🚀Getting Started🔐Authentication⏱️Rate Limits📡Endpoints⚠️Errors💳Billing🧊Cache & Freshness🪝Webhooks📝Attribution
    ⬇️Download openapi.yaml🔑Manage API Keys
    Developers / Webhooks

    Webhooks

    Last verified: 2026-05-03 (Task #334 — docs accuracy pass).

    Webhooks let your service react to weather changes without polling. Register an HTTPS endpoint, choose the events you care about, and the delivery worker POSTs signed payloads as the watches you've created cross their thresholds.

    Event types

    The MVP supports the events below. Subscribing to an event you don't need is harmless — just drop it on receipt.

    Event typeWhen it fires
    weather.change_detectedA watch's snapshot crossed one of its change_rules thresholds.
    weather.severe_alert_addedA new severe/extreme alert appeared on a snapshot the watch is tracking.
    weather.watch_expiredA watch reached its ends_at and was retired automatically.
    weather.watch_failedThe refresh worker failed too many times for a watch and gave up.
    weather.snapshot_refreshedA watch's snapshot was refreshed even though no rule was crossed. Useful for "always emit" pipelines.
    weather.webhook_delivery_failedTerminal state for a delivery — the worker exhausted retries and gave up on the endpoint.
    weather.webhook_endpoint_disabledAuto-disable safeguard tripped: the endpoint hit the consecutive-failure or rolling-give-up threshold and was transitioned to status='disabled'. Fires exactly once per disable.

    The events field on POST /v1/webhooks is a non-empty subset of this list.

    Creating an endpoint

    curl -s -X POST "https://api.travelmode.app/v1/webhooks" \
      -H "Authorization: Bearer tm_weather_..." \
      -H "Content-Type: application/json" \
      -d '{
        "url": "https://example.com/hooks/weather",
        "events": ["weather.change_detected", "weather.severe_alert_added"],
        "description": "Production weather notifier"
      }'
    

    The response includes a secret — only on creation. It looks like whsec_<32 hex chars>. Store it somewhere your handler can read. Subsequent reads (GET /v1/webhooks, GET /v1/webhooks/{id}) only return the public secret_prefix.

    Verifying signatures

    Every delivery includes three headers:

    HeaderValue
    X-Travelmode-SignatureHMAC-SHA256 hex digest of {timestamp}.{raw_body} using your endpoint's secret.
    X-Travelmode-TimestampUnix-seconds at the moment we signed and dispatched the request.
    X-Travelmode-Event-IdUUID of the underlying weather_change_event row.

    To verify a delivery:

    1. Read the raw request body (do not parse-and-reserialize — bytes matter).
    2. Recompute hex(HMAC-SHA256(secret, timestamp + "." + raw_body)).
    3. Compare in constant time to the value of X-Travelmode-Signature.
    4. Reject the request if the timestamp drift is more than ~5 minutes from now. This blocks replay attacks even if the signature matches.
    import crypto from 'node:crypto';
    
    function verify(req, secret) {
      const sig = req.headers['x-travelmode-signature'];
      const ts = req.headers['x-travelmode-timestamp'];
      const expected = crypto
        .createHmac('sha256', secret)
        .update(`${ts}.${req.rawBody}`)
        .digest('hex');
      return crypto.timingSafeEqual(
        Buffer.from(expected, 'hex'),
        Buffer.from(sig, 'hex'),
      );
    }
    

    Retry behavior

    Failed deliveries (any non-2xx response, or a network error) are retried with exponential backoff. Each attempt is recorded in webhook_delivery_attempts with response status, response time, response body excerpt, and the signature/timestamp pair we sent. The attempts are queryable via GET /v1/webhooks/{id}/attempts.

    Once retries are exhausted the worker emits a weather.webhook_delivery_failed event for the same endpoint, so you can be notified about your own unreachable URLs.

    Required scope

    All /v1/webhooks endpoints (and the attempts endpoint) require the weather:webhooks scope.

    Required URL scheme

    Webhook URLs must use HTTPS. The create endpoint rejects http:// URLs at validation time — we never send signed payloads over plaintext.

    Disabling an endpoint

    DELETE /v1/webhooks/{id} soft-disables the endpoint (status='disabled', disabled_at=now). The delivery worker stops targeting it; the row itself is retained so historical attempts stay queryable.

    Auto-disable safeguard

    To stop a persistently-broken endpoint from burning worker capacity forever, the delivery worker auto-disables an endpoint when either of the following holds true:

    • 50 consecutive failed deliveries. The failure_count column is bumped on every non-2xx attempt and reset to 0 on every success, so this fires only when there's been no recovery in between.
    • 6 retry chains exhausted (giving_up attempts) in a rolling 24h window. Counts terminal failures across separate events, so a single noisy day still trips the safeguard.

    When the safeguard fires:

    1. The endpoint transitions to status='disabled' with disabled_at=now and disabled_reason populated (one of auto_disabled_consecutive_failures or auto_disabled_giveup_window).
    2. A final weather.webhook_endpoint_disabled event is emitted with the reason, the threshold counts, and the original webhook_id in its payload. Subscribe to it on a different webhook so the notification actually lands.
    3. The worker stops dispatching new events to the endpoint.

    Re-enable by POST /v1/webhooks with the corrected URL after fixing your endpoint — the call mints a fresh secret and a fresh endpoint row. (The original disabled row is retained for audit history.)

    Previous
    ← Cache & Freshness
    Next
    Attribution →