Webhooks
Webhooks push real-time event notifications to your systems via HTTP POST requests. Configure webhooks to receive alerts in your SIEM, ticketing system, Slack, or any HTTP endpoint.
Webhook Configuration
Create a Webhook
curl -X POST https://api.turqoa.com/v1/webhooks \
-H "Authorization: Bearer $TURQOA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"name": "SIEM Integration",
"url": "https://siem.example.com/api/ingest/turqoa",
"events": ["perimeter_breach", "zone_entry", "seal_violation", "ais_gap"],
"min_severity": "high",
"secret": "whsec_your_signing_secret_here",
"active": true,
"headers": {
"X-Source": "turqoa",
"X-Environment": "production"
}
}'
Response
{
"data": {
"id": "whk_abc123",
"name": "SIEM Integration",
"url": "https://siem.example.com/api/ingest/turqoa",
"events": ["perimeter_breach", "zone_entry", "seal_violation", "ais_gap"],
"min_severity": "high",
"active": true,
"secret": "whsec_your_signing_secret_here",
"created_at": "2026-04-06T10:00:00Z",
"last_triggered_at": null,
"success_count": 0,
"failure_count": 0
}
}
Update a Webhook
curl -X PUT https://api.turqoa.com/v1/webhooks/whk_abc123 \
-H "Authorization: Bearer $TURQOA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"events": ["perimeter_breach", "zone_entry", "seal_violation", "ais_gap", "gate_locked"],
"min_severity": "medium"
}'
List Webhooks
curl https://api.turqoa.com/v1/webhooks \
-H "Authorization: Bearer $TURQOA_API_KEY"
Delete a Webhook
curl -X DELETE https://api.turqoa.com/v1/webhooks/whk_abc123 \
-H "Authorization: Bearer $TURQOA_API_KEY"
Configuration Fields
| Field | Type | Required | Description |
|---|---|---|---|
name | string | Yes | Human-readable name |
url | string | Yes | HTTPS endpoint URL |
events | string[] | Yes | Event types to subscribe to (or ["*"] for all) |
min_severity | string | No | Minimum severity: low, medium, high, critical |
secret | string | Yes | Signing secret for payload verification |
active | boolean | No | Whether the webhook is active (default: true) |
headers | object | No | Custom HTTP headers to include |
categories | string[] | No | Filter by event category |
zones | string[] | No | Filter by zone name |
Event Types
Subscribe to specific event types or use ["*"] to receive all events.
| Event Type | Category | Description |
|---|---|---|
gate_transaction_created | gate | New gate transaction |
ocr_mismatch | gate | Container ID mismatch |
seal_violation | gate | Seal tamper detected |
damage_detected | gate | Container damage found |
gate_override | gate | Operator override |
gate_locked | gate | Gate locked |
motion_detected | perimeter | Motion in zone |
perimeter_breach | perimeter | Perimeter breached |
fence_tamper | perimeter | Fence sensor alert |
zone_entry | maritime | Vessel entered zone |
zone_exit | maritime | Vessel exited zone |
ais_gap | maritime | AIS signal lost |
risk_score_change | maritime | Risk score changed |
dwell_threshold | maritime | Dwell time exceeded |
mission_launched | drone | Drone mission started |
mission_completed | drone | Drone mission ended |
evidence_captured | drone | Evidence captured |
incident_created | system | New incident |
incident_resolved | system | Incident resolved |
system_health_change | system | Component status change |
Payload Format
Every webhook delivery sends a JSON payload with a consistent structure:
{
"id": "whdlv_20260406_001",
"webhook_id": "whk_abc123",
"event": {
"id": "evt_20260406_1045_001",
"type": "zone_entry",
"timestamp": "2026-04-06T10:45:00Z",
"category": "maritime",
"severity": "high",
"title": "Unauthorized vessel entered LNG exclusion zone",
"description": "Vessel MV Suspicious (MMSI: 555123456) entered the LNG Terminal Exclusion Zone without authorization.",
"location": {
"lat": 51.4835,
"lng": -0.0472,
"zone": "LNG Terminal Exclusion Zone"
},
"metadata": {
"mmsi": 555123456,
"vessel_name": "MV Suspicious",
"vessel_type": "cargo",
"risk_score": 78,
"zone_type": "exclusion",
"authorized": false
}
},
"delivered_at": "2026-04-06T10:45:01Z",
"attempt": 1
}
HTTP Headers
Every webhook request includes these headers:
| Header | Description | Example |
|---|---|---|
Content-Type | Always JSON | application/json |
X-Turqoa-Webhook-ID | Webhook configuration ID | whk_abc123 |
X-Turqoa-Delivery-ID | Unique delivery ID | whdlv_20260406_001 |
X-Turqoa-Event-Type | Event type | zone_entry |
X-Turqoa-Signature | HMAC-SHA256 signature | sha256=a1b2c3... |
X-Turqoa-Timestamp | Delivery timestamp | 1712398800 |
User-Agent | Turqoa identifier | Turqoa-Webhooks/1.0 |
Signature Verification
Every webhook payload is signed with your webhook secret using HMAC-SHA256. Always verify the signature before processing the payload to ensure authenticity.
How Signing Works
- Turqoa concatenates the timestamp and payload:
{timestamp}.{payload_json} - Computes HMAC-SHA256 using your webhook secret
- Sends the signature in the
X-Turqoa-Signatureheader
Verification Examples
Python
import hmac
import hashlib
import json
from flask import Flask, request, abort
app = Flask(__name__)
WEBHOOK_SECRET = "whsec_your_signing_secret_here"
@app.route("/webhook", methods=["POST"])
def handle_webhook():
# Get signature and timestamp from headers
signature = request.headers.get("X-Turqoa-Signature", "")
timestamp = request.headers.get("X-Turqoa-Timestamp", "")
payload = request.get_data(as_text=True)
# Verify signature
expected = hmac.new(
WEBHOOK_SECRET.encode(),
f"{timestamp}.{payload}".encode(),
hashlib.sha256,
).hexdigest()
if not hmac.compare_digest(f"sha256={expected}", signature):
abort(401, "Invalid signature")
# Verify timestamp is recent (within 5 minutes)
import time
if abs(time.time() - int(timestamp)) > 300:
abort(401, "Timestamp too old")
# Process the event
event = json.loads(payload)["event"]
print(f"Received: [{event['severity']}] {event['title']}")
return "", 200
Node.js
import express from "express";
import crypto from "crypto";
const app = express();
const WEBHOOK_SECRET = "whsec_your_signing_secret_here";
app.post("/webhook", express.raw({ type: "application/json" }), (req, res) => {
const signature = req.headers["x-turqoa-signature"] as string;
const timestamp = req.headers["x-turqoa-timestamp"] as string;
const payload = req.body.toString();
// Verify signature
const expected = crypto
.createHmac("sha256", WEBHOOK_SECRET)
.update(`${timestamp}.${payload}`)
.digest("hex");
if (!crypto.timingSafeEqual(
Buffer.from(`sha256=${expected}`),
Buffer.from(signature)
)) {
return res.status(401).send("Invalid signature");
}
// Verify timestamp is recent
if (Math.abs(Date.now() / 1000 - parseInt(timestamp)) > 300) {
return res.status(401).send("Timestamp too old");
}
// Process the event
const { event } = JSON.parse(payload);
console.log(`Received: [${event.severity}] ${event.title}`);
res.status(200).send();
});
app.listen(3000);
curl (Testing)
Generate a test signature to verify your implementation:
TIMESTAMP=$(date +%s)
PAYLOAD='{"id":"test","event":{"type":"test","severity":"low","title":"Test event"}}'
SECRET="whsec_your_signing_secret_here"
SIGNATURE=$(echo -n "${TIMESTAMP}.${PAYLOAD}" | openssl dgst -sha256 -hmac "$SECRET" | cut -d' ' -f2)
curl -X POST http://localhost:3000/webhook \
-H "Content-Type: application/json" \
-H "X-Turqoa-Signature: sha256=${SIGNATURE}" \
-H "X-Turqoa-Timestamp: ${TIMESTAMP}" \
-d "$PAYLOAD"
Retry Policy
If your endpoint fails to respond with a 2xx status code, Turqoa retries the delivery with exponential backoff:
| Attempt | Delay | Total Elapsed |
|---|---|---|
| 1 | Immediate | 0 |
| 2 | 30 seconds | 30s |
| 3 | 2 minutes | 2m 30s |
| 4 | 10 minutes | 12m 30s |
| 5 | 30 minutes | 42m 30s |
| 6 | 1 hour | 1h 42m 30s |
| 7 | 3 hours | 4h 42m 30s |
| 8 (final) | 6 hours | 10h 42m 30s |
Retry Behavior
- Retries occur for any non-2xx response or connection timeout
- A 410 Gone response permanently disables the webhook
- If all 8 attempts fail, the webhook is marked as failing and an alert is sent to the admin
- After 3 consecutive full failures (24 events each failing all 8 attempts), the webhook is automatically deactivated
Monitoring Webhook Health
curl https://api.turqoa.com/v1/webhooks/whk_abc123 \
-H "Authorization: Bearer $TURQOA_API_KEY"
{
"data": {
"id": "whk_abc123",
"name": "SIEM Integration",
"url": "https://siem.example.com/api/ingest/turqoa",
"active": true,
"health": {
"status": "healthy",
"success_count": 1247,
"failure_count": 3,
"success_rate_percent": 99.8,
"last_success_at": "2026-04-06T10:44:00Z",
"last_failure_at": "2026-04-05T14:22:00Z",
"last_failure_reason": "Connection timeout",
"avg_response_time_ms": 142
}
}
}
Viewing Delivery History
curl "https://api.turqoa.com/v1/webhooks/whk_abc123/deliveries?per_page=10" \
-H "Authorization: Bearer $TURQOA_API_KEY"
{
"data": [
{
"id": "whdlv_20260406_001",
"event_type": "zone_entry",
"status": "delivered",
"attempts": 1,
"response_code": 200,
"response_time_ms": 120,
"delivered_at": "2026-04-06T10:45:01Z"
},
{
"id": "whdlv_20260406_002",
"event_type": "perimeter_breach",
"status": "delivered",
"attempts": 2,
"response_code": 200,
"response_time_ms": 95,
"first_attempt_error": "Connection reset",
"delivered_at": "2026-04-06T10:48:32Z"
}
]
}
Replaying Failed Deliveries
Replay a specific delivery:
curl -X POST https://api.turqoa.com/v1/webhooks/whk_abc123/deliveries/whdlv_20260406_003/replay \
-H "Authorization: Bearer $TURQOA_API_KEY"
Replay all failed deliveries from a time range:
curl -X POST https://api.turqoa.com/v1/webhooks/whk_abc123/replay \
-H "Authorization: Bearer $TURQOA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"start_date": "2026-04-05T00:00:00Z",
"end_date": "2026-04-06T00:00:00Z",
"status": "failed"
}'
Testing Webhooks
Send a Test Event
curl -X POST https://api.turqoa.com/v1/webhooks/whk_abc123/test \
-H "Authorization: Bearer $TURQOA_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"event_type": "zone_entry"
}'
This sends a synthetic event to your endpoint with test data, allowing you to verify your integration without waiting for a real event.
Local Development
For testing webhooks during local development, use a tunnel service:
# Using ngrok
ngrok http 3000
# Then update your webhook URL to the ngrok URL
curl -X PUT https://api.turqoa.com/v1/webhooks/whk_abc123 \
-H "Authorization: Bearer $TURQOA_API_KEY" \
-d '{"url": "https://abc123.ngrok.io/webhook"}'