Chapter 3 defined what a WIA-ENE-057 record looks like at rest. This chapter is about records in motion: the PHASE-2 REST interface through which field teams, satellite pipelines and dashboards submit observations, ask for a desertification risk assessment, manage restoration plans, subscribe to alerts and request UNCCD-style reports. It draws on two documents that are easy to confuse. spec/PHASE-2-API.md describes the resources, request bodies and responses. spec/PHASE-2-API-INTERFACE.md describes the layer around them — the signed manifest, the discovery document, the conformance tiers and the security baseline — that lets a third party check what an implementation actually supports.
The reading stance is the same as elsewhere in this book. The specification's JSON examples are used as they are written, with the running example location SAHEL-REGION-001, but every number in them is an illustrative example value, not a measurement from Mali or anywhere else. Where an example is internally inconsistent, the chapter says so and shows the arithmetic, because an API that reports a number it cannot reproduce is exactly the problem a data standard is meant to remove. Two tabs of the companion simulator are useful alongside it: the third tab (“3. Protocol”, linked below this chapter) lists the core endpoints and a WebSocket demonstration, and the second tab (“2. Algorithms”) contains a risk calculator that is discussed in Section 4.4.
All PHASE-2 resources live under a versioned prefix, /api/v1. The version in the path is the major version of the interface; minor and patch changes, which by the versioning rules must not break existing clients, do not change the prefix. The specification's header names a base URL on the host api.wia-ene-057.org, and some later examples use the same host. That host is an illustration of where a deployment might sit, not a live public service, and nothing in this chapter assumes it exists. In your own deployment the prefix is appended to your platform's origin, for example https://platform.example.org/api/v1.
The resources fall into six groups: monitoring, assessment, restoration, locations, alerts and reports. Two further groups — satellite raster ingestion and chunked bulk upload — are listed here because they share the same prefix and authentication, but their wire-level behaviour belongs to PHASE-3 and is covered in Chapter 5.
| Method | Path (under /api/v1) | Purpose | Success |
|---|---|---|---|
| POST | /monitoring/vegetation | Submit NDVI/EVI/LAI observation for a location | 201 |
| GET | /monitoring/vegetation/{locationId}/trend | Time series and statistics for one metric (period, metric) | 200 |
| GET | /monitoring/vegetation?page=&limit= | Paged listing of vegetation records | 200 |
| POST | /monitoring/soil | Submit soil moisture profile, chemistry and degradation flags | 201 |
| GET | /assessment/desertification/{locationId} | Compute or return the current desertification risk score | 200 |
| POST | /assessment/desertification/batch | Assess several locations in one call, with a summary | 200 |
| POST | /restoration/plan | Create a restoration plan for a location | 201 |
| PUT | /restoration/plan/{planId}/progress | Report progress against the plan | 200 |
| POST | /locations | Register a monitored location and its baseline | 201 |
| GET | /locations/{locationId} | Current status, active projects and counters | 200 |
| POST | /alerts/subscribe | Create an alert subscription (webhook and/or e-mail) | 201 |
| POST | /reports/unccd-ldn | Generate a Land Degradation Neutrality report package | 200 |
| POST | /ingest/satellite | Upload a GeoTIFF/NetCDF scene with metadata (Chapter 5) | see PHASE-3 |
| POST / GET | /bulk/upload/initiate, …/{uploadId}/chunk, …/complete, …/status | Chunked bulk upload and status polling (Chapter 5) | see PHASE-3 |
Three design choices run through the whole table. First, the location is the anchor. Almost every path either contains a locationId or requires one in the body; the monitoring, assessment and restoration resources are all views onto one registered place. Second, creation returns 201 and computation returns 200. Submitting a record or a plan creates a resource with its own identifier; an assessment or a report is a computed answer, even when it is triggered with POST. Third, responses carry hypermedia links. A successful vegetation submission returns a links object with self, location and trend; registering a location returns links to its monitoring and assessment resources. PHASE-3 lists HATEOAS as a base principle, and in practice it means a client can follow the links the server returns instead of assembling URLs from string templates — which matters when a deployment moves or when a later major version changes a path.
Two gaps are worth noting before writing a client. The table has no DELETE operations and no GET for an individual soil record, although PHASE-3 lists DELETE among the HTTP methods and the event catalogue in Chapter 5 includes a data.deleted event. An implementation that supports correction or withdrawal of records should expose it explicitly and document it in its OpenAPI document rather than let clients guess. And the record identifiers in the PHASE-2 examples (VEG-20251225-001, SOIL-20251225-001) are shorter than the PHASE-1 pattern (VEG-YYYYMMDD-REGION-NNN, as in VEG-20251225-SAHEL-001). This book follows the PHASE-1 pattern, because an identifier that encodes the region is unique across a multi-region deployment, while the short form is not.
PHASE-2 offers two ways to authenticate, and both end in the same place: an Authorization: Bearer … header on every request.
The simplest is a long-lived API key sent as a bearer token. It suits a single field server pushing data on behalf of one organisation, and it is how every example request in the specification is written. Its weakness is that it carries no scope: whoever holds the key can do whatever the key's owner can do, until the key is revoked.
The second is the OAuth 2.0 client-credentials grant. A machine client posts its client_id, client_secret and the scopes it wants to /oauth/token, and receives a short-lived access token:
POST /oauth/token
Content-Type: application/x-www-form-urlencoded
grant_type=client_credentials
&client_id=YOUR_CLIENT_ID
&client_secret=YOUR_CLIENT_SECRET
&scope=monitoring:read restoration:write
HTTP/1.1 200 OK
Content-Type: application/json
{
"access_token": "eyJhbGciOiJSUzI1NiIsInR5cCI6IkpXVCJ9...",
"token_type": "Bearer",
"expires_in": 3600,
"scope": "monitoring:read restoration:write"
}
expires_in is in seconds, so the example token lives for one hour. A well-behaved client caches the token, refreshes it shortly before expiry (a margin of a minute or so avoids requests racing the deadline), and requests a new one after any 401 response before giving up. Client credentials belong on a server, never in a mobile app or browser bundle; field apps that act for a person should obtain tokens through an interactive flow instead, as described in Chapter 5.
The specification names only two scopes, monitoring:read and restoration:write. That is enough to show the pattern — a resource group followed by an action — but not enough for a real deployment, where the partner that feeds satellite scenes should not be able to edit restoration budgets, and a donor dashboard should be able to read reports without submitting anything. A practical scope set, which is this book's suggestion and not part of the standard, extends the same pattern:
| Scope | Named in spec? | Grants |
|---|---|---|
monitoring:read | Yes | Read vegetation and soil records and trends |
monitoring:write | No (suggested) | Submit vegetation and soil records, satellite scenes, bulk uploads |
assessment:read | No (suggested) | Read single and batch risk assessments |
restoration:write | Yes | Create plans and report progress |
alerts:manage | No (suggested) | Create and remove alert subscriptions |
reports:generate | No (suggested) | Generate and download LDN report packages |
Whatever set you choose, list it in the OpenAPI document's security scheme so that auditors and client developers see the same list. The mechanics of the tokens themselves — JWT claims, signing algorithms, token binding to the TLS session and the authorization-code flow for interactive users — are protocol-level concerns and are covered in Chapter 5. One naming caution belongs here, because it appears in the standard's own reference list: IETF RFC 9700 is the OAuth 2.0 Security Best Current Practice, not “OAuth 2.1”. OAuth 2.1 is still an Internet-Draft at the time of writing. When you write your own documentation, cite RFC 6749 for the grant types and RFC 9700 for the hardening advice, and name them correctly.
The single most common integration error with this API is submitting data for a place the server has never heard of. The specification anticipates it with a dedicated error code: a request that names an unregistered location fails with INVALID_LOCATION, and the error's details.suggestion tells the caller to “register location first using POST /api/v1/locations”. The workflow is therefore fixed: register, then submit, then read.
Registration declares the identifier, a human-readable name, a reference point in WGS84, the area, the administrative hierarchy and a baseline. The baseline block in the example (vegetationCover, soilHealth, desertificationRisk, baselineYear) is what later trend and LDN calculations compare against, so it deserves more care than the specification's brief example suggests. Record where each baseline value came from and by which method; a baseline copied from a different sensor or a different risk model makes every later “change” partly an artefact of the switch.
Note also that the registration payload carries a single point (latitude 14.5, longitude −4.0) and an area of 10,000 ha. A point plus an area is enough for a catalogue, but it is not enough to clip a satellite raster or to compute a mean NDVI over the site. Deployments that do raster work should accept a boundary geometry as well — a GeoJSON polygon in longitude–latitude order, as discussed in Chapter 5 — and keep the point as a label position.
The README shows a one-line SDK call, client.submitRecord(record), from a package named @wia/desertification-prevention. That package is not published, and the api/typescript/ directory the README refers to is not in the repository, so the example below uses the platform's own fetch and nothing else. It registers the running example location, submits one vegetation observation, reads the 90-day NDVI trend and requests an assessment. Values are the specification's illustrative ones.
// wia-ene-057-client.ts — minimal fetch-based client (Node 18+ or browser)
const BASE = process.env.WIA_BASE_URL ?? "https://platform.example.org/api/v1";
const TOKEN = process.env.WIA_TOKEN!;
interface WiaErrorBody {
error: { code: string; message: string; details?: Record<string, unknown>;
timestamp?: string; requestId?: string };
}
export class WiaApiError extends Error {
constructor(public status: number, public code: string,
public requestId: string | undefined, public details: unknown) {
super(`${status} ${code}`);
}
}
async function call<T>(method: string, path: string, body?: unknown): Promise<T> {
const res = await fetch(`${BASE}${path}`, {
method,
headers: {
"Authorization": `Bearer ${TOKEN}`,
"Accept": "application/json",
...(body ? { "Content-Type": "application/json" } : {}),
},
body: body ? JSON.stringify(body) : undefined,
});
const text = await res.text();
const json = text ? JSON.parse(text) : undefined;
if (!res.ok) {
const e = (json as WiaErrorBody | undefined)?.error;
throw new WiaApiError(res.status, e?.code ?? "UNKNOWN", e?.requestId, e?.details);
}
return json as T;
}
export async function run(): Promise<void> {
// 1. Register the location (once).
try {
await call("POST", "/locations", {
locationId: "SAHEL-REGION-001",
name: "Sahel Test Region",
coordinates: { latitude: 14.5, longitude: -4.0, datum: "WGS84" },
area: { size: 10000, unit: "hectares" },
baseline: { vegetationCover: 45.2, soilHealth: 38.5, baselineYear: 2025 },
});
} catch (err) {
// A 409-style "already registered" answer is fine on re-runs.
if (!(err instanceof WiaApiError) || err.status !== 409) throw err;
}
// 2. Submit a vegetation observation (illustrative values).
const veg = await call<{ data: { recordId: string }; links: Record<string, string> }>(
"POST", "/monitoring/vegetation", {
locationId: "SAHEL-REGION-001",
timestamp: "2025-12-25T10:00:00Z",
sensorData: { ndvi: 0.35, evi: 0.28, lai: 1.2 },
source: "MODIS",
resolution: "250m",
quality: { cloudCover: 5, confidence: 0.92 },
});
console.log("stored", veg.data.recordId, "trend at", veg.links.trend);
// 3. Read the 90-day NDVI trend.
const trend = await call<{ statistics: { trend: string; changeRate: number } }>(
"GET", "/monitoring/vegetation/SAHEL-REGION-001/trend?period=90d&metric=ndvi");
console.log("trend", trend.statistics.trend, trend.statistics.changeRate);
// 4. Ask for the current risk assessment.
const risk = await call<{ riskScore: number; riskCategory: string }>(
"GET", "/assessment/desertification/SAHEL-REGION-001");
console.log("risk", risk.riskScore, risk.riskCategory);
}
Two details in this client are deliberate. The error handler reads the specification's error envelope and keeps requestId, because that identifier is what an operator needs to find the failing request in server logs. And the registration step tolerates a conflict answer on re-runs. The specification's status-code table does not list 409 Conflict, so what a server returns for a duplicate locationId is left open; an implementation should pick one behaviour (409 with an explicit code is the conventional choice) and document it, because clients will otherwise treat a harmless re-registration as a fatal error.
The trend example returns three monthly NDVI values — 0.45, 0.40 and 0.35 — with mean 0.40, min 0.35, max 0.45, trend "declining", changeRate −0.033 and significance 0.95. The mean, minimum and maximum follow directly from the three points. The change rate is consistent with a total drop of 0.10 spread over the three-month (90-day) window, that is −0.10 ÷ 3 ≈ −0.033 per month, but the response does not say “per month”; with a slope fitted through the three dated points the figure would be about −0.05 per month instead. Nor does it say what significance means — a confidence level, a p-value complement, or the output of a particular trend test. Three points are too few for any trend test to carry much weight. An implementation should therefore state, in its API documentation and ideally in the response itself, the unit of changeRate and the method and meaning of significance. Chapter 2 discusses suitable trend methods and why seasonality and rainfall must be considered before a decline is called degradation.
The assessment resource is where the standard stops describing data and starts drawing a conclusion, so it deserves the closest reading in this chapter.
GET /assessment/desertification/{locationId} returns an overall riskScore, a riskCategory and a factors object with four entries, each carrying a score, a weight, a status and the metrics that informed it. The weights in the example are fixed: vegetation 0.30, soil 0.25, climate 0.25 and human activity 0.20, which sum to 1.0. Each factor score lies between 0 and 100, and higher means worse — a vegetation score of 35 with status degraded sits alongside a human-activity score of 90 with status high-pressure. The natural reading, and the only one that makes the weights meaningful, is a weighted sum:
R = Σ wᵢ · sᵢ with Σ wᵢ = 1, 0 ≤ sᵢ ≤ 100 ⇒ 0 ≤ R ≤ 100
w = { vegetation: 0.30, soil: 0.25, climate: 0.25, humanActivity: 0.20 }
Applying that formula to the specification's own factor scores gives the following.
| Factor | Score sᵢ (example) | Weight wᵢ | wᵢ · sᵢ |
|---|---|---|---|
| Vegetation | 35.0 | 0.30 | 10.50 |
| Soil | 72.0 | 0.25 | 18.00 |
| Climate | 85.0 | 0.25 | 21.25 |
| Human activity | 90.0 | 0.20 | 18.00 |
| Weighted sum R | 67.75 | ||
The example response, however, prints riskScore: 68.5, and the same 68.5 reappears in the location registration baseline, the location status and the batch assessment. The difference of 0.75 cannot come from rounding, because 67.75 rounds to 67.8 or 68, never to 68.5. Either the specification's model contains a term it does not show, or the example number was written independently of the factors. For an implementer the lesson is practical: the overall score an API returns must be reproducible from the factors it returns. A conformance test should recompute Σ wᵢ·sᵢ from the response body and fail if the reported score differs by more than the declared rounding. An auditor or donor who sees a score that cannot be recomputed from its own components has no reason to trust the rest of the report.
The weighted sum is the easy part. Three harder parts are not specified, and an implementation has to decide and publish them.
ndvi 0.35 and cover 45.2 as its metrics and arrives at 35; the climate factor lists rainfall, a deviation from normal of −23.6 % and a drought index of −1.8 and arrives at 85. No normalisation function is given. Whether NDVI is scaled against a regional reference, a long-term site maximum or a fixed range changes the answer completely. Publish the function, its reference data and its version. (Chapter 2 also notes that an SPI of −1.8 falls in McKee's “severely dry” class, so the example's labelling elsewhere of that value as moderate drought should not be copied.)medium-risk, high-risk and critical for scores of 52.3, 68.5, 72.3, 81.5 and 82.1, but the specification never defines the thresholds between them. Many different band choices would be consistent with those five examples. An implementation must publish its thresholds as configuration, version them, and return the threshold set's version with each assessment, so that a change of bands is not mistaken for a change on the ground.projections block gives one-year and five-year scores (72.3 and 81.5 in the example). These are outputs of whatever forecasting model the implementation runs — they depend on assumed rainfall, grazing and intervention scenarios — and should be labelled as model projections with their assumptions, not presented as expected outcomes.The companion simulator makes the first two points concrete. Its risk calculator (tab “2. Algorithms”) uses the same four weights but its own normalisation: the vegetation score is (1 − NDVI) × 100, soil moisture is scored against 30 %, annual rainfall against 600 mm, and land-use intensity is mapped to fixed values. With its default inputs (NDVI 0.35, soil moisture 12.5 %, rainfall 325 mm, moderate land use) the factor scores are 65.0, 58.3, 45.8 and 30, and the weighted sum is about 51.5, which the simulator labels MEDIUM using its own bands at 30 and 60. For the same NDVI of 0.35 the specification's example assigns a vegetation score of 35 and the simulator 65. Neither is wrong as a teaching device; together they show that two implementations with identical weights can disagree by a wide margin, and that a score is meaningless without its normalisation rules and band table.
The following function shows the part that the standard does fix, with the parts it does not fix made explicit as inputs. The threshold values in the example configuration are placeholders chosen for the illustration; WIA-ENE-057 does not define them.
// risk-score.ts — weighted desertification risk score
type Factor = "vegetation" | "soil" | "climate" | "humanActivity";
export const WEIGHTS: Record<Factor, number> = {
vegetation: 0.30, soil: 0.25, climate: 0.25, humanActivity: 0.20,
};
// Implementation-defined. EXAMPLE VALUES ONLY — not part of WIA-ENE-057.
export interface Bands { version: string; bands: { min: number; label: string }[] }
export const EXAMPLE_BANDS: Bands = {
version: "example-bands-0.1",
bands: [ // checked from the highest min downwards
{ min: 80, label: "critical" },
{ min: 60, label: "high-risk" },
{ min: 40, label: "medium-risk" },
{ min: 0, label: "low-risk" },
],
};
export function riskScore(scores: Record<Factor, number>,
weights: Record<Factor, number> = WEIGHTS): number {
const sumW = Object.values(weights).reduce((a, b) => a + b, 0);
if (Math.abs(sumW - 1) > 1e-9) throw new Error(`weights sum to ${sumW}, not 1`);
let r = 0;
for (const f of Object.keys(weights) as Factor[]) {
const s = scores[f];
if (!(s >= 0 && s <= 100)) throw new Error(`${f} score out of range: ${s}`);
r += weights[f] * s;
}
return Math.round(r * 100) / 100; // two decimals, declared in the docs
}
export function categorise(r: number, cfg: Bands = EXAMPLE_BANDS) {
const band = cfg.bands.find(b => r >= b.min)!;
return { category: band.label, bandsVersion: cfg.version };
}
// Spec example factors → 67.75 (the spec prints 68.5; see text)
const r = riskScore({ vegetation: 35, soil: 72, climate: 85, humanActivity: 90 });
console.log(r, categorise(r));
POST /assessment/desertification/batch takes a list of locationIds and options (includeProjections, projectionsYears such as 1, 5 and 10) and returns an assessmentId, a per-location result list and a summary. In the example the three illustrative scores are 68.5, 52.3 and 82.1; their mean is 202.9 ÷ 3 ≈ 67.63, which matches the reported averageRisk 67.6. The summary also reports highRiskCount 2 and criticalCount 1, while only one location is labelled high-risk and one critical. The count of two only makes sense if “high risk” means “high or above”. That is a reasonable convention, but it must be written down, or two dashboards reading the same summary will draw different bar charts. For long location lists, an implementation should either cap the list size and say so, or return 202 Accepted and signal completion with the assessment.completed event described in Chapter 5.
Restoration is modelled as a plan attached to a location, plus a series of progress reports against it. POST /restoration/plan takes the location, a project name, the organisation, the area, objectives such as soil-stabilization and vegetation-recovery, constraints (budget, timeline, water availability) and preferences (native species, community involvement, carbon sequestration). The response returns a planId and a proposed strategy in phases — soil preparation and water conservation with half-moon micro-catchments, stone bunds and rainwater harvesting; native planting with named species such as Acacia senegal and Balanites aegyptiaca; then monitoring and maintenance — along with expected outcomes, risk mitigations and a monitoring plan.
The example's phase costs (100,000 + 250,000 + 150,000) add up to the stated budget of 500,000, which is the kind of internal check a server should perform before it returns a plan. The expectedOutcomes block is another matter. Its return on investment, payback period, jobs created and carbon figures are illustrative; nothing in the standard defines how they would be computed, and a platform that returns such figures should either show its method or leave them out. This is especially true of carbon. Chapter 6 explains why a projected tonnage is not the same thing as issuable credits.
The plan resource and the PHASE-1 restorationProject record describe overlapping things with different identifiers: the API example assigns REST-PLAN-20251225-001, while the PHASE-1 record and the location's activeProjects list use REST-2025-SAHEL-001. A clean implementation treats the plan as the planning artefact and the project record as the operational record, and links them explicitly — for instance by storing the planId in the project record and returning the project identifier in the plan's links. The field mapping is straightforward: plan phases map to the project's timeline and milestones, planned species to interventions.reforestation.species[], soil and water activities to soilConservation.measures[] and waterManagement.techniques[], and the budget to the project's funding block, whose sources must add up to the total budget.
PUT /restoration/plan/{planId}/progress takes a date, the current phase, a completion percentage, per-activity counts (for example trees planted with a survival rate, or half-moons created against a target), outcome indicators and challenges. The server answers with overall progress, an onTrack flag and the next milestone.
HTTP defines PUT as idempotent: sending the same request twice must leave the server in the same state as sending it once. That property is valuable here, because progress reports often come from field offices with unreliable connections, and a client that did not see the response will retry. But it only holds if the server treats each report as a replacement of a well-defined piece of state, not as an increment. Two rules make that true:
(planId, phase, date); a second PUT with the same key replaces the first. Corrections are then made by re-sending the report for that date.For concurrent editors — a field officer and a head-office manager correcting the same report — use HTTP conditional requests. The server returns an ETag with each stored report, and the client sends it back in If-Match; if someone else changed the report in the meantime, the server answers 412 Precondition Failed instead of silently overwriting. For the POST endpoints, which are not idempotent (creating a plan twice creates two plans), accept a client-generated idempotency key header and return the original result for a repeated key. The IETF has an Internet-Draft for an Idempotency-Key header field; whichever header name you use, document it.
POST /alerts/subscribe registers interest in one or more locations. The subscriber chooses among four alert types — rapid-vegetation-decline, critical-soil-moisture, drought-alert and erosion-risk — filters by severity (for example only high and critical) and chooses a delivery method: a webhook URL, an e-mail address, or both. The response returns a subscriptionId such as SUB-20251225-001 and echoes how many locations and alert types are being watched.
The webhook payload is the most self-consistent example in the PHASE-2 document, and it is a good model of what an alert should contain:
{
"alertId": "ALERT-20251225-001",
"subscriptionId": "SUB-20251225-001",
"timestamp": "2025-12-25T10:30:00Z",
"severity": "high",
"type": "rapid-vegetation-decline",
"location": { "locationId": "SAHEL-REGION-001", "name": "Sahel Test Region",
"coordinates": { "latitude": 14.5, "longitude": -4.0 } },
"details": {
"metric": "ndvi",
"currentValue": 0.35,
"previousValue": 0.45,
"change": -0.10,
"changePeriod": "90 days",
"threshold": -0.05
},
"message": "NDVI decreased by 22% over 90 days, exceeding alert threshold"
}
Every number in details can be checked: 0.35 − 0.45 = −0.10; −0.10 ÷ 0.45 ≈ −22 %; and an absolute change of −0.10 is beyond the −0.05 threshold. This is what makes an alert actionable. The recipient can see the metric, the two values compared, the window and the rule that fired, and can decide whether the decline is a real signal or, for example, the normal end of a rainy season. An alert that only says “vegetation declining” forces the recipient back to the API to reconstruct what happened. When you design further alert types, keep the same shape: metric, current and reference values, change, period and threshold, with the threshold's units stated.
The message field is prose for humans; clients should act on the structured fields. Alerts also need the delivery guarantees that any webhook does — a stable alertId for de-duplication, retries with back-off when the receiver is down, and a signature so the receiver can verify the sender. Those belong to the protocol layer: Chapter 5 shows the X-WIA-Signature header and how to verify it with a timing-safe comparison over the raw request body. On the subscription side, a sensible server verifies that it can reach the webhook URL (for example with a test delivery) before it activates a subscription, and offers a way to list and delete subscriptions, which the specification does not yet show.
POST /reports/unccd-ldn takes a set of locations, a reporting period, a report type (for example annual), an output format and whether to include projections. The response contains, per location, a baseline and a current block, each with the three LDN sub-indicators — landCover, productivity and carbonStocks — plus a downloadUrl and an expiresAt timestamp for the generated package.
The three sub-indicators are the right ones: land cover, land productivity and soil organic carbon stocks are the components of SDG indicator 15.3.1 that Chapter 1 introduced. The response's ldnStatus block, however, reports achieved, progress and neutralityGap values that are not UNCCD concepts. Under the UNCCD good-practice guidance, an area is judged degraded if any one of the three sub-indicators shows significant negative change against the baseline — the “one out, all out” principle — and neutrality is assessed by comparing degraded and improved area, not by a single progress percentage. A platform can of course compute internal progress measures for its own dashboards, but the report package sent onward should present the three sub-indicators and the one-out-all-out result, and label anything else as the platform's own metric.
Treat the downloadUrl as a capability: anyone who has the link can fetch the package until expiresAt. Generate it as a short-lived signed URL, bind it to the requesting organisation where possible, and log each download. Large reports are also good candidates for the same asynchronous 202 Accepted pattern.
Every PHASE-2 error uses the same envelope: an error object with a machine-readable code, a human-readable message, a free-form details object, a timestamp and a requestId.
HTTP/1.1 404 Not Found
Content-Type: application/json
{
"error": {
"code": "INVALID_LOCATION",
"message": "Location ID 'INVALID-001' not found",
"details": {
"locationId": "INVALID-001",
"suggestion": "Register location first using POST /api/v1/locations"
},
"timestamp": "2025-12-25T10:00:00Z",
"requestId": "req-abc123"
}
}
The code is the contract; clients branch on it. The message may change between versions and may be translated, so no client should parse it. Annex E of the specification treats renaming or removing fields, “even in error payloads”, as a change that requires a version bump, which means the error codes an implementation returns are part of its interface and belong in its OpenAPI document.
The API-INTERFACE document and the README both cite IETF RFC 9457, Problem Details for HTTP APIs, which defines a standard error format with the media type application/problem+json and the members type, title, status, detail and instance, plus any extension members. The two formats can coexist. A practical approach is to serve the WIA envelope by default for compatibility, and to return Problem Details when the client's Accept header asks for application/problem+json, carrying the WIA fields as extension members:
HTTP/1.1 404 Not Found
Content-Type: application/problem+json
{
"type": "https://platform.example.org/problems/invalid-location",
"title": "Location not registered",
"status": 404,
"detail": "Location ID 'INVALID-001' not found",
"instance": "/api/v1/monitoring/vegetation",
"code": "INVALID_LOCATION",
"requestId": "req-abc123",
"suggestion": "Register location first using POST /api/v1/locations"
}
The specification lists eight status codes. The table below adds, in the third column, codes that the workflows in this chapter need but the specification does not list; they are recommendations, marked as such.
| Status | Listed in PHASE-2 | Typical use in this API |
|---|---|---|
| 200 OK | Yes | Reads, assessments, progress updates, synchronous reports |
| 201 Created | Yes | New records, locations, plans, subscriptions |
| 202 Accepted | No (recommended) | Long batch assessments and national-scale reports processed asynchronously |
| 400 Bad Request | Yes | Validation failures, e.g. NDVI outside −1…1 |
| 401 Unauthorized | Yes | Missing, invalid or expired token |
| 403 Forbidden | Yes | Valid token without the required scope or tenant |
| 404 Not Found | Yes | Unknown record, plan or location (INVALID_LOCATION) |
| 409 Conflict | No (recommended) | Duplicate locationId on registration |
| 412 Precondition Failed | No (recommended) | If-Match mismatch on a progress update |
| 429 Too Many Requests | Yes | Rate limit exceeded; send Retry-After |
| 500 Internal Server Error | Yes | Unexpected failure; always include requestId |
PHASE-2 describes three tiers — standard at 1,000 requests per hour, professional at 10,000 and enterprise unlimited — and three response headers: X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset. These tiers are design targets for a hosted service, not measured capacity, and a self-hosted deployment will set its own. Two refinements help clients. First, the example gives X-RateLimit-Reset as an ISO 8601 timestamp, while many APIs use seconds until reset; state which one you use. Second, on a 429 response also send the standard Retry-After header defined in HTTP semantics (RFC 9110), which generic HTTP clients and proxies already understand. The IETF is also working on standard RateLimit header fields in an Internet-Draft; implementations may adopt them alongside the X- headers once they stabilise.
List endpoints take page and limit and return a pagination block (page, limit, totalPages, totalRecords, hasNext, hasPrevious) plus links for first, previous, next and last. The example is consistent: 487 records at 50 per page is 10 pages. Clients should follow links.next until it is absent rather than computing page numbers themselves. Because monitoring data is appended continually, page numbers can shift while a client walks them; for large exports, add an opaque cursor tied to a stable sort (timestamp plus record ID).
The resources above say what an implementation can do. The API-INTERFACE document adds the machinery by which it states, verifiably, what it does support. This is what separates a WIA-conformant deployment from an API that merely resembles the examples.
API-INTERFACE §4 places a discovery document at a well-known URI (RFC 8615): /.well-known/wia/desertification-prevention. It declares the supported operation groups, the URL of the OpenAPI document and the key used to sign the manifest, and it is itself signed with the same Sigstore key. A client or auditor that knows only a deployment's origin can fetch this one document and learn everything else. The specification fixes the location and the required contents but not the exact field names; a minimal document might look like this (field names illustrative):
GET /.well-known/wia/desertification-prevention
{
"standard": "WIA-ENE-057",
"standardSlug": "desertification-prevention",
"apiBase": "https://platform.example.org/api/v1",
"operationGroups": ["monitoring", "assessment", "restoration",
"locations", "alerts", "reports"],
"openapi": "https://platform.example.org/openapi.json",
"manifest": "https://platform.example.org/wia/manifest.json",
"signingKey": { "type": "sigstore", "identity": "release@platform.example.org" }
}
RFC 8615 expects new well-known URI suffixes to be registered with IANA. Check the registration status of the wia suffix before building clients that depend on it outside your own ecosystem.
API-INTERFACE §2 requires a signed manifest with a fixed set of contents: standardSlug with the constant value "desertification-prevention"; the implemented version in Semantic Versioning 2.0.0; the implementation's name, build digest and SBOM URL; the profile name and version; the support status of each requirement; and a Sigstore DSSE signature, anchored in the Rekor transparency log at a cadence the deployment declares. Annex I adds that each requirement is marked supported, partial (with a reason) or excluded (with a justification). An illustrative manifest, before signing:
{
"standardSlug": "desertification-prevention",
"version": "1.0.0",
"implementation": {
"name": "example-land-monitor",
"build": "sha256:<image digest>",
"sbom": "https://platform.example.org/wia/sbom.cdx.json"
},
"profile": { "named": "WIA-P2-API-Edge-only", "version": 2 },
"requirements": [
{ "id": "P2-LOC-REGISTER", "status": "supported" },
{ "id": "P2-ASSESS-BATCH", "status": "partial",
"reason": "max 50 locations per call" },
{ "id": "P2-REPORT-UNCCD", "status": "excluded",
"justification": "reporting done by partner system" }
]
}
The requirement identifiers in this example are invented for the illustration: the specification does not yet publish a numbered requirement list, so an implementation must, for now, derive its own from the normative statements and state how it did so. That is also why “partial” with a reason matters. A deployment that caps batch size, or that does not generate LDN reports, is still useful; what harms users is a deployment that implies full support it does not have.
API-INTERFACE §3 defines three tiers — Surface (data formats accepted, self-attested), Verified (annual third-party audit) and Anchored (a continuous evidence package) — and asks implementations to declare their tier in the OpenAPI document through an x-wia-conformance-tier extension. Chapter 8 covers what each tier requires in evidence; here the point is simply where the declaration lives.
The repository's api/openapi.yaml is not a usable starting point. It is a seven-line placeholder declaring OpenAPI 3.1.0 with a single generic /resource path, and it does not describe any of the endpoints in this chapter. Several annexes also refer to an “Annex A” for tiers and to test vectors under tests/phase-vectors/; neither exists in the repository. An implementer therefore has to author a real OpenAPI 3.1 document. A skeleton that carries the tier declaration and one fully described operation looks like this:
openapi: 3.1.0
info:
title: Example Land Monitor — WIA-ENE-057 PHASE 2
version: 1.0.0
x-wia-conformance-tier: Surface
servers:
- url: https://platform.example.org/api/v1
components:
securitySchemes:
oauth:
type: oauth2
flows:
clientCredentials:
tokenUrl: https://platform.example.org/oauth/token
scopes:
monitoring:read: Read monitoring records and trends
restoration:write: Create plans and report progress
paths:
/assessment/desertification/{locationId}:
get:
operationId: getRiskAssessment
security: [{ oauth: [monitoring:read] }]
parameters:
- { name: locationId, in: path, required: true,
schema: { type: string, pattern: "^[A-Z0-9-]+$" } }
responses:
"200": { description: Risk assessment with factor breakdown }
"404": { description: INVALID_LOCATION }
The specification does not say whether the extension belongs under info or at the document root; OpenAPI allows x- extensions in both places. Choose one, and have your discovery document point to the OpenAPI URL so that the declaration can be found mechanically.
Two further API-INTERFACE rules shape how this API evolves and how it is protected. On versioning, the documents disagree: §6 and Annex F require at least a 90-day overlap when a major version changes, while Annex H describes a 180-day parallel window. Until this is reconciled, plan for the stricter figure — keep /api/v1 running for at least 180 days after /api/v2 appears — and announce deprecations with the standard Deprecation and Sunset response headers covered in Chapter 5. On security, §7 requires TLS 1.3 in transit and AES-256-GCM or stronger at rest, role-based access control and a tamper-evident audit log. PHASE-3 elsewhere permits TLS 1.2; treat TLS 1.3 as the requirement for PHASE-2 endpoints and note the discrepancy in your deployment policy, which §7 also requires to name the privacy regime that applies to any personal data — relevant here because restoration records can contain names of community members and landholders.
locationId: register first, then submit, then follow the returned links.code, offer RFC 9457, and publish a discovery document, an honest signed manifest and a real OpenAPI 3.1 document — the repository's stub is not one.Chapter 5 moves below the resource layer to the protocols that carry these requests and events: WebSocket streaming, MQTT sensor topics, satellite and bulk ingestion, OGC services, webhook signatures and the transport security baseline.