TerrAlert sends HMAC-SHA256 signed HTTP POST requests to your registered endpoints when a change detection is confirmed. This guide covers registration, payload format, signature verification, and retry behaviour.
---
You need a Pro or Enterprise subscription and a publicly reachable HTTPS URL.
curl -X POST https://api.terralert.io/webhooks \ -H "Authorization: Bearer $TOKEN" \ -H "Content-Type: application/json" \ -d '{"name": "Production Server", "url": "https://your-server.com/hooks/terralert"}'
Response (one-time only):
{ "id": "wh_3fa85f64...", "name": "Production Server", "url": "https://your-server.com/hooks/terralert", "signing_secret": "whsec_f4c2a8...", "is_active": true, "created_at": "2026-06-27T10:00:00Z" }
Save signing_secret immediately — it is displayed only once. You can always rotate it by deleting and re-creating the webhook.
signing_secret
TerrAlert posts JSON with Content-Type: application/json:
Content-Type: application/json
{ "event": "detection.confirmed", "detection_id": "3fa85f64-5717-4562-b3fc-2c963f66afa6", "aoi_id": "a1b2c3d4-...", "aoi_name": "Amazon Basin Watch", "change_type": "deforestation", "confidence": 0.94, "area_km2": 3.2, "capture_date": "2026-06-27", "satellite": "Sentinel-2", "created_at": "2026-06-27T14:38:05Z" }
Event types:
detection.confirmed
alert.failed
Every request includes an X-TerrAlert-Signature header:
X-TerrAlert-Signature
X-TerrAlert-Signature: sha256=abc123...
The signature is computed as:
HMAC-SHA256(key=signing_secret, message=raw_request_body)
Always verify the signature before processing the payload to prevent spoofed requests.
import hashlib import hmac from flask import Flask, request, abort app = Flask(__name__) SIGNING_SECRET = b"whsec_f4c2a8..." # from webhook registration @app.route("/hooks/terralert", methods=["POST"]) def webhook(): sig_header = request.headers.get("X-TerrAlert-Signature", "") if not sig_header.startswith("sha256="): abort(400, "Missing signature") expected = "sha256=" + hmac.new( SIGNING_SECRET, request.get_data(), hashlib.sha256 ).hexdigest() if not hmac.compare_digest(expected, sig_header): abort(401, "Invalid signature") payload = request.get_json() print(f"Detection: {payload['change_type']} — {payload['area_km2']} km²") return "", 200
const express = require('express'); const crypto = require('crypto'); const app = express(); const SIGNING_SECRET = 'whsec_f4c2a8...'; app.post('/hooks/terralert', express.raw({ type: 'application/json' }), (req, res) => { const sigHeader = req.headers['x-terralert-signature'] ?? ''; const expected = 'sha256=' + crypto .createHmac('sha256', SIGNING_SECRET) .update(req.body) .digest('hex'); if (!crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(sigHeader))) { return res.status(401).json({ error: 'Invalid signature' }); } const payload = JSON.parse(req.body); console.log('Detection:', payload.change_type, payload.area_km2, 'km²'); res.sendStatus(200); });
package main import ( "crypto/hmac" "crypto/sha256" "encoding/hex" "fmt" "io" "net/http" ) var signingSecret = []byte("whsec_f4c2a8...") func webhook(w http.ResponseWriter, r *http.Request) { body, _ := io.ReadAll(r.Body) sig := r.Header.Get("X-TerrAlert-Signature") mac := hmac.New(sha256.New, signingSecret) mac.Write(body) expected := "sha256=" + hex.EncodeToString(mac.Sum(nil)) if !hmac.Equal([]byte(expected), []byte(sig)) { http.Error(w, "Invalid signature", http.StatusUnauthorized) return } fmt.Println("Verified webhook:", string(body)) w.WriteHeader(http.StatusOK) }
If your endpoint does not return a 2xx status within 10 seconds, TerrAlert retries with exponential backoff:
2xx
After 5 failed attempts the alert is marked failed and no further retries are made. You can view delivery history in the Alerts dashboard or via GET /alerts.
failed
GET /alerts
Use the Test button on the Account → Webhooks page, or via the API:
curl -X POST "https://api.terralert.io/webhooks/{id}/test" \ -H "Authorization: Bearer $TOKEN"
TerrAlert sends a synthetic detection.confirmed payload and reports the HTTP status code your server returned.
If your firewall requires it, TerrAlert webhook requests originate from:
35.180.10.1/32 35.180.10.2/32
These IPs are stable but may change with advance notice via the status page.
# List webhooks curl https://api.terralert.io/webhooks -H "Authorization: Bearer $TOKEN" # Delete a webhook curl -X DELETE "https://api.terralert.io/webhooks/{id}" \ -H "Authorization: Bearer $TOKEN"
All documentation