Turqoa Docs

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

FieldTypeRequiredDescription
namestringYesHuman-readable name
urlstringYesHTTPS endpoint URL
eventsstring[]YesEvent types to subscribe to (or ["*"] for all)
min_severitystringNoMinimum severity: low, medium, high, critical
secretstringYesSigning secret for payload verification
activebooleanNoWhether the webhook is active (default: true)
headersobjectNoCustom HTTP headers to include
categoriesstring[]NoFilter by event category
zonesstring[]NoFilter by zone name

Event Types

Subscribe to specific event types or use ["*"] to receive all events.

Event TypeCategoryDescription
gate_transaction_createdgateNew gate transaction
ocr_mismatchgateContainer ID mismatch
seal_violationgateSeal tamper detected
damage_detectedgateContainer damage found
gate_overridegateOperator override
gate_lockedgateGate locked
motion_detectedperimeterMotion in zone
perimeter_breachperimeterPerimeter breached
fence_tamperperimeterFence sensor alert
zone_entrymaritimeVessel entered zone
zone_exitmaritimeVessel exited zone
ais_gapmaritimeAIS signal lost
risk_score_changemaritimeRisk score changed
dwell_thresholdmaritimeDwell time exceeded
mission_launcheddroneDrone mission started
mission_completeddroneDrone mission ended
evidence_captureddroneEvidence captured
incident_createdsystemNew incident
incident_resolvedsystemIncident resolved
system_health_changesystemComponent 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:

HeaderDescriptionExample
Content-TypeAlways JSONapplication/json
X-Turqoa-Webhook-IDWebhook configuration IDwhk_abc123
X-Turqoa-Delivery-IDUnique delivery IDwhdlv_20260406_001
X-Turqoa-Event-TypeEvent typezone_entry
X-Turqoa-SignatureHMAC-SHA256 signaturesha256=a1b2c3...
X-Turqoa-TimestampDelivery timestamp1712398800
User-AgentTurqoa identifierTurqoa-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

  1. Turqoa concatenates the timestamp and payload: {timestamp}.{payload_json}
  2. Computes HMAC-SHA256 using your webhook secret
  3. Sends the signature in the X-Turqoa-Signature header

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:

AttemptDelayTotal Elapsed
1Immediate0
230 seconds30s
32 minutes2m 30s
410 minutes12m 30s
530 minutes42m 30s
61 hour1h 42m 30s
73 hours4h 42m 30s
8 (final)6 hours10h 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"}'