TerraGuard

CAP Webhook Ingestion

How external organizations push Common Alerting Protocol (CAP v1.2) alerts into TerraGuard via an authenticated webhook, and how the per-source transformation layer turns them into disaster events.

Overview

TerraGuard accepts inbound disaster/weather alerts from external authorities using the Common Alerting Protocol (CAP) v1.2 — the OASIS standard used by national weather services and emergency agencies worldwide (ČHMÚ, MeteoAlarm, NOAA/NWS, …).

Unlike GDACS/USGS/NHC (which TerraGuard polls), CAP is a push integration: a registered organization sends each alert to a webhook, authenticated with an API key issued to that organization. A per-source transformation layer maps each authority's CAP dialect (event vocabulary, severity scale, area codes, language) into TerraGuard's internal event model — so a new organization can be onboarded by configuration alone, with no code change or redeploy.

Sending data as an external partner? This page is the architecture/admin reference. For a step-by-step guide to pushing CAP alerts (request format, message requirements, responses, retries, and FAQ), see Sending CAP Data.

Two services collaborate. The Event Processor (Go) hosts the ingest webhook and runs the transformation + event pipeline. The Backend API (Python) hosts the admin API that registers organizations and issues/revokes their API keys. Both read the same PostgreSQL tables (cap_sources, cap_source_api_keys).

Architecture

Loading diagram...

The API-key model (developer-application style)

Each external organization is registered as a CAP source and issued one or more API keys — much like creating a developer application and receiving a credential:

  • A key is generated with 256 bits of entropy and stored only as a SHA-256 hash. The plaintext is shown once at issuance and is never retrievable again.
  • Every ingest request presents the key in the X-API-Key header. The Event Processor hashes it and looks it up, which identifies the originating organization — so every resulting event is attributed to that org via disaster_events.cap_source_id.
  • Keys can be revoked (and sources deactivated) at any time; either immediately rejects further pushes with 401.

Admin API (/api/v1/cap-sources)

Gated by the manage_ai_agents permission. Run against the Backend API.

MethodPathPurpose
GET/api/v1/cap-sourcesList sources + active key counts
POST/api/v1/cap-sourcesRegister an organization (incl. transform_config)
GET/api/v1/cap-sources/{id}Fetch one source
PATCH/api/v1/cap-sources/{id}Update name / contact / is_active / transform_config
DELETE/api/v1/cap-sources/{id}Soft-delete (deactivate)
GET/api/v1/cap-sources/{id}/api-keysList keys (incl. revoked)
POST/api/v1/cap-sources/{id}/api-keysIssue a key (plaintext returned once)
DELETE/api/v1/cap-sources/{id}/api-keys/{key_id}Revoke a key

The ingest webhook

Production:  POST https://api.terraguard.ai/v1/ingest/webhook
Local dev:   POST http://localhost:5606/v1/ingest/webhook
Header:      X-API-Key: <the org's key>
Body:        CAP v1.2 as XML or JSON  (auto-detected; max 8 MiB)

api.terraguard.ai path-routes only /v1/ingest/webhook to the otherwise-internal event-processor (every other path on that host goes to the backend API). The event-processor's own routes — /test/*, /poll/*, /metrics, /docs, /adapters — are never publicly reachable.

HTTPMeaning
202 AcceptedStored. status: "accepted" (transformed → pipeline) or status: "UNPROFILED" (stored raw)
401 UnauthorizedMissing / invalid / revoked key, or source deactivated
413 Payload Too LargeBody exceeds 8 MiB
422 Unprocessable EntityBody persisted but failed CAP validation (raw kept for debugging)
503 Service UnavailablePersistence or pipeline failure — the sender should retry

Example success response:

{
  "success": true,
  "message": "accepted and processed (processed)",
  "payload": {
    "raw_record_id": "460da342-1cd3-48f0-8241-97bb17396169",
    "source_slug": "chmu",
    "status": "accepted",
    "alert_identifier": "2.49.0.0.203.0.CZ.260401074337.XOCZ50_OKPR_000107",
    "msg_type": "Update"
  }
}

End-to-end flow

Loading diagram...

The transformation layer

CAP standardizes the envelope, but authorities differ in the details — event names, how they use the severity scale, which area-code system they use, and language. The transformer is config-driven: each source carries a transform_config (JSONB on cap_sources, set via the admin API) that the generic transformer reads at ingest time.

{
  "language": "en",                       // which <info> block to use (prefix match)
  "event_type_map": {                     // CAP <event> text → internal event type
    "Rain Flood": "FLOOD",
    "Forest Fire": "WILDFIRE"
  },
  "severity_map": {                       // optional override of CAP severity → alert level
    "Extreme": "RED",
    "Severe": "ORANGE"
  },
  "default_countries": ["CZE"],           // attached when the message has no usable geometry
  "geocode": { "system": "CISORP", "strategy": "polygon_then_centroid" }
}

How each field maps:

CAP inputResolves toRule
<info><event> (or <eventCode>)event_typeevent_type_map (case-insensitive); unmapped → OTHER
<info><severity>alert_levelseverity_map override, else default (Extreme→RED, Severe→ORANGE, Moderate/Minor→GREEN)
<info><headline> / <event>titlefirst non-empty (GeoPop may enrich it further)
<area> geometry / geocodeslocationpolygon centroid → circle centre → country centroid fallback
<area><geocode> EMMA_ID / configcountriesdefault_countries, else ISO-2 from EMMA_ID prefixes

Location resolution

Many weather authorities (ČHMÚ included) send only area geocodes (e.g. CISORP municipality codes, EMMA_ID region codes) with no coordinates. The transformer resolves a point in this order:

  1. Polygon centroid, if the CAP <area> carries one
  2. Circle centre, if present
  3. Country centroid of the first resolved country (a built-in table)

Anchoring a geocode-only, country-wide alert at the country centroid is intentional — it lets the pipeline's GeoPop step derive the authoritative country and exposed population there. Precise per-geocode placement (CISORP→coordinate lookups) is a planned enhancement.

Scoring

CAP messages carry a severity but not the physical magnitudes USGS/GDACS provide, so CAP events are scored from alert level + GeoPop population at the resolved location. See Scoring & Priority.

UNPROFILED safety net. A source with no transform_config (or whose message can't be placed) is still accepted and stored raw with processing_status = UNPROFILED — never dropped. Once a config is added, those raw records can be replayed. This lets an organization start sending before its mapping is finalized.

Custom logic (escape hatch)

For an authority whose mapping can't be expressed in config, a Go profile (implementing the cap.Profile interface in the Event Processor) can be registered under the source slug to take over transformation. Config-driven onboarding is the default; code profiles are the exception.

Onboarding a new organization

1. Register the organization

curl -X POST https://<backend>/api/v1/cap-sources \
  -H "Authorization: Bearer <admin-jwt>" -H "Content-Type: application/json" \
  -d '{
    "slug": "chmu",
    "name": "Czech Hydrometeorological Institute",
    "contact_email": "ops@example.org",
    "transform_config": {
      "language": "en",
      "event_type_map": {"Rain Flood": "FLOOD"},
      "severity_map": {"Extreme": "RED", "Severe": "ORANGE"},
      "default_countries": ["CZE"],
      "geocode": {"system": "CISORP", "strategy": "polygon_then_centroid"}
    }
  }'

2. Issue an API key

curl -X POST https://<backend>/api/v1/cap-sources/<id>/api-keys \
  -H "Authorization: Bearer <admin-jwt>" -H "Content-Type: application/json" \
  -d '{"name": "chmu-prod"}'
# → returns plaintext_key ONCE. Store it securely and hand it to the org.

3. Hand the key to the organization

The organization sends each CAP alert to POST /v1/ingest/webhook with X-API-Key: <plaintext_key>. Every resulting event is attributed to them via cap_source_id.

4. Verify

A push with a valid key and a configured source returns 202 { status: "accepted" } and creates a disaster_event. Tune transform_config with PATCH /api/v1/cap-sources/{id} as needed — no redeploy required.

Data model

TablePurpose
cap_sourcesOne row per organization: slug, name, is_active, transform_config
cap_source_api_keysSHA-256 key hashes, key_prefix, last_used_at, revoked_at
disaster_events.cap_source_idFK attributing a CAP event to its originating organization
raw_event_recordsEvery received body (incl. UNPROFILED / ERROR), for audit & replay

On this page