Chapter 4

Chapter 4: The API Interface — Endpoints and SDK

Chapters 2 and 3 defined what a flood observation, a prediction and a warning look like as data. This chapter is about how those objects travel: the operation groups that PHASE-2 of WIA-ENE-033 names, the concrete /api/v1/... paths that the reference TypeScript SDK actually calls, the headers and error conventions on the wire, the discovery document, and the shell CLI that wraps the same calls. Where the specification text, the README and the SDK disagree, the chapter says so and proposes a rule an implementer can defend in an audit.

4.1 Operation Groups and Concrete Paths

PHASE-2 does not list URLs. It defines five operation groups and says that each is documented in an OpenAPI document. The groups are the normative vocabulary: an implementation declares, in its interoperability profile, which of them it supports, partially supports or excludes. The concrete paths come from two other sources, the README endpoint list and the FloodPredictionSDK class in api/typescript/src/index.ts. The OpenAPI file that PHASE-2 refers to (api/openapi.yaml) is not present in the published repository, so for v1.0 the SDK is the most precise statement of the wire contract that exists.

Operation group (PHASE-2)Purpose as statedConcrete v1.0 pathsObservation
gaugeIngestSubmit gauge observations (water level, discharge)noneNo write path for observations exists in the SDK, README or CLI. Only read paths under /monitoring exist.
forecastQueryRetrieve ensemble flood-forecast results/flood/predict, /flood/prediction/{id}, /flood/predictions, /flood/risk-map/{regionId}Predictions are created by a POST and read back by ID or by listing.
warningPublishPublish flood warnings to subscribers; MUST also emit CAP 1.2/alert/create, /alert/active, /alert/{id}, /alert/{id}/update, /alert/{id}/cancelThe SDK returns only the WIA-native FloodAlert; no path returns CAP.
simulationSubmitSubmit hindcast or hypothetical scenarios/flood/simulationSynchronous in the SDK; there is no job or status resource.
evacuationRouteQuery evacuation routes for a polygon/evacuation/routes[/{id}], /evacuation/shelters[/{id}]The SDK queries from a single origin point, not a polygon.
(not a named group)—/monitoring/*, /analytics/*Real-time monitoring and KPI reporting exist only as paths.

Two practical consequences follow. First, a deployment that ingests gauge data must, for now, define its own ingest path or rely on the telemetry protocols listed in PHASE-2 §12.1 (MQTT, HTTP or WebSocket, carrying JSON, XML or CSV). In its profile manifest it should mark gaugeIngest as partial or excluded with a reason rather than claim support for an interface nobody can test against. Second, monitoring and analytics paths have no operation group to be declared under. A careful profile lists them as extensions and states which SDK methods it serves.

The base URL problem

The SDK builds each request URL as config.endpoint + path, and every path already begins with /api/v1. The README, however, configures the SDK with endpoint: 'https://api.wia.org/ene-033/v1', and the CLI uses the same string as its default. Combined, the version segment appears twice. The README's cURL example uses yet a third form, /ene-033/v1/flood/predict, with no /api/v1 at all.

// README example: endpoint already carries a version segment
endpoint = "https://api.wia.org/ene-033/v1"
path     = "/api/v1/flood/predict"        // hard-coded in the SDK
request  = "https://api.wia.org/ene-033/v1/api/v1/flood/predict"   // doubled

// Recommended: configure the origin (plus any gateway prefix) only
endpoint = "https://flood.example.go.kr"
request  = "https://flood.example.go.kr/api/v1/flood/predict"

The rule this book adopts: endpoint is the origin plus any gateway prefix the operator adds, and never contains the API version. The version lives in the path the SDK appends and in the X-WIA-Version header. A server that must also accept the README form can add a rewrite at its gateway, but should not advertise it. Note too that api.wia.org appears only in examples; nothing in the repository establishes it as a running service, so examples in this chapter use a placeholder host, flood.example.go.kr.

4.2 Prediction API

The prediction group is the heart of the interface. Five SDK methods map onto five paths.

Method and pathSDK methodRequestResponse dataGroup
POST /api/v1/flood/predictpredictFloodRiskFloodPredictionRequestFloodPredictionResultforecastQuery
GET /api/v1/flood/prediction/{id}getPredictionpath parameterFloodPredictionResultforecastQuery
POST /api/v1/flood/simulationrunSimulationFloodPredictionRequest & { scenarios: string[] }FloodPredictionResultsimulationSubmit
GET /api/v1/flood/risk-map/{regionId}getRiskMappath parameteruntyped (any)forecastQuery
GET /api/v1/flood/predictionslistPredictionsPaginationParams + regionId, riskLevelPaginatedResponse<FloodPredictionResult>forecastQuery

Creating a prediction

In the SDK types only location and timeHorizon are required. The include* flags, resolution, runSimulation and simulationScenarios are optional, whereas the PHASE-1 data format marks the source flags as required (Chapter 3 discusses the difference). A server should therefore apply explicit, documented defaults when a flag is missing and echo what it used, for example through the contributingFactors it returns. Silently treating a missing includeDamStatus as false can produce a forecast that ignores a reservoir release upstream.

The location object is a full Location, and name is required alongside regionId and coordinates. The README's SDK example omits name and would not type-check. A minimal valid body looks like this:

{
  "location": {
    "regionId": "REG-EXAMPLE-001",
    "name": "Example riverside district",
    "coordinates": { "latitude": 37.52, "longitude": 127.04 }
  },
  "timeHorizon": "6h",
  "resolution": "medium",
  "includePrecipitation": true,
  "includeRiverLevels": true,
  "includeDamStatus": true,
  "includeDrainage": true,
  "includeTopography": false,
  "runSimulation": false
}

Two values need validation on the server even though the SDK already exports helpers for them: timeHorizon must be one of 1h, 6h, 24h or 72h (isValidTimeHorizon), and resolution one of coarse, medium or fine (isValidResolution). A client that sends 48h should receive a 4xx response with an explanation, not the nearest supported horizon.

The same call over plain HTTP looks like this. The -w option is worth keeping in scripts, because curl -s on its own discards the status code.

# Save the request body above as predict-request.json first.
# -w prints the HTTP status, which the response body alone does not carry.
curl -sS -X POST "https://flood.example.go.kr/api/v1/flood/predict" \
  -H "Authorization: Bearer $WIA_API_KEY" \
  -H "Content-Type: application/json" \
  -H "X-WIA-Standard: ENE-033" \
  -H "X-WIA-Version: 1.0.0" \
  --data @predict-request.json \
  -w '\nHTTP %{http_code}\n'

Reading, listing and mapping

getPrediction retrieves a stored result by predictionId. A server should keep results immutable once returned: if new observations change the picture, it issues a new prediction with a new ID rather than editing the old one. That keeps the audit trail intact, so a warning can always be traced to the forecast on which it was based.

listPredictions serialises every supplied parameter into the query string, so a call with { page: 1, limit: 20, riskLevel: RiskLevel.LEVEL_3 } becomes ?page=1&limit=20&riskLevel=LEVEL-3. Whether pages start at 0 or 1 is not specified anywhere in v1.0. Pick one, publish it in the profile, and return the effective page in PaginatedResponse so clients can detect a mismatch.

getRiskMap is typed as any. Until the specification defines a risk-map schema, a server should return a GeoJSON FeatureCollection whose features carry a riskLevel property using the same LEVEL-0…LEVEL-4 tokens, since that is what every other object in the standard uses.

Simulation

runSimulation takes a FloodPredictionRequest intersected with an extra required field, scenarios: string[]. The request type already has an optional simulationScenarios field, so a body can carry two lists with the same meaning. The book's rule is that scenarios governs on this path and simulationScenarios is ignored. Scenario names are free strings, but the result's scenarios object has fixed bestCase, worstCase and mostLikely members, so a server should reject names it cannot map.

Two-dimensional inundation runs can take far longer than the SDK's default 30-second timeout. Because v1.0 has no job resource, a long simulation either blocks the client until timeout or has to be kept short. Deployments with heavy models should answer the POST quickly with a stored result that the client polls through getPrediction, and should document that behaviour, since the SDK does not know about it.

4.3 Monitoring API

The monitoring group exposes current observations and a summary dashboard. All of its list methods accept an optional regionId, sent as a query parameter.

Path (GET)SDK methodResponse data
/api/v1/monitoring/riversmonitorRiversRiverLevelData[]
/api/v1/monitoring/rivers/{riverId}getRiverStatusRiverLevelData
/api/v1/monitoring/damsmonitorDamsDamStatusData[]
/api/v1/monitoring/dams/{damId}getDamStatusDamStatusData
/api/v1/monitoring/rainfallmonitorRainfalluntyped (any)
/api/v1/monitoring/drainagemonitorDrainageDrainageData[]
/api/v1/monitoring/dashboardgetMonitoringDashboardMonitoringDashboard

The rainfall endpoint stands out: PrecipitationData is fully defined in the types, yet monitorRainfall returns any. Returning PrecipitationData[], one element per rain gauge or radar cell, keeps the rainfall path consistent with the others and lets clients use the same type they already receive inside contributingFactors.

MonitoringDashboard bundles an overview (active alerts, high-risk areas, evacuated population, operational shelters), a regions list with per-region riskLevel and evacuation progress, sensor counts (total, active, offline, maintenance) and a system block with uptime, latency and data-quality figures. Its refreshInterval, in seconds, is the server telling the client how often polling is useful. Clients should honour it rather than hammering the endpoint during an event, which is exactly when load peaks.

The SDK is poll-only. PHASE-2 §12.1 lists MQTT and WebSocket among the protocols of the hydrological network, but no subscription interface is defined for API consumers. Push delivery to downstream consumers therefore belongs to the CAP feed and to the alert distribution channels covered in Chapter 5, not to the monitoring API.

Pitfall: monitoring data is not a warning

A RiverLevelData.status.condition of danger is an observation that a threshold has been crossed. It is not an issued warning, and a consumer must not present it to the public as one. Only objects returned by the alert paths carry an issuer and a legal meaning.

4.4 Alert and Evacuation API

Alert life cycle

The alert paths implement a create, read, update and cancel cycle for FloodAlert objects.

Method and pathSDK methodBodyResponse data
POST /api/v1/alert/createcreateAlertOmit<FloodAlert, 'alertId' | 'timestamp'>FloodAlert
GET /api/v1/alert/activegetActiveAlerts— (optional regionId)FloodAlert[]
GET /api/v1/alert/{id}getAlert—FloodAlert
PUT /api/v1/alert/{id}/updateupdateAlertPartial<FloodAlert>FloodAlert
POST /api/v1/alert/{id}/cancelcancelAlert{ reason?: string }none (void)

The create body omits alertId and timestamp: the server assigns both. That is the correct design, because the issuing system, not the client, is the authority for when an alert came into force. The server should also check that issuer.organizationId matches the authenticated caller. Otherwise any key holder could issue a warning in another authority's name.

The update path uses PUT with a partial body, which is really merge-patch semantics. Implementers should say so in their documentation, and should decide how arrays are merged: a partial body with a new regionIds list is normally meant to replace the old list, not to be appended to it. Every change of alert.level is operationally a new message to the public. The server should keep the previous version and, on the CAP side, emit an Update message that references the original identifier.

Cancellation is a POST with an optional reason, not a DELETE. The distinction matters: a cancelled alert must remain retrievable by ID for the audit trail, and the CAP feed must carry a Cancel message so that downstream systems withdraw it. Requiring the reason at the server, even though the SDK makes it optional, is good practice for later review of false alarms.

Retries and duplicate warnings

POST /alert/create is not idempotent. If a request times out after the server has committed the alert, a naïve retry creates a second one and the public may receive the same warning twice. v1.0 defines no idempotency key. Until it does, a client that sees NETWORK_ERROR on a create should first call getActiveAlerts(regionId) and look for an alert it may already have issued before trying again. A server can help by accepting a client-generated request identifier in a header and returning the existing alert when it sees the same identifier twice; that is an extension and should be declared as one.

The CAP 1.2 obligation

PHASE-2 says that the warningPublish operation MUST emit warnings in CAP 1.2 in addition to the WIA-native envelope. None of the alert paths returns CAP, and the SDK has no method for it. The requirement is therefore met, if at all, outside the SDK. A conforming deployment should publish a CAP 1.2 document (media type application/cap+xml is common practice for CAP feeds) for every create, update and cancel, and link to that feed from its discovery manifest (§4.7). Chapter 3 gives the field mapping; the point here is that the CAP rendering is a publication side effect of the alert API, not a separate manual step.

Evacuation paths

Method and pathSDK methodParameters / bodyResponse data
GET /api/v1/evacuation/routesgetEvacuationRouteslatitude, longitude, optional type = shelter | high-groundEvacuationRoute[]
GET /api/v1/evacuation/routes/{routeId}getEvacuationRoute—EvacuationRoute
GET /api/v1/evacuation/sheltersgetEvacuationSheltersoptional regionIdEvacuationShelter[]
GET /api/v1/evacuation/shelters/{shelterId}getShelterCapacity—EvacuationShelter
PUT /api/v1/evacuation/shelters/{shelterId}updateShelterStatusPartial<EvacuationShelter>EvacuationShelter

The README lists a separate GET /api/v1/evacuation/capacity for shelter occupancy. The SDK has no such path: its getShelterCapacity method reads the whole shelter object from /evacuation/shelters/{shelterId}, whose capacity member holds total, current and available. The SDK form is the better one, because capacity is meaningless without the shelter's identity, status and accessibility. A server that wants to offer the README path can serve it as an alias returning capacity objects keyed by shelterId, but clients should target the SDK path.

Route queries are point-based: the client sends its origin coordinates, and each returned EvacuationRoute carries a path, distance, estimated time, safetyScore, hazards (flooded segments with depth, blocked roads) and alternatives. PHASE-2 describes evacuationRoute as a query "for a polygon", for example routes out of a whole warned zone. Nothing in v1.0 carries a polygon on this path, so a polygon query is a profile-declared extension. Route answers go stale quickly during an event, so servers should set short cache lifetimes and fill in updates whenever a segment changes status.

Shelter updates are a write operation that affects where people are sent. They need the same authorisation care as alert creation, typically limited to shelter operators and the emergency office.

4.5 Analytics API

Method and pathSDK methodInputResponse data
GET /api/v1/analytics/kpigetKPIDashboardoptional regionId, startDate, endDateKPIDashboard
POST /api/v1/analytics/reportgenerateReport{ regionId, period: DateRangeFilter }FloodResponseReport
GET /api/v1/analytics/report/{reportId}getReport—FloodResponseReport
GET /api/v1/analytics/eventslistHistoricalEventsPaginationParams, DateRangeFilter, regionIdPaginatedResponse<FloodPredictionResult>

KPIDashboard groups prediction KPIs (accuracy by horizon, lead time, false-alarm and missed-event rates) and response KPIs (delivery time and success, evacuation completion, damage indicators). Chapter 7 treats these as reporting objects. At the API level, three details matter.

PaginatedResponse returns items, total, page, limit and hasMore. Offset pagination is simple, but results shift when new predictions arrive during an event. Analytics consumers exporting a period should therefore bound the query with a fixed endDate in the past, so that the page set cannot change beneath them.

4.6 Authentication, Headers and Errors

This is where the three sources diverge most. The table puts them side by side.

AspectPHASE-2 / PHASE-4 textReference SDKCLI
Client authenticationPHASE-2: "OAuth 2.1 (RFC 9700)" with mTLS for institutional callers; PHASE-4: OAuth 2.0 + MFAAuthorization: Bearer <apiKey> with a static keySame bearer key, from WIA_API_KEY or the config file
Standard headersnot specifiedX-WIA-Standard: ENE-033, X-WIA-Version: 1.0.0, Content-Type: application/jsonSame four headers
Error bodyProblem Details (RFC 9457)Reads message from the body; falls back to statusTextPrints the raw body
Rate limiting429 per RFC 6585 with retry-after semanticsReported as HTTP_429; response headers are not exposedStatus code not captured
Timeout / retries—30 s default timeout; retries option (default 3) exists but is never usedNo timeout set
Response shape—Server body is the bare object; the SDK wraps it in ApiResponse<T>Expects the server body itself to be { success, data }

Authentication

A correction first. RFC 9700 is the OAuth 2.0 Security Best Current Practice. OAuth 2.1 is still an IETF Internet-Draft and has no RFC number. The sensible reading of PHASE-2 is "OAuth 2.0 deployed according to RFC 9700", which in practice means authorization code with PKCE for interactive clients, client credentials for machine callers, sender-constrained tokens where possible, and no implicit grant. The mTLS requirement for institutional callers can be met with RFC 8705 (OAuth 2.0 Mutual-TLS Client Authentication and Certificate-Bound Access Tokens).

The SDK's static bearer key is a convenience for development and small single-tenant deployments. Because the key is placed in the same Authorization: Bearer header an OAuth access token would use, a deployment can switch to short-lived tokens without changing the SDK: pass the current access token as apiKey and construct a new client when it is refreshed. Whatever the mechanism, the server should scope credentials by role. A key that can read monitoring data must not be able to call /alert/create or PUT /evacuation/shelters/{id}.

The response envelope

Every SDK method returns ApiResponse<T>: success, optional data, optional error (code, message, details) and metadata (timestamp, version). The envelope is created by the SDK: on a 2xx response the whole JSON body becomes data. The CLI, by contrast, runs jq -e '.success' on the raw server body and reads .data.predictionId, so it expects the server itself to send the envelope. A server cannot satisfy both at once. If it returns a bare object, the CLI reports failure. If it returns an envelope, SDK users receive data.data.

This book recommends the SDK's reading, bare resource objects on the wire, for three reasons: it matches RFC 9457 error handling (the status code carries success or failure), it matches how OpenAPI documents usually describe responses, and the SDK is the more complete artefact. The CLI then needs a small fix, described in §4.8. Whichever choice a deployment makes, it must be written into the profile manifest. For the same reason, remember the README's SDK example: it prints prediction.riskLevel, but the result is in prediction.data.riskLevel.

Errors in the SDK

The SDK's request helper produces two kinds of error code: HTTP_<status> when the server answers with a non-2xx status, and NETWORK_ERROR when an exception is thrown. Several situations end up in the second bucket that are not network errors at all:

For HTTP errors the SDK uses data.message as the message. A server that follows RFC 9457 puts its explanation in title and detail, so the SDK falls back to the reason phrase, which may be empty when the transport is HTTP/2. The full problem object is still available in error.details. A problem response for an invalid horizon would look like this:

HTTP/1.1 422 Unprocessable Content
Content-Type: application/problem+json

{
  "type": "https://flood.example.go.kr/problems/invalid-time-horizon",
  "title": "Unsupported time horizon",
  "status": 422,
  "detail": "timeHorizon '48h' is not one of 1h, 6h, 24h, 72h",
  "instance": "/api/v1/flood/predict"
}

A server can include a message member as an extension (RFC 9457 allows extension members) to serve today's SDK. Alternatively, a deployment that maintains its own SDK build can make the handler tolerant. The sketch below reads the body as text first, accepts empty bodies and surfaces Retry-After:

// Sketch of a more tolerant response handler (not part of the v1.0 SDK).
const text = await response.text();                 // never assume a body
let body: any = undefined;
if (text.length > 0) {
  try { body = JSON.parse(text); } catch { body = { raw: text.slice(0, 500) }; }
}
if (!response.ok) {
  return {
    success: false,
    error: {
      code: `HTTP_${response.status}`,
      // RFC 9457 uses detail/title; keep message for older servers
      message: body?.detail ?? body?.title ?? body?.message ?? response.statusText,
      details: { body, retryAfter: response.headers.get('Retry-After') },
    },
  };
}
return { success: true, data: body as T };           // 204 gives data === undefined

One more note for maintainers. Type-checking the published sources with tsc reports four TS2484 errors ("Export declaration conflicts with exported declaration") at the end of types.ts. The trailing export type { Timestamp, Coordinates, Location, GeoJSON } block re-exports names that are already exported. Deleting that block removes the errors without changing the public API.

Worked example: predict, decide, warn

The following TypeScript puts the pieces together. It requests a six-hour forecast, retries only transient failures with exponential back-off, compares risk levels numerically, stops at a human decision point before issuing anything, and treats an unconfirmed create as a situation to reconcile rather than retry. Identifiers, the host name and the contact number are placeholders.

import {
  FloodPredictionSDK,
  FloodPredictionResult,
  RiskLevel,
  AlertSeverity,
  AlertUrgency,
  AlertCertainty,
  FloodEventType,
  formatRiskLevel,
} from '@wia/ene-033';

const sdk = new FloodPredictionSDK({
  apiKey: process.env.WIA_API_KEY ?? '',
  endpoint: 'https://flood.example.go.kr', // origin only: the SDK adds /api/v1
  timeout: 15_000,                         // default is 30 000 ms
});

// Compare levels as numbers, not as 'LEVEL-n' strings.
const LEVEL_RANK: Record<RiskLevel, number> = {
  [RiskLevel.LEVEL_0]: 0,
  [RiskLevel.LEVEL_1]: 1,
  [RiskLevel.LEVEL_2]: 2,
  [RiskLevel.LEVEL_3]: 3,
  [RiskLevel.LEVEL_4]: 4,
};

// Only failures that did not reach application logic are worth retrying.
function isTransient(code: string | undefined): boolean {
  return code === 'NETWORK_ERROR' || code === 'HTTP_429' ||
         code === 'HTTP_502' || code === 'HTTP_503' || code === 'HTTP_504';
}

async function predictWithRetry(attempts = 3): Promise<FloodPredictionResult> {
  for (let i = 1; ; i++) {
    const res = await sdk.predictFloodRisk({
      location: {
        regionId: 'REG-EXAMPLE-001',
        name: 'Example riverside district',
        coordinates: { latitude: 37.52, longitude: 127.04 },
      },
      timeHorizon: '6h',
      resolution: 'medium',
      includePrecipitation: true,
      includeRiverLevels: true,
      includeDamStatus: true,
      includeDrainage: true,
    });
    if (res.success && res.data) return res.data;
    if (!isTransient(res.error?.code) || i >= attempts) {
      throw new Error(`predict failed: ${res.error?.code} ${res.error?.message}`);
    }
    await new Promise((r) => setTimeout(r, 2 ** i * 1000)); // 2 s, 4 s, ...
  }
}

export async function assessAndWarn(
  officerApproves: (p: FloodPredictionResult) => Promise<boolean>,
): Promise<string | null> {
  const p = await predictWithRetry();
  const label = formatRiskLevel(p.riskLevel);
  console.log(`${p.predictionId}: ${label.en}, ` +
              `probability ${p.probability}%, confidence ${p.confidence}%`);

  if (!p.evacuationRequired || LEVEL_RANK[p.riskLevel] < 3) return null;
  if (!(await officerApproves(p))) return null;       // human decision point

  const onset = new Date();
  const expiry = new Date(onset.getTime() + 6 * 3_600_000);
  const created = await sdk.createAlert({
    issuer: {
      organizationId: 'ORG-EXAMPLE-DISTRICT',
      name: 'Example District Disaster Safety Office',
      authorityLevel: 'municipal',
    },
    alert: {
      level: p.riskLevel,
      severity: LEVEL_RANK[p.riskLevel] >= 4 ? AlertSeverity.EXTREME : AlertSeverity.SEVERE, // LEVEL-4 maps to extreme (Chapter 3 example table)
      urgency: AlertUrgency.EXPECTED,
      certainty: AlertCertainty.LIKELY,
    },
    affectedAreas: {
      regionIds: p.affectedAreas,
      geoJson: p.inundationArea.geoJson,
      population: p.inundationArea.affectedPopulation,
      buildings: p.inundationArea.affectedBuildings,
    },
    event: {
      type: FloodEventType.RIVER_FLOOD,
      onset: onset.toISOString(),
      expiry: expiry.toISOString(),
      peakTime: p.peakTime,
    },
    instructions: {
      ko: '하천 수위 상승. 저지대 주민은 지정 대피소로 이동하십시오.',
      en: 'River level rising. Residents of low-lying areas move to designated shelters.',
      actions: ['evacuate-low-lying', 'avoid-underground'], // deployment-defined action codes, not free text
      evacuationRequired: true,
      evacuationZones: p.affectedAreas,
    },
    contact: {
      emergency: '119',
      information: '+82-2-000-0000',            // placeholder
      website: 'https://flood.example.go.kr/alerts',
    },
  });

  if (!created.success || !created.data) {
    // Do NOT blindly retry a create: the alert may already exist.
    // Reconcile with getActiveAlerts() before issuing again.
    throw new Error(`alert not confirmed: ${created.error?.code}`);
  }
  return created.data.alertId;
}

Three details are deliberate. The LEVEL_RANK map exists because comparing RiskLevel values with >= compares strings. It works for LEVEL-0 to LEVEL-4 only because single digits sort lexically, and would break silently if the scale ever grew. The test combines evacuationRequired with the level instead of trusting either alone. And the officerApproves callback marks where the decision to warn belongs to an accountable person. Chapter 5 explains which cases the protocol allows to be automatic and which it sends to manual review.

4.7 Discovery

PHASE-2 specifies that operation discovery uses the well-known URI mechanism of RFC 8615 at /.well-known/wia/ene-033, and that manifest endpoints are signed with Sigstore using a DSSE envelope, with keys rotated according to deployment policy. Beyond the path and the signing method, v1.0 does not fix the manifest's fields. Annex I, however, says what a profile manifest must express: each requirement or group as supported, partial (with a reason) or excluded (with a justification). Annex J adds that the topology is declared in the manifest.

The following is an illustrative discovery document that combines those rules with the gaps discussed in this chapter. The field names are this book's proposal, not normative text.

{
  "standard": "WIA-ENE-033",
  "phase": "PHASE-2-API-INTERFACE",
  "specVersion": "1.0",
  "apiBase": "https://flood.example.go.kr/api/v1",
  "profile": "WIA-P2-API-INTERFACE-Edge-only/1",
  "topology": "single-tenant-edge",
  "operationGroups": {
    "gaugeIngest":      { "status": "excluded", "justification": "observations arrive over the agency's existing telemetry feed" },
    "forecastQuery":    { "status": "supported" },
    "warningPublish":   { "status": "partial",  "reason": "CAP 1.2 feed published separately; see links.cap" },
    "simulationSubmit": { "status": "excluded", "justification": "no 2D inundation model deployed" },
    "evacuationRoute":  { "status": "supported" }
  },
  "auth": ["bearer-api-key"],
  "links": {
    "openapi": "https://flood.example.go.kr/openapi.json",
    "cap": "https://flood.example.go.kr/cap/active.xml"
  }
}

Several properties make such a document useful. It tells a client where the API base is, which removes the guesswork of §4.1. It is honest about gaugeIngest and about the CAP obligation, which Annex I explicitly asks for ("without misrepresenting partial conformance as full conformance"). It names a profile with a major version, following Annex I's naming convention. And it links the OpenAPI document and the CAP feed, the two artefacts a consumer needs next. The signed DSSE envelope wraps this JSON as its payload. Clients that act on the manifest, for instance to discover the CAP feed they will relay to the public, should verify the signature and the Rekor transparency-log entry before trusting it.

Keep the manifest small and cacheable. It changes when the deployment changes, not per request, and a failed fetch of the manifest must never block the alert path itself.

4.8 Command-Line Interface

cli/flood-prediction.sh is a Bash wrapper around curl that sends the same headers as the SDK. It reads WIA_API_ENDPOINT and WIA_API_KEY from the environment, or values stored by the config command in ~/.wia/flood-prediction.conf (written with mode 600). Output is formatted by jq when it is installed.

CommandOptionsCalls
configinteractive prompts—
predict-risk--region, --horizon (default 24h)POST /flood/predict
monitor-river--river-id, --thresholdGET /monitoring/rivers/{id}
monitor-dam--dam-idGET /monitoring/dams/{id}
create-alert--region, --level (default LEVEL-3), --messagePOST /alert/create
get-alerts--regionGET /alert/active
evacuation-route--location, --destination-typeGET /evacuation/routes
shelter-status--regionGET /evacuation/shelters
dashboard--regionGET /monitoring/dashboard
kpi--region, --start-date, --end-dateGET /analytics/kpi

A typical operator session follows. The printed values are illustrative, and use the same illustrative gauge ladder as the Chapter 3 example (attention 3.5 m, warning 4.5 m, danger 5.0 m); the 4.0 m value is the operator's own --threshold option, not a gauge level.

$ export WIA_API_ENDPOINT="https://flood.example.go.kr"   # origin only
$ ./cli/flood-prediction.sh config                         # prompts, then writes
                                                           # ~/.wia/flood-prediction.conf (mode 600)
$ ./cli/flood-prediction.sh monitor-river --river-id HAN-001 --threshold 4.0
[ℹ] Monitoring river: HAN-001

Current Level: 4.2m
Warning Level: 4.5m
Danger Level: 5.0m
Status: attention

[⚠] ⚠️  Water level (4.2m) exceeds threshold (4.0m)!
{ ...full JSON response... }

$ ./cli/flood-prediction.sh get-alerts --region REG-EXAMPLE-001
$ ./cli/flood-prediction.sh kpi --region REG-EXAMPLE-001 \
      --start-date 2026-07-01T00:00:00Z --end-date 2026-07-31T23:59:59Z

The CLI is useful for smoke tests and for operators at a terminal, but in v1.0 it should not be used to issue real warnings. Reading the script shows why:

These are fixable in a few lines each: take coordinates as options, build JSON with jq -n --arg instead of a heredoc, pass headers to curl through -H @file or a config file read from standard input, capture the status with -w, and remove the advertised but missing commands. Until then, treat the CLI as a read-mostly diagnostic tool.

Summary of the chapter

The interface of WIA-ENE-033 v1.0 is best understood in layers. PHASE-2 supplies the vocabulary (five operation groups, OAuth-based authentication, RFC 9457 errors, RFC 8615 discovery, signed manifests and a CAP 1.2 obligation). The SDK supplies the paths and types that code actually uses. The CLI and README are thin, partly inconsistent wrappers around the same calls. A deployment that wants to be auditable should settle the open points explicitly: origin-only base URLs, bare resources on the wire, a documented page base, an idempotency strategy for alert creation, a CAP feed linked from the manifest, and honest partial or excluded entries for gaugeIngest and anything else it does not serve. Chapter 5 turns from transport to what happens inside the forecasting node: the models, the ensemble and the rules that decide when a prediction becomes a warning.