Climacert‑X — Super Admin → IoT Platforms (Webhook‑Only v2)
Version: 2025‑08‑28 • Last updated: 2025‑08‑31
Back to Index Open Wireframe
This spec defines a single webhook integration model for all external IoT providers with HMAC signatures, timestamp hardening, IP allow‑listing, and operational metrics. It preserves tenant hard‑lock on first platform assignment.
Table of Contents 1) Overview 2) Goals & Scope 3) Terminology 4) Admin UI — Platforms Tab 5) Provider Setup (Give This to Provider) 6) Webhook Endpoint (Server Spec) 7) Mapping & Ingestion 8) Security & Compliance 9) Observability (Ops) 10) Acceptance Criteria

1) Overview

Webhook‑Only v2 introduces a unified inbound integration for external IoT platforms. Super Admins configure per‑provider settings and secrets. The public endpoint validates signatures, timestamps, IP allow‑lists, and payload size, then enqueues events for downstream processing and rollups. Tenants are hard‑locked to a single platform.

2) Goals & Scope

  • One integration model: HTTPS POST + HMAC signature verification.
  • Preserve tenant hard‑lock: one platform per tenant; immutable after first assignment.
  • Lightweight devices via externalId mapping; ingestion → bucketization → monthly rollups.
  • No long‑term raw time‑series; normalized buckets + monthly rollups as source of truth.

3) Terminology

  • Platform: External IoT provider (e.g., Disrupt‑X Ignite Shield).
  • Secret: Shared key used to sign requests (HMAC‑SHA256).
  • Signature Header: Default X‑Platform‑Signature; alternative Disrupt‑X‑Signature.
  • Mode A (Simple): HMAC over JSON.stringify(body).
  • Mode B (Hardened): HMAC over "<timestamp>.<rawBody>" with X‑Request‑Timestamp; max 5‑minute skew.

4) Admin UI — Platforms Tab

Create / Edit Platform (Webhook Mode only)

  • Required: Name, Logo, Domain (display only)
  • Connection Mode: fixed to “Webhook (HMAC)”
  • Webhook URL (read‑only, copy): https://api.climacert.io/integrations/{platformId}/webhook
  • Signature header: dropdown [X‑Platform‑Signature (default), Disrupt‑X‑Signature]
  • Hash algorithm: sha256 (fixed)
  • Secret: auto‑generated 32 bytes hex; show once. Action: Rotate Secret
  • Require timestamp header: toggle (enables Mode B)
  • Allowed source IPs: optional CIDR allow‑list
  • Max payload size: default 256 KB
  • Events accepted: [Readings, DeviceStatus, Ping]
  • Test tools: Copy sample cURL, Send Test Ping

Status card (read‑only)

  • State: Connected / Not Connected
  • Last delivery time; 24h delivery stats
  • Failures: signature_mismatch / 4xx / 5xx
  • Buttons: Rotate secret

Tenant assignment (unchanged): Super Admin assigns exactly one platform to a tenant. After first assignment, platform is hard‑locked.

5) Provider Setup (Give This to Provider)

Method: POST

URL: https://api.climacert.io/integrations/{platformId}/webhook

TLS: Required (valid public certificate)

Headers:

  • Content‑Type: application/json
  • Signature: X‑Platform‑Signature (or Disrupt‑X‑Signature)
  • Optional (Mode B): X‑Request‑Timestamp (ISO 8601 in UTC)

Body (envelope):

{
  "timestamp": "2025-08-27T16:48:45.878Z",
  "type": "IgniteShield_Reading",
  "data": { /* provider-specific payload */ }
}

How to sign:

  • Mode A (Simple): signature = HMAC_SHA256(secret, JSON.stringify(body))
  • Mode B (Hardened): signature = HMAC_SHA256(secret, "<timestamp>.<rawBody>")

Retries: retry on non‑200. Treat only HTTP 200 as success. Respect 429 with backoff.

Sample cURL (bash + openssl):

SECRET=<32-byte-hex-secret>
BODY='{"timestamp":"2025-08-27T16:48:45.878Z","type":"Ping","data":{"hello":"world"}}'
# Mode A
SIG=$(printf "%s" "$BODY" | openssl dgst -sha256 -hmac "$SECRET" -binary | xxd -p -c 256)
curl -sS -X POST \
  -H 'Content-Type: application/json' \
  -H 'X-Platform-Signature: '$SIG \
  --data "$BODY" \
  https://api.climacert.io/integrations/{platformId}/webhook

6) Webhook Endpoint (Server Spec)

POST /v1/integrations/{platformId}/webhook

Validation steps

  1. Read raw body bytes + headers.
  2. Load platform config (secret, header name, mode, allow‑list, max size).
  3. Enforce TLS; reject HTTP.
  4. If IP allow‑list set, verify source IP ∈ allow‑list → else 403.
  5. Require Content‑Type: application/json; enforce max body size.
  6. If Mode B: require X‑Request‑Timestamp; reject if |now – ts| > 5 minutes.
  7. Compute expected signature using secret (mode‑specific) and constant‑time compare with header value.
  8. Idempotency: drop duplicates by (eventId if present) or (platformId, signature, timestamp) within 24h.
  9. On success: enqueue event and return 200 immediately.
  10. On failure: return 401/403/415/413 accordingly; never leak internal detail in body.

Responses

  • 200 Accepted
  • 401 Unauthorized
  • 403 Forbidden
  • 413 Payload Too Large
  • 415 Unsupported Media Type
  • 429 Too Many Requests

Example pseudo‑code

// Express-style pseudo-code
app.post('/v1/integrations/:platformId/webhook', (req, res) => {
  const cfg = loadPlatform(req.params.platformId);
  if (!cfg) return res.status(403).end();
  if (!req.secure) return res.status(403).end();
  if (cfg.allowIps && !ipAllowed(req.ip, cfg.allowIps)) return res.status(403).end();
  if (req.get('content-type') !== 'application/json') return res.status(415).end();
  if (req.rawBody.length > cfg.maxBytes) return res.status(413).end();
  const raw = req.rawBody.toString('utf8');
  const ts = req.get('X-Request-Timestamp');
  if (cfg.mode === 'B' && (!ts || skewTooLarge(ts))) return res.status(401).end();
  const sigHeader = req.get(cfg.sigHeaderName || 'X-Platform-Signature');
  const expected = hmacSha256(cfg.secret, cfg.mode === 'B' ? `${ts}.${raw}` : raw);
  if (!constantTimeEqual(sigHeader, expected)) {
    audit('signature_mismatch', { platformId: cfg.id });
    return res.status(401).end();
  }
  if (isDuplicate(cfg.id, sigHeader, ts)) return res.status(200).end();
  enqueueEvent(cfg.id, JSON.parse(raw));
  metrics.bump('delivery_success', cfg.id);
  audit('webhook_received', { platformId: cfg.id });
  res.status(200).end();
});

7) Mapping & Ingestion

  • Resolve tenant by locked platform: tenant.platformId.
  • Map device by provider device id → device.externalId (connectionType = "webhook").
  • Normalize timestamps to UTC; validate schema version (if provided).
  • Bucketize readings (minute/hour buckets) → update month‑to‑date counters.
  • Persist MonthlyRollup for historical queries and certificates.
  • Update device.lastReading and device.status for UI tables.

8) Security & Compliance

  • Secrets encrypted at rest; show‑once in UI. Rotation invalidates old secret immediately.
  • Constant‑time signature comparison; no timing side‑channels.
  • Strict TLS; HSTS on public endpoint.
  • Logging with redaction: never log secrets or raw signature values; log only last4 and digests.
  • Audit trail (immutable): platform_created, platform_assigned, webhook_secret_rotated, webhook_received, signature_mismatch, ip_blocked.
  • DDoS/burst protection: WAF + per‑platform rate limiting + payload cap.

9) Observability (Ops)

  • Metrics per platform: delivery_success_rate, signature_mismatch_rate, last_success_at, p99_receive_latency.
  • Dashboards: 24h deliveries, failures by reason, retries observed, top noisy devices.
  • Alerts: sustained signature_mismatch > 5% over 15 min; no deliveries > 1h.

10) Acceptance Criteria

  • Given valid secret and signature → 200 and event enqueued.
  • Given allow‑list, request outside list → 403.
  • Given hardened mode, timestamp skew > 5 minutes → 401.
  • Given secret rotation, old secret used → 401.
  • Given duplicate eventId within 24h → dropped without side‑effects.