Webhooks and automation

What TrailHUB offers for automation today, what it does not (no webhooks yet), and practical patterns that work now.

Short version: TrailHUB does not have webhooks yet. You cannot register a URL to be called when a trail closes or a notice is posted. What you can do is push changes into TrailHUB on a schedule, pull the published data out of TrailHUB at an interval, and let the Grooming Tracker app post conditions for you.

#What does not exist (yet)

  • Outbound webhooks or event subscriptions for changes in TrailHUB.
  • A streaming or long-polling endpoint.
  • Server-side scheduled actions ("close all trails every night at 5 pm") configured inside TrailHUB.
  • Rate limiting on the API (so be considerate), and per-key usage reports beyond the Last used date.

If you need any of these, tell us; it helps prioritize.

#Patterns that work today

#1. Scheduled scripts with an API key (cron + curl)

Anything you can do in the web app you can do with a Management API call, so a cron job (or GitHub Actions, a cloud scheduler, Home Assistant, Zapier's webhook action, …) can run routine updates.

Create a dedicated key in User Settings → API Keys (read & write, limited to the trail system, with an expiry), store it in an environment variable, and schedule the calls.

Close every trail at 17:00 and reopen at 07:00, with a notice overnight:

# /etc/cron.d/trailhub   (TRAILHUB_KEY and TS set in the environment)
0 17 * * *  curl -s -X POST https://trailhub.org/api/v1/trail-systems/$TS/trails/status -H "Authorization: Bearer $TRAILHUB_KEY" -H "Content-Type: application/json" -d '{"status":"Closed","all":true}'
1 17 * * *  curl -s -X POST https://trailhub.org/api/v1/trail-systems/$TS/updates -H "Authorization: Bearer $TRAILHUB_KEY" -H "Content-Type: application/json" -d '{"type":"notice","severity":"info","description":"Trails are closed overnight and reopen at 7 am.","expiresInHours":14}'
0 7  * * *  curl -s -X POST https://trailhub.org/api/v1/trail-systems/$TS/trails/status -H "Authorization: Bearer $TRAILHUB_KEY" -H "Content-Type: application/json" -d '{"status":"Open","all":true}'

Post a daily snow report from your own weather station data:

#!/bin/sh
NEW24=$(my-station --new-snow-24h)   # your own source
BASE=$(my-station --base-depth)
curl -s -X POST "https://trailhub.org/api/v1/trail-systems/$TS/updates" \
  -H "Authorization: Bearer $TRAILHUB_KEY" -H "Content-Type: application/json" \
  -d "{\"type\":\"snowReport\",\"twentyFourHours\":$NEW24,\"baseDepth\":$BASE,\"expiresInHours\":24}"

Tips:

  • Updates expire (24 h by default); a daily job that posts a fresh one is the intended pattern. Use PATCH /updates/:id if you would rather extend an existing one.
  • Set notify: true only when you really want every subscriber emailed or texted.
  • Check the HTTP status; errors are JSON { "error": { "code", "message" } }.
  • One key per job makes Last used meaningful and lets you revoke one job without breaking another.

#2. Let an AI assistant do it

The MCP server turns the same API into tools for Claude Desktop, Claude Code and other MCP clients. It is interactive rather than scheduled, but it is the fastest way to handle one-off situations ("close the north side and post a notice about the logging operation").

#3. The Grooming Tracker app posts conditions automatically

When an operator records a grooming pass with the Grooming Tracker, the uploaded GPS track is compared against the system's trails and a Groomed surface-conditions update (24 h expiry, no notification) is posted for every trail with more than 10% coverage. No scripting required. See How coverage is calculated.

#4. Build on the published GeoJSON (pull, don't push)

Every change, whatever its source, triggers TrailHUB's compile step, which rewrites a single public GeoJSON file per trail system containing all trails, points and active updates with their status. The public read API gives you its URL (geoJsonUrl on GET /api/ts/:id).

For a website map, a lobby display or a digital sign, poll that file every few minutes. It is public, needs no key and is not subject to the CORS allowlist, so a browser can load it directly. Because the file name changes on every recompile, re-fetch /api/ts/:id (server side or from an allowlisted origin) to get the current URL, or use the authenticated GET /api/v1/trail-systems/:id/trails with an API key from your server.

If you only need the system-wide picture (how many trails are open at each system), GET /api/trail-systems is one GeoJSON file for all systems, rebuilt every 3 hours.

#Compile pipeline latency

Writes through the web app, the Management API, the MCP server and the tracker do not change the public map directly. They write to the database and queue a compile request. A background function then:

  1. Reads all trails, points and unexpired updates for the system.
  2. Resolves each trail's status (including None → derived from activities), attaches trail-level updates and recomputes the system's counts and distances.
  3. Writes a new GeoJSON file, deletes the previous one and updates the trail system document with the new geoJsonPath and stats.

This typically completes in a few seconds. Expect GET /api/ts/:id, the public map and embeds to reflect a change within roughly 5–15 seconds; the API response to your write will already show the new document state immediately. POST /trail-systems/:id/compile forces a rebuild if something looks stale (it rarely should).

Subscriber notifications (notify: true) are sent by a separate function when the update document is created, independent of the compile.

#Keeping an eye on things

  • GET /api/v1/trail-systems/:id/updates?includeExpired=true lists history (up to 100), useful for auditing what a script posted. Updates created through the API carry source: "api".
  • Each trail and point records updatedOn and updatedBy (the account whose key made the change).
  • User Settings → API Keys shows Last used per key.