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
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-Keyheader. The Event Processor hashes it and looks it up, which identifies the originating organization — so every resulting event is attributed to that org viadisaster_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.
| Method | Path | Purpose |
|---|---|---|
GET | /api/v1/cap-sources | List sources + active key counts |
POST | /api/v1/cap-sources | Register 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-keys | List keys (incl. revoked) |
POST | /api/v1/cap-sources/{id}/api-keys | Issue 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.
| HTTP | Meaning |
|---|---|
202 Accepted | Stored. status: "accepted" (transformed → pipeline) or status: "UNPROFILED" (stored raw) |
401 Unauthorized | Missing / invalid / revoked key, or source deactivated |
413 Payload Too Large | Body exceeds 8 MiB |
422 Unprocessable Entity | Body persisted but failed CAP validation (raw kept for debugging) |
503 Service Unavailable | Persistence 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
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 input | Resolves to | Rule |
|---|---|---|
<info><event> (or <eventCode>) | event_type | event_type_map (case-insensitive); unmapped → OTHER |
<info><severity> | alert_level | severity_map override, else default (Extreme→RED, Severe→ORANGE, Moderate/Minor→GREEN) |
<info><headline> / <event> | title | first non-empty (GeoPop may enrich it further) |
<area> geometry / geocodes | location | polygon centroid → circle centre → country centroid fallback |
<area><geocode> EMMA_ID / config | countries | default_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:
- Polygon centroid, if the CAP
<area>carries one - Circle centre, if present
- 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
| Table | Purpose |
|---|---|
cap_sources | One row per organization: slug, name, is_active, transform_config |
cap_source_api_keys | SHA-256 key hashes, key_prefix, last_used_at, revoked_at |
disaster_events.cap_source_id | FK attributing a CAP event to its originating organization |
raw_event_records | Every received body (incl. UNPROFILED / ERROR), for audit & replay |
Related
Google Flood Forecasting
Integration with Google's Flood Forecasting API to ingest forward-looking flood predictions, gated by a severity filter and a GeoPop population check before they enter the event pipeline.
Sending CAP Data
A step-by-step integration guide for external organizations pushing Common Alerting Protocol (CAP v1.2) alerts into TerraGuard — request format, message requirements, responses, retries, and FAQ.