Webhooks

Stop polling. Subscribe once and we POST a signed payload to your endpoint when each event fires — sunrise, sunset, civil dawn / dusk, solar noon, moonrise / moonset.

Endpoints (Pro+ tier):

  • POST /v1/{product}/webhooks/ — register
  • GET /v1/{product}/webhooks/ — list mine
  • GET /v1/{product}/webhooks/<uuid>/ — retrieve
  • DELETE /v1/{product}/webhooks/<uuid>/ — deactivate

{product} is one of ephemerides or solar. Same contract for both.

Quickstart

1. Register a subscription

curl -X POST \
     -H "Authorization: Bearer $THIRDTRAIL_KEY" \
     -H "Content-Type: application/json" \
     -d '{
       "event_type": "sunrise",
       "lat": 53.349, "lon": -6.260, "elevation": 20,
       "callback_url": "https://yourapp.example.com/wh/sunrise",
       "horizon_days": 30
     }' \
     "https://thirdtrail.life/v1/ephemerides/webhooks/"

Response (201 Created):

{
  "id": "8a4c6f12-9b5e-4ad7-8c19-1e7f4a2b5d7c",
  "event_type": "sunrise",
  "lat": 53.349, "lon": -6.260, "elevation_m": 20,
  "horizon_days": 30,
  "callback_url": "https://yourapp.example.com/wh/sunrise",
  "is_active": true,
  "created_at": "2026-05-19T11:24:30+00:00",
  "last_delivered_at": null,
  "secret": "Hd7Lo_ec1ddNTJpA6_3a9cYJnXzQK6w7Ld9iqJqg7vQ"
}
⚠️ The secret is shown exactly once. Store it immediately; we do not return it on subsequent reads. No rotation endpoint in v1 — to rotate, delete the webhook and register a new one.

2. Receive a delivery

When the next event fires, we POST to your callback_url:

POST /wh/sunrise HTTP/1.1
Host: yourapp.example.com
Content-Type: application/json
User-Agent: thirdtrail-webhook/1.0
X-Thirdtrail-Signature: sha256=2c8b3e0fa7d4...
X-Thirdtrail-Event: sunrise
X-Thirdtrail-Delivery: f12a7b30-2cd1-4eaa-9b1c-8d9e0f3a4c5d

{
  "event_id": "f12a7b30-2cd1-4eaa-9b1c-8d9e0f3a4c5d",
  "webhook_id": "8a4c6f12-9b5e-4ad7-8c19-1e7f4a2b5d7c",
  "event_type": "sunrise",
  "scheduled_at": "2026-05-20T05:33:18+00:00",
  "attempt": 1,
  "data": {
    "lat": 53.349, "lon": -6.260, "elevation_m": 20
  }
}

3. Verify the signature

import hmac, hashlib

def verify(secret: str, body: bytes, header: str) -> bool:
    expected = hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
    sent = header.removeprefix("sha256=")
    return hmac.compare_digest(expected, sent)

Use hmac.compare_digest (constant-time comparison) to mitigate timing attacks. Reject any request whose signature does not match.

4. Be idempotent

We retry failed deliveries. If your service was actually able to process attempt 1 but the response was lost, you may receive attempt 2 for the same event. Use X-Thirdtrail-Delivery as your idempotency key — store it on first success, treat repeats as no-ops.

Event catalogue

event_typeProductDescription
sunriseEphemeridesSun's apparent disc clears the elevation-corrected horizon.
sunsetEphemeridesSun's apparent disc drops below the elevation-corrected horizon.
civil_dawnEphemeridesSun crosses −6° altitude going up.
civil_duskEphemeridesSun crosses −6° altitude going down.
solar_noonEphemerides + SolarSun reaches its daily meridian transit.
moonriseEphemeridesMoon's apparent disc clears the horizon.
moonsetEphemeridesMoon's apparent disc drops below the horizon.

Delivery contract

Headers we send

HeaderPurpose
Content-TypeAlways application/json.
User-Agentthirdtrail-webhook/<version>.
X-Thirdtrail-Signaturesha256=<hex> — HMAC-SHA256(webhook.secret, body).
X-Thirdtrail-EventThe event_type, mirrored for log / route convenience.
X-Thirdtrail-DeliveryUUID, unique per attempt. Use as your idempotency key.

Timeouts and retries

  • Request timeout: 10 seconds.
  • Success: any 2xx status code. Webhook's last_delivered_at is updated, no retry queued.
  • Caller opt-out: 410 Gone means "you have removed this endpoint" — we do not retry that delivery (the subscription itself stays active).
  • Other failures (5xx, network errors, non-2xx-non-410): exponential-backoff retry on the schedule below.
AttemptDelay before retry
1 fails1 minute
2 fails5 minutes
3 fails30 minutes
4 fails2 hours
5 failsgive up — webhook is deactivated; no further deliveries fire.

When a webhook auto-deactivates, its is_active flag flips to false and deactivated_at is set. The subscription will not be resurrected without a manual re-POST.

What you should return

  • 2xx: with an empty body or any body. We don't read it.
  • 4xx (other than 410): we retry on the schedule above.
  • 410 Gone: stop retrying that delivery; subscription stays active.
  • Any 3xx redirect: we do not follow redirects. Use a stable URL.

Security

  • The secret is the only credential needed to verify our callbacks. Treat it like any production secret — do not log it, do not commit it to source control.
  • Rotation: there is no PATCH endpoint to roll the secret. Delete the existing webhook and register a new one.
  • We use TLS for all delivery POSTs by default. If you accept HTTP callback URLs, please don't — they leak both your signature and any user identifiers in the body.

Limits

PlanWebhooks per accountDeliveries / day
Freenot enabled—
Starternot enabled—
Pro100~3 000
Business10 000~300 000
Enterprisecustomcustom

Start using it

Create a free account