# Living Signals REST API v1

Use the read-only Living Signals REST API to explore news-derived Claims,
corroborated Signals, and related Topics. All endpoints use the `/api/v1`
base path.

Start with [`GET /api/v1/countries`](/api/v1/countries) to discover your
access. Read [`GET /api/v1/rubric`](/api/v1/rubric) before you build filters.
You can also read this reference on the [documentation page](/docs), as
[Markdown](/api/v1/docs), download the
[OpenAPI 3.1 document](/api/v1/openapi.json), or open the
[browsable reference](/reference).

> **Changed from the previous contract:** `/clusters`, the `level`, `cluster`,
> and `offset` parameters, the `stable_id`, `rubric_tags`, and
> `topic_stable_id` fields, and the `countries` and `languages` aliases are not
> part of v1. There is no compatibility layer.

## The model

Living Signals uses four core concepts:

1. A **Claim** is an immutable assertion extracted from one source. It includes
   article and Source provenance.
2. A **Signal** combines corroborating Claims into a discrete, time-bounded
   occurrence. Signals are the primary exploration resource. Signal detail
   includes every retrievable Claim in the Signal.
3. A **Topic** groups related Signals for browsing.
4. The **rubric** describes each Signal with binary attributes. A category
   groups related attributes and matches when any member attribute is true.
   Both attributes and categories support multiple values on one Signal.

**Annotation layers** independently classify Signals through attributed
standards or operational vocabularies. Access to each layer is granted
separately.

The rubric and annotation layers describe Signals after they form. They do not
determine how Claims form Signals or how Signals form Topics.

## Getting started

Send the API key on authenticated requests:

```http
Authorization: Bearer ls_live_<token>
```

Then follow this path:

1. Discover your countries, coverage, and annotation layers:

   ```http
   GET /api/v1/countries
   ```

2. Learn the category and attribute vocabulary:

   ```http
   GET /api/v1/rubric
   ```

3. Explore environmental Signals in Cameroon:

   ```http
   GET /api/v1/signals?country=CM&category=environmental
   ```

4. Retrieve one Signal with every supporting Claim:

   ```http
   GET /api/v1/signals/28eeab9d-47f6-4a2e-bd28-cafe6ad6ca19
   ```

5. Poll newly extracted Claims by ingestion time:

   ```http
   GET /api/v1/claims?since_extracted=24h
   ```

For paginated requests, follow every non-null `nextPage` unchanged before you
advance a polling watermark.

## Authentication and entitlements

Send an API key in either header:

```http
Authorization: Bearer ls_live_<token>
```

```http
X-API-Key: ls_live_<token>
```

Create and manage keys in the Living Signals app at `/access#keys`. Keys
begin `ls_live_`. Keys issued before 2026-07-21 may begin `ls_cust_`; they
behave identically.

| Route | Authentication |
| --- | --- |
| `GET /api/v1` | Public |
| `GET /api/v1/rubric` | Public |
| `GET /api/v1/docs` | Public |
| `GET /api/v1/openapi.json` | Public |
| Every other route in this reference | API key required |

Customer keys can access only their active country grants. Omit `country` to
use every country available to your key. Requesting any unentitled country
rejects the entire request with `403 country_not_entitled`. The API never
returns a partial country subset. Team keys can access every country.

Annotation layer grants are independent of country grants. Customer Signal
responses include only granted layers with stored annotations. Filtering by an
ungranted layer rejects the entire request with
`403 annotation_layer_not_entitled`. Team keys can filter by every registered
layer. Set `include_annotations=true` to include stored annotations in a team
response.

Use `GET /api/v1/countries` to discover annotation layer ids. Use
`GET /api/v1/annotation-layers` to retrieve their code vocabularies.

Enrichment grants are separate from annotation layer and country grants.
Customer Signal detail includes satellite imagery only when the organization
has the `geospatial_v1` enrichment enabled. Team keys can access every
enrichment.

Authentication and entitlement failures use distinct codes:

| HTTP | Code | Meaning |
| --- | --- | --- |
| 401 | `missing_credentials` | Neither a Bearer token nor `X-API-Key` was supplied. |
| 401 | `invalid_credentials` | The supplied key is not recognized. |
| 403 | `key_revoked` | The key has been revoked. |
| 403 | `key_expired` | The key has expired. |
| 403 | `customer_inactive` | The customer account is not active. |
| 403 | `country_not_entitled` | At least one explicitly requested country is outside the key's grants. |
| 403 | `annotation_layer_not_entitled` | An annotation-layer filter names a layer not granted to the customer key. |

## Conventions

### Dates and timestamps

Timestamps use RFC 3339 UTC, such as `2026-07-20T21:31:21.628Z`. Timestamp
fields include `extracted_at`, `article.published_at`, `first_claim_at`,
`last_claim_at`, and `formed_at`.

`event_date`, `occurred_on`, `signal_start`, and `signal_end` use
`YYYY-MM-DD`. These fields can be `null`. `occurred_on` is the canonical
occurrence date for a Claim: `event_date` when the source states one, else the
article's publish date. A `null` `occurred_on` means the occurrence time is
unknown; such Claims match only `since=all`. Because `event_date` comes from
source content, it can be imprecise or unreliable. Poll Claims with
`since_extracted` instead.

Every `since` parameter is an exclusive bound. It accepts:

- One or more digits followed by `h`, `d`, or `w`. Units are case-insensitive.
- An ISO date or timestamp beginning with `YYYY-MM-DD`. Use an RFC 3339
  timestamp when you need a time of day.
- `all`, which removes the bound.

Valid examples include `24h`, `7d`, `2w`, `all`, `2026-07-20`, and
`2026-07-20T21:30:00Z`. Invalid suffixes and date strings return
`400 invalid_parameter`.

### The default window

`/signals`, `/topics`, and `/claims` return the last 90 days when you omit
`since`. Set `since` to choose a different window, or `since=all` to search the
whole corpus.

The window measures when something happened, not when it was ingested. A story
published today about a flood two years ago produces a Claim dated two years
ago, and the default window excludes it. Claims filter on `occurred_on`;
Signals filter on `signal_end`, the latest `occurred_on` across their Claims.
Records whose occurrence time is unknown appear only under `since=all`.

Passing `since_extracted` turns the default window off, so a poll returns
everything ingested since your last one whatever its `occurred_on`.

### Strict validation

Query parameters follow these validation rules:

| Input | Behavior |
| --- | --- |
| A supported parameter with a malformed value | `400 invalid_parameter`. |
| A parameter recognized elsewhere in v1 but unsupported by this endpoint | `400 invalid_parameter` with an endpoint hint. |
| A retired parameter listed below | `400 invalid_parameter` with its replacement. |
| A truly unknown parameter name | Ignored, consistent with ordinary HTTP query handling. |

`/docs` and `/openapi.json` serve fixed documents and ignore all query
parameters.

Retired parameters are handled explicitly:

| Retired parameter | Replacement |
| --- | --- |
| `countries` | Use repeatable or comma-separated `country`. |
| `languages` | Use repeatable or comma-separated `language`. |
| `offset` | Pagination is keyset-only; follow `nextPage`. |
| `level` | Use the separate `/signals` and `/topics` resources. |
| `cluster` | Use `signal` on `/claims`, or use `/signals` and `/topics` directly. |

The API applies only safe normalization. It trims country codes and converts
them to uppercase. It trims language codes and converts them to lowercase. It
removes duplicate repeated or comma-separated values. A positive integer
`limit` above 500 is clamped to 500. Check `pagination.limit` for the effective
page size.

Every known path answers `OPTIONS` with `204` and `Allow: GET, OPTIONS`,
with or without an API key.
Unsupported methods return `405 method_not_allowed` with the same `Allow`
header.

Unknown paths return a JSON `404 not_found` response. Authentication runs
before routing under `/api/v1/*`. An unknown protected path without a valid key
therefore returns `401`. Public paths are `/api/v1`, `/rubric`, `/docs`, and
`/openapi.json`.

### Error envelope and codes

Most errors use this envelope:

```json
{
  "error": {
    "code": "invalid_parameter",
    "message": "Unknown rubric category \"climate\". See GET /api/v1/rubric for valid category ids."
  }
}
```

`410 signal_retired` is the Signal-detail exception. It uses the structured
retirement body documented under `GET /api/v1/signals/{id}` so integrations can
distinguish a durable reference with no successor from an id that never
existed.

Error codes:

| HTTP | Code | Meaning |
| --- | --- | --- |
| 400 | `invalid_parameter` | A supported value is malformed, a vocabulary value is unknown, or a recognized parameter is unsupported here. |
| 400 | `invalid_cursor` | A cursor is empty, malformed, or has the wrong sort-key shape. |
| 401 | `missing_credentials` | No API key was supplied. |
| 401 | `invalid_credentials` | The supplied key is not recognized. |
| 403 | `key_revoked` | The key has been revoked. |
| 403 | `key_expired` | The key has expired. |
| 403 | `customer_inactive` | The customer account is not active. |
| 403 | `country_not_entitled` | An explicitly requested country is not entitled. |
| 403 | `annotation_layer_not_entitled` | An annotation filter uses an ungranted layer. |
| 404 | `not_found` | The path or requested resource does not exist. |
| 405 | `method_not_allowed` | The path exists but does not support the method; check `Allow`. |
| 410 | `signal_retired` | The Signal id existed but has no active successor. |
| 429 | `rate_limit_exceeded` | The customer-key limit was exceeded; check `Retry-After`. |
| 500 | `internal_error` | An unexpected server error occurred. |
| 503 | `auth_unavailable` | Authentication could not reach its backing service. |
| 503 | `release_in_progress` | A coordinated rubric update is in progress. Retry after the `Retry-After` interval. |

### Rate limits

Customer keys use a sliding-window limit of 240 requests per minute per key by
default. Your deployment may use a different limit. Check these headers on
authenticated customer responses:

| Header | Meaning |
| --- | --- |
| `X-RateLimit-Limit` | Requests allowed in the current window. |
| `X-RateLimit-Remaining` | Requests remaining in the current window. |
| `X-RateLimit-Reset` | Seconds until the current window resets; this is a relative duration, not a Unix timestamp. |

A `429 rate_limit_exceeded` response also includes `Retry-After` in seconds.
Authentication runs before rate limiting, so `401` responses do not include
rate limit headers. Public responses and team-key responses do not include
them either. Team keys are not rate limited.

### Pagination

`/signals`, `/topics`, `/claims`, and `/sources` use keyset pagination:

```json
{
  "nextPage": "https://api.livingsignals.example/api/v1/signals?country=CM&limit=50&cursor=MjEw",
  "pagination": {
    "limit": 50,
    "count": 50
  }
}
```

`nextPage` is an absolute URL on a first-party Living Signals host. Follow it
unchanged until it is `null`. Its query string preserves your parameters as
you sent them (for example, a lowercase `country=cm` stays lowercase even
though filtering normalizes it) — treat the URL as opaque rather than parsing
it. `count` reports rows on the current page, not total results.

Cursors are base64url-encoded sort keys. Treat them as opaque. Omit `cursor` to
start a page walk. An empty, malformed, or incompatible cursor returns
`400 invalid_cursor`.

Cursors are not bound to a filter set. Some endpoints share a cursor shape, so
reusing a cursor may not fail even when it points to an undefined page
position. Never create, edit, or reuse a cursor. Follow the `nextPage` issued
for the exact query you are paging through.

## Endpoint reference

### `GET /api/v1`

Begin here to find core concepts, authentication rules, pagination guidance,
supported parameters, and a recommended integration path.

| Parameter | Meaning |
| --- | --- |
| None | |

Example:

```json
{
  "name": "Living Signals REST API",
  "version": "v1",
  "model": {
    "data_primitives": "Claims are immutable assertions extracted from one source. Signals combine corroborating Claims into time-bounded occurrences. Explore Signals as the primary resource.",
    "enrichment": "Topics group related Signals for browsing.",
    "filtering_and_organization": "The versioned rubric organizes Signals with categories and attributes. Annotation layers add independent classification vocabularies available to your key."
  },
  "getting_started": [
    {
      "step": 1,
      "request": "GET /api/v1/countries",
      "purpose": "Discover your country access, data coverage, and annotation layers."
    },
    {
      "step": 2,
      "request": "GET /api/v1/rubric",
      "purpose": "Learn the rubric vocabulary before you build filters."
    },
    {
      "step": 3,
      "request": "GET /api/v1/signals?country=CM&category=environmental",
      "purpose": "Explore corroborated occurrences in an available country."
    },
    {
      "step": 4,
      "request": "GET /api/v1/signals/{id}",
      "purpose": "Retrieve a Signal with its complete Claim provenance."
    },
    {
      "step": 5,
      "request": "GET /api/v1/claims?since_extracted=24h",
      "purpose": "Poll /claims with since_extracted. Follow nextPage and deduplicate by Claim id."
    }
  ],
  "documentation": "/api/v1/docs",
  "documentation_note": "Read the public Markdown reference at /api/v1/docs. Download OpenAPI at /api/v1/openapi.json. Browse the rendered reference at /reference. Manage keys at /access#keys.",
  "openapi": "/api/v1/openapi.json",
  "reference": "/reference",
  "auth": {
    "scheme": "Bearer or X-API-Key header",
    "customer_example": "Authorization: Bearer ls_live_<token>",
    "team_example": "Authorization: Bearer ls_live_<token>",
    "key_management": "/access#keys",
    "entitlements": "Customer keys can access granted countries, annotation layers, and enrichments. Requesting an unentitled country or annotation layer returns 403; unentitled imagery is absent from Signal detail and its PNG route returns 404. Internal keys can access every country, layer, and enrichment."
  },
  "rate_limit": {
    "applies_to": "Customer keys.",
    "default": "240 requests per minute for each API key.",
    "response": "HTTP 429 rate_limit_exceeded with Retry-After in seconds.",
    "headers": "Metered responses include X-RateLimit-Limit, X-RateLimit-Remaining, and X-RateLimit-Reset."
  },
  "validation": {
    "parameters": "On data endpoints, malformed values and parameters supported by a different endpoint return HTTP 400 invalid_parameter. Unknown parameter names are ignored. The static /docs and /openapi.json endpoints ignore all query parameters.",
    "vocabulary": "Unknown rubric ids and annotation codes return HTTP 400 invalid_parameter. The error points to /rubric or /annotation-layers.",
    "paths": "Unknown paths return a JSON HTTP 404 not_found envelope.",
    "methods": "OPTIONS returns HTTP 204. Other non-GET methods return HTTP 405 method_not_allowed with Allow: GET, OPTIONS."
  },
  "pagination": {
    "next_page": "Follow the absolute nextPage URL unchanged until it is null.",
    "cursor": "Treat cursors as opaque and endpoint-specific. Omit the cursor to start. Empty or incompatible cursors return HTTP 400 invalid_cursor.",
    "response": "pagination contains the effective limit and the number of rows on the current page."
  },
  "endpoints": {
    "GET /api/v1": {
      "description": "Public integration guide and endpoint index.",
      "auth": "public",
      "params": {}
    },
    "GET /api/v1/countries": {
      "description": "Your available countries, Claim coverage, and annotation layer ids.",
      "auth": "API key",
      "params": {}
    },
    "GET /api/v1/rubric": {
      "description": "Rubric categories and attributes for filtering Signals and Topics.",
      "auth": "public",
      "params": {}
    },
    "GET /api/v1/annotation-layers": {
      "description": "Annotation vocabularies available to your key.",
      "auth": "API key",
      "params": {}
    },
    "GET /api/v1/signals": {
      "description": "Corroborated Signals with rubric filters and available annotations.",
      "auth": "API key",
      "params": {
        "country": "Repeat or comma-separate ISO-2 codes to use OR. Omission uses your full country access.",
        "category": "Repeat or comma-separate category ids to match any Signal with a member attribute from any listed category.",
        "attribute": "Repeat or comma-separate attribute ids. Every listed attribute must be true.",
        "attribute_any": "Repeat or comma-separate attribute ids. At least one listed attribute must be true.",
        "glide_v1": "Repeat or comma-separate GLIDE codes to use OR within the layer.",
        "acled_v1": "Repeat or comma-separate ACLED codes to use OR within the layer.",
        "iasc_v1": "Repeat or comma-separate IASC codes to use OR within the layer.",
        "iep_positive_peace_v1": "Repeat or comma-separate IEP Positive Peace pillars to use OR within the layer.",
        "geospatial_v1": "Repeat or comma-separate geospatial imagery-suitability codes to use OR within the layer.",
        "topic": "Parent Topic UUID.",
        "since": "Exclusive bound on when the occurrences happened, using signal_end (latest member occurrence date; Signals with no datable occurrence match only all). Defaults to 90d; pass all for the full corpus. Accepts <n>h, <n>d, <n>w, or an ISO date or timestamp.",
        "limit": "Page size. Defaults to 50 and clamps at 500.",
        "cursor": "Opaque cursor from nextPage.",
        "include_annotations": "For team keys, true includes all stored layers. Customer responses always follow grants."
      }
    },
    "GET /api/v1/signals/{id}": {
      "description": "The canonical active Signal with all of its Claims and entitled attached imagery. Retired merge and split ids resolve to their primary successor.",
      "auth": "API key",
      "params": {
        "id": "Signal UUID.",
        "include_annotations": "For team keys, true includes all stored layers. Customer responses always follow grants."
      }
    },
    "GET /api/v1/signals/{id}/imagery/{kind}.png": {
      "description": "An attached satellite imagery chip for the canonical active Signal when geospatial_v1 enrichment access is enabled.",
      "auth": "API key",
      "params": {
        "id": "Signal UUID.",
        "kind": "Imagery kind, currently post_event."
      }
    },
    "GET /api/v1/topics": {
      "description": "Browse groups of related Signals. Topic ids can change between processing runs, so store Signal or Claim ids instead.",
      "auth": "API key",
      "params": {
        "country": "Repeat or comma-separate ISO-2 codes to use OR.",
        "category": "Match any listed category on one grouped Signal.",
        "attribute": "Require every listed attribute on the same grouped Signal.",
        "attribute_any": "Require any listed attribute on the same grouped Signal.",
        "glide_v1": "Match any listed GLIDE code on the same grouped Signal.",
        "acled_v1": "Match any listed ACLED code on the same grouped Signal.",
        "iasc_v1": "Match any listed IASC code on the same grouped Signal.",
        "iep_positive_peace_v1": "Match any listed IEP Positive Peace pillar on the same grouped Signal.",
        "geospatial_v1": "Match any listed geospatial imagery-suitability code on the same grouped Signal.",
        "since": "Exclusive occurrence-time bound met by one grouped Signal. Defaults to 90d; pass all for the full corpus.",
        "limit": "Page size. Defaults to 50 and clamps at 500.",
        "cursor": "Opaque cursor from nextPage."
      }
    },
    "GET /api/v1/claims": {
      "description": "Extracted Claims with article and Source provenance plus current Signal assignment.",
      "auth": "API key",
      "params": {
        "country": "Repeat or comma-separate ISO-2 codes to use OR.",
        "signal": "Current Signal UUID.",
        "since": "Exclusive occurrence-time bound on occurred_on (event date, else article publish date; unknown-date Claims match only all). Defaults to 90d unless since_extracted is set; pass all for the full corpus.",
        "since_extracted": "Exclusive extracted_at bound for incremental polling. Turns off the default since window so polling stays lossless.",
        "limit": "Page size. Defaults to 50 and clamps at 500.",
        "cursor": "Opaque cursor from nextPage."
      }
    },
    "GET /api/v1/claims/{id}": {
      "description": "One extracted Claim.",
      "auth": "API key",
      "params": {
        "id": "Numeric Claim id."
      }
    },
    "GET /api/v1/sources": {
      "description": "Active Sources by country, with recent Claim coverage.",
      "auth": "API key",
      "params": {
        "country": "Repeat or comma-separate ISO-2 codes to use OR.",
        "language": "Repeat or comma-separate two- or three-letter language codes to use OR.",
        "limit": "Page size. Defaults to 100 and clamps at 500.",
        "cursor": "Opaque cursor from nextPage."
      }
    },
    "GET /api/v1/docs": {
      "description": "Public Markdown API reference.",
      "auth": "public",
      "params": {}
    },
    "GET /api/v1/openapi.json": {
      "description": "Public OpenAPI 3.1 document.",
      "auth": "public",
      "params": {}
    }
  }
}
```

Responses use `Cache-Control: public, max-age=3600`.

### `GET /api/v1/countries`

Call this endpoint first to discover your country access, data coverage, and
annotation layers. Countries are ordered by `code`.

| Parameter | Meaning |
| --- | --- |
| None | |

| Field | Meaning |
| --- | --- |
| `countries` | Countries available to your key. Customer grants remain visible when country metadata is unavailable. |
| `annotation_layers` | Layer ids available to your key. Team keys receive layer ids present in stored Signal annotations. |
| `countries[].code` | ISO 3166-1 alpha-2 country code. |
| `countries[].name` | Country name, or `null` when metadata is unavailable. |
| `countries[].status` | `live`, `in_curation`, `pending`, or `archived`; `null` when metadata is unavailable. |
| `countries[].claim_count` | Retrievable Claims for the country. |
| `countries[].source_count` | Distinct active Sources available for the country. |
| `countries[].first_claim_at` | Earliest Claim ingestion timestamp, or `null`. |
| `countries[].last_claim_at` | Latest Claim ingestion timestamp, or `null`. |

Example:

```json
{
  "countries": [
    {
      "code": "CM",
      "name": "Cameroon",
      "status": "live",
      "claim_count": 18420,
      "source_count": 37,
      "first_claim_at": "2026-05-02T06:14:20.931Z",
      "last_claim_at": "2026-07-20T21:26:42.133Z"
    }
  ],
  "annotation_layers": ["acled_v1", "geospatial_v1", "glide_v1", "iasc_v1"]
}
```

Coverage includes only successfully extracted Claims available through
`/claims`. Rejected and failed extraction outcomes do not contribute. A
country's `claim_count` equals a complete country-scoped `/claims` walk. Its
`last_claim_at` comes from a Claim available through `/claims`.

Responses use `Cache-Control: private, max-age=300`.

### `GET /api/v1/rubric`

Use this public, versioned vocabulary to build `category`, `attribute`, and
`attribute_any` filters. An attribute is a binary property that may be true for
a Signal. A category groups related attributes and matches when any member
attribute is true. Scaled and numeric properties are not returned because they
are not graded or stored.

| Parameter | Meaning |
| --- | --- |
| None | |

| Field | Meaning |
| --- | --- |
| `version` | Current rubric vocabulary version, presently `v1.1`. |
| `categories[].id` | Exact value accepted by `category`. |
| `categories[].label` | Category display label. |
| `categories[].attributes` | Ordered member attribute ids. |
| `attributes[].id` | Exact value accepted by `attribute` and `attribute_any`. |
| `attributes[].label` | Short display label when the attribute belongs to a category, or `null`. |
| `attributes[].description` | Definition used to grade the attribute. |
| `attributes[].section` | Group used to organize related attributes. This value is not filterable. |
| `attributes[].categories` | Categories that include the attribute. An empty array means it does not belong to a category. |

Selected elements from the complete response:

```json
{
  "version": "v1.1",
  "categories": [
    {
      "id": "environmental",
      "label": "Environmental",
      "attributes": [
        "involves_environmental_damage",
        "involves_deforestation",
        "involves_water_contamination",
        "involves_extractive_industry"
      ]
    }
  ],
  "attributes": [
    {
      "id": "involves_environmental_damage",
      "label": "Environmental Damage",
      "description": "General environmental harm, ecosystem damage",
      "section": "harm_type",
      "categories": ["environmental"]
    },
    {
      "id": "involves_air_pollution",
      "label": null,
      "description": "Air quality issues, toxic fumes, emissions, smog, respiratory hazards",
      "section": "harm_type",
      "categories": []
    }
  ]
}
```

Ids are case-sensitive. Responses use `Cache-Control: public, max-age=3600`.

### `GET /api/v1/annotation-layers`

Retrieve the annotation vocabularies available to your key. Customer
keys receive only granted layers. An empty `layers` array means no layers are
granted. Team keys receive every registered layer.

| Parameter | Meaning |
| --- | --- |
| None | |

| Field | Meaning |
| --- | --- |
| `layers[].id` | Versioned layer id used as the Signal filter parameter and `annotations` key. |
| `layers[].name` | Display name for the classification vocabulary. |
| `layers[].version` | Vocabulary version. |
| `layers[].attribution.name` | Organization that maintains the vocabulary. |
| `layers[].attribution.url` | Official website for the vocabulary. |
| `layers[].codes[].code` | Exact, case-sensitive API filter and annotation value. |
| `layers[].codes[].canonical` | Canonical value or name in the vocabulary. |
| `layers[].codes[].meaning` | Definition of the code. |

Selected codes from one granted layer:

```json
{
  "layers": [
    {
      "id": "glide_v1",
      "name": "GLIDE disaster event types",
      "version": "1",
      "attribution": {
        "name": "GLIDE",
        "url": "https://glidenumber.net"
      },
      "codes": [
        {
          "code": "ff",
          "canonical": "FF — Flash Flood",
          "meaning": "Taxonomy path: natural hazard / flash flood / flash flood. Key phrases: flash flood, sudden flooding, rapid-onset flood, torrential rain flooding, urban flash flood, floodwaters surged, runoff flooding, swept away by flood."
        },
        {
          "code": "fl",
          "canonical": "FL — Flood",
          "meaning": "Taxonomy path: natural hazard / flood / flood. Key phrases: flood, flooding, river overflow, inundation, overflowing river, waterlogged areas, floodwaters, seasonal flooding."
        }
      ]
    }
  ]
}
```

Each layer's `codes` array is complete and ordered. Responses use
`Cache-Control: private, max-age=300`.

### `GET /api/v1/signals`

With `query`, results use word matching and semantic similarity across the accessible corpus. `search.mode` is `hybrid`, or `lexical` when the embedding service is unavailable. `search.reranked` indicates whether evidence-based reranking succeeded; false means the base retrieval order is used. `search.truncated` indicates more matches than the result limit; `nextPage` is null for search. Search is a relevance-ranked retrieval aid, not an exhaustive export. Without `query`, the existing paginated listing is unchanged. No results means no matching reports under the selected filters, not that no event occurred.

Explore active, corroborated Signals with rubric and annotation filters.
Parameters combine with AND. Values within `country`, `category`,
`attribute_any`, and each annotation layer use OR. Multiple `attribute` values
use AND. Results use a stable keyset order.

| Parameter | Meaning |
| --- | --- |
| `query` | Search words and meaning (2–500 characters). Returns up to `limit` results ranked by relevance. No cursor is accepted with a query; narrow your query or filters when `search.truncated` is true. |
| `country` | Filter by ISO-2 country code. Repeat or comma-separate values to use OR. Matching ignores case. Omission uses your full access; any unentitled value rejects the request. |
| `category` | Filter by exact category id. Repeat or comma-separate ids to match a Signal with any member attribute from any listed category. |
| `attribute` | Filter by exact attribute id. Repeat or comma-separate ids; every listed attribute must be true. |
| `attribute_any` | Filter by exact attribute id. Repeat or comma-separate ids; at least one must be true. This group combines with every other filter using AND. |
| `glide_v1` | Filter by exact GLIDE code. Repeat or comma-separate codes to use OR within this layer. |
| `acled_v1` | Filter by exact ACLED code. Repeat or comma-separate codes to use OR within this layer. |
| `iasc_v1` | Filter by exact IASC code. Repeat or comma-separate codes to use OR within this layer. |
| `iep_positive_peace_v1` | Filter by exact IEP Positive Peace pillar. Repeat or comma-separate codes to use OR within this layer. Append `:strengthening`, `:eroding`, or `:mixed` to narrow one pillar, as `well_functioning_government:eroding`. |
| `iep_positive_peace_v1_direction` | Filter by `strengthening`, `eroding`, or `mixed`. Repeat or comma-separate to use OR. With a pillar filter the direction must hold for that pillar; alone it matches any pillar carrying it. |
| `geospatial_v1` | Filter by exact geospatial imagery-suitability code. Repeat or comma-separate codes to use OR within this layer. |
| `topic` | Filter by current parent Topic UUID. Invalid UUIDs return `400 invalid_parameter`. |
| `since` | Filter by Signals whose latest occurrence falls after this exclusive bound. Defaults to `90d`. Use the shared date and window formats, or `all`. |
| `limit` | Set the page size. The default is 50; values above 500 are clamped to 500. |
| `cursor` | Continue with the opaque cursor from `nextPage`. Omit it to start; empty or invalid values return `400 invalid_cursor`. |
| `include_annotations` | For team keys, set `true` to include all stored annotations. `false` or an empty value omits them. Customer responses always follow grants. |

Within an annotation layer, a Signal matches any requested code. A Signal
without stored data for that layer does not match. Separate annotation layers
combine with AND. Rubric filters, annotation filters, `country`, `topic`, and
`since` also combine with AND.

Unknown category, attribute, or code values return `400 invalid_parameter`.
Filtering by an ungranted customer layer returns
`403 annotation_layer_not_entitled`.

| Field | Meaning |
| --- | --- |
| `id` | Signal UUID, subject to the merge lifecycle described below. |
| `label` | Human-readable name for the occurrence. |
| `description` | Longer Signal description, or `null`. |
| `country_code` | ISO-2 country code. |
| `signal_start` | Earliest effective date among the Signal's Claims in `YYYY-MM-DD` format, or `null`. |
| `signal_end` | Latest effective date among the Signal's Claims in `YYYY-MM-DD` format, or `null`. |
| `last_claim_at` | Most recent Claim activity timestamp, or `null`. |
| `formed_at` | Signal formation timestamp. |
| `claim_count` | Claims in the Signal. |
| `source_count` | Distinct Sources supporting the Signal. |
| `topic` | Current parent `{id, label}`, or `null`. |
| `rubric.version` | Rubric version used to grade the Signal. Legacy rows use the current vocabulary. |
| `rubric.categories` | Derived category ids in canonical category order. |
| `rubric.attributes` | True attribute ids in stored order. |
| `annotations` | Stored annotations available to your key, keyed by layer id. Each value contains `codes` and `rationale`, and `directions` on layers whose codes name a condition. The field is absent when none are included. |

Example:

```json
{
  "signals": [
    {
      "id": "28eeab9d-47f6-4a2e-bd28-cafe6ad6ca19",
      "label": "Flooding disrupts homes and water access in Far North",
      "description": "Seasonal floodwater affected several communities and interrupted safe-water access.",
      "country_code": "CM",
      "signal_start": "2026-07-18",
      "signal_end": "2026-07-20",
      "last_claim_at": "2026-07-20T08:45:00.000Z",
      "formed_at": "2026-07-20T09:10:00.000Z",
      "claim_count": 1,
      "source_count": 1,
      "topic": {
        "id": "18aad58a-73fb-4966-a640-626c79aa7d39",
        "label": "Far North climate and displacement"
      },
      "rubric": {
        "version": "v1.1",
        "categories": ["displacement", "natural_disasters", "environmental"],
        "attributes": [
          "involves_flood",
          "involves_displacement",
          "involves_water_contamination"
        ]
      },
      "annotations": {
        "glide_v1": {
          "codes": ["fl"],
          "rationale": "The Signal describes river flooding affecting several communities."
        },
        "iasc_v1": {
          "codes": ["wash", "shelter"],
          "rationale": "The reported impacts concern safe water and emergency shelter."
        },
        "iep_positive_peace_v1": {
          "codes": ["equitable_distribution_of_resources"],
          "directions": { "equitable_distribution_of_resources": "eroding" },
          "rationale": "Damage to water infrastructure widened the gap in access between affected communities."
        }
      }
    }
  ],
  "nextPage": null,
  "pagination": {
    "limit": 50,
    "count": 1
  }
}
```

Responses use `Cache-Control: private, max-age=60`.

### `GET /api/v1/signals/{id}`

Retrieve the canonical active Signal with all of its retrievable Claims.
Claims are ordered by numeric id. If the requested id was retired by a merge
or split, the API follows its primary successor and returns the canonical
Signal instead.

| Parameter | Meaning |
| --- | --- |
| `id` | Signal UUID. Invalid UUIDs return `400 invalid_parameter`. |
| `include_annotations` | Uses the same annotation behavior as the Signal list. |

The response adds `claims` to the Signal fields returned by the list endpoint.
Each embedded Claim uses the list Claim shape. Its `signal_id` equals the
parent Signal id.

When an attached satellite chip exists and the organization has the
`geospatial_v1` enrichment enabled, the response also includes an `imagery`
array. Each entry contains `kind`, `provider`, `scene_id`,
`scene_datetime`, `scene_cloud_cover`, `aoi_cloud_fraction`, `bbox` as
`[west, south, east, north]`, pixel `width` and `height`, `attribution`, an
analyst-written `caption` (a string, or `null` when none has been written),
and the authenticated PNG `url`. Cloud values are numbers or `null`. Entries
are ordered as a before/after story: `pre_event`, then `post_event`, then any
derived analysis layer such as `burn_map`. The entire key is absent without
the enrichment grant, while imagery is pending, when no clear scene was found,
after a failure, or when imagery does not apply to the Signal.

Example:

```json
{
  "signal": {
    "id": "28eeab9d-47f6-4a2e-bd28-cafe6ad6ca19",
    "label": "Flooding disrupts homes and water access in Far North",
    "description": "Seasonal floodwater affected several communities and interrupted safe-water access.",
    "country_code": "CM",
    "signal_start": "2026-07-18",
    "signal_end": "2026-07-20",
    "last_claim_at": "2026-07-20T08:45:00.000Z",
    "formed_at": "2026-07-20T09:10:00.000Z",
    "claim_count": 1,
    "source_count": 1,
    "topic": {
      "id": "18aad58a-73fb-4966-a640-626c79aa7d39",
      "label": "Far North climate and displacement"
    },
    "rubric": {
      "version": "v1.1",
      "categories": ["displacement", "natural_disasters", "environmental"],
      "attributes": [
        "involves_flood",
        "involves_displacement",
        "involves_water_contamination"
      ]
    },
    "imagery": [
      {
        "kind": "post_event",
        "provider": "sentinel-2-l2a",
        "scene_id": "S2B_20260723T091029_CM",
        "scene_datetime": "2026-07-23T09:10:29.000Z",
        "scene_cloud_cover": 8.42,
        "aoi_cloud_fraction": 0.0312,
        "bbox": [14.2731, 10.5487, 14.3583, 10.6336],
        "width": 512,
        "height": 512,
        "attribution": "Contains modified Copernicus Sentinel data 2026",
        "caption": null,
        "url": "/api/v1/signals/28eeab9d-47f6-4a2e-bd28-cafe6ad6ca19/imagery/post_event.png"
      }
    ],
    "claims": [
      {
        "id": "5210",
        "claim": "Floodwater entered homes and interrupted access to a community water point.",
        "event_date": "2026-07-19",
    "occurred_on": "2026-07-19",
      "occurred_on": "2026-07-19",
        "occurred_on": "2026-07-19",
        "extracted_at": "2026-07-20T08:45:00.000Z",
        "country_code": "CM",
        "geo_scope": "point",
        "place_name": "Mayo Crossing",
        "place_country": "CM",
        "region_code": null,
        "lat": 10.5912,
        "lng": 14.3157,
        "signal_id": "28eeab9d-47f6-4a2e-bd28-cafe6ad6ca19",
        "article": {
          "url": "https://cameroon-observer.example/far-north-flood-update",
          "title": "Communities report flood damage in Far North",
          "published_at": "2026-07-20T07:30:00.000Z"
        },
        "source": {
          "id": "a12e699c-cfa2-4ce9-8c01-bec72963ea91",
          "name": "Cameroon Observer",
          "language": "en",
          "country_code": "CM"
        }
      }
    ]
  }
}
```

An alias response adds `resolved_from` beside `signal`. This abbreviated
example focuses on the response wrapper:

```json
{
  "signal": {
    "id": "28eeab9d-47f6-4a2e-bd28-cafe6ad6ca19",
    "label": "Flooding disrupts homes and water access in Far North",
    "claims": []
  },
  "resolved_from": {
    "requested_id": "831ecb98-66b4-4503-b6f7-b66a7477afc0",
    "relation": "merged",
    "resolved_at_depth": 2,
    "overlap": 0.875
  }
}
```

The actual `signal` object is the complete canonical payload shown above,
including only that Signal's Claims. `relation` and `overlap` describe the
first transition from the requested id; `resolved_at_depth` is the number of
transitions followed. Treat `resolved_from` as a prompt to replace the stored
reference with `signal.id`.

If a known id has no active successor, the endpoint returns
`410 Gone`:

```json
{
  "error": "signal_retired",
  "id": "831ecb98-66b4-4503-b6f7-b66a7477afc0",
  "relation": "dissolved",
  "last_seen": "2026-07-20T09:10:00.000Z",
  "successors": []
}
```

`last_seen` is omitted when it cannot be derived. An id that never existed
still returns `404 not_found`.

An existing Signal outside your country access returns
`403 country_not_entitled`. Responses use
`Cache-Control: private, max-age=60`.

### `GET /api/v1/signals/{id}/imagery/{kind}.png`

Retrieve one attached satellite imagery chip. The route resolves retired merge
and split ids to the same canonical active Signal as Signal detail, then applies
the same country entitlement check. Customer organizations must also have the
`geospatial_v1` enrichment enabled.

| Parameter | Meaning |
| --- | --- |
| `id` | Signal UUID. Invalid UUIDs return `400 invalid_parameter`. |
| `kind` | Imagery kind. The currently emitted value is `post_event`. |

A missing Signal, unknown imagery kind, pending row, failed row, terminal
no-scene row, absent row, or missing enrichment grant returns the same
`404 not_found`. An existing Signal outside your country access returns
`403 country_not_entitled`.

Successful responses use `Content-Type: image/png`,
`Cache-Control: private, max-age=86400`, and include the required attribution in
`X-Imagery-Attribution`.

### `GET /api/v1/topics`

Topics group related Signals for browsing.

> **Topic ids are temporary.** They can change between processing runs, and
> the API does not preserve old ids. Store Signal or Claim ids instead. Use
> filters to find the current Topics.

Results use a stable keyset order. Topic responses do not include `rubric` or
`annotations`.

| Parameter | Meaning |
| --- | --- |
| `country` | Filter by the Topic's ISO-2 country code. Repeat or comma-separate values to use OR. Matching ignores case. |
| `category` | Filter by exact category id on a grouped Signal. Repeat or comma-separate ids to use OR. |
| `attribute` | Require one grouped Signal to have every listed attribute. Repeat or comma-separate exact ids. |
| `attribute_any` | Require the same grouped Signal to have at least one listed attribute. Repeat or comma-separate exact ids. |
| `glide_v1` | Match any listed GLIDE code on the same grouped Signal. Repeat or comma-separate exact codes. |
| `acled_v1` | Match any listed ACLED code on the same grouped Signal. Repeat or comma-separate exact codes. |
| `iasc_v1` | Match any listed IASC code on the same grouped Signal. Repeat or comma-separate exact codes. |
| `iep_positive_peace_v1` | Match any listed IEP Positive Peace pillar on the same grouped Signal. Repeat or comma-separate exact codes. |
| `iep_positive_peace_v1_direction` | Match any listed direction on the same grouped Signal. Repeat or comma-separate `strengthening`, `eroding`, or `mixed`. |
| `geospatial_v1` | Match any listed geospatial imagery-suitability code on the same grouped Signal. Repeat or comma-separate exact codes. |
| `since` | Filter by Topics grouping a Signal whose latest occurrence falls after this exclusive bound. Defaults to `90d`. Use the shared date and window formats, or `all`. |
| `limit` | Set the page size. The default is 50; values above 500 are clamped to 500. |
| `cursor` | Continue with the opaque cursor from `nextPage`. Omit it to start; empty or invalid values return `400 invalid_cursor`. |

One active, labeled Signal within a Topic must satisfy every rubric and
annotation filter. Two Signals cannot satisfy different parts of the same
request.
`category`, `attribute_any`, and codes within one annotation layer use OR.
`attribute`, separate layers, and separate filter groups use AND.

| Field | Meaning |
| --- | --- |
| `id` | Temporary Topic UUID. Do not store it. |
| `label` | Topic label. |
| `description` | Topic description, or `null`. |
| `country_code` | ISO-2 country code. |
| `signal_count` | Active Signals grouped into the Topic. |
| `claim_count` | Stored number of Claims represented by the Topic. |
| `last_claim_at` | Most recent Topic activity timestamp, or `null`. |
| `formed_at` | Topic formation timestamp. |
| `is_fallback` | Whether this is the fallback Topic for Signals that do not form a coherent group. |

Example:

```json
{
  "topics": [
    {
      "id": "18aad58a-73fb-4966-a640-626c79aa7d39",
      "label": "Far North climate and displacement",
      "description": "Current reporting on seasonal hazards and their community impacts.",
      "country_code": "CM",
      "signal_count": 4,
      "claim_count": 18,
      "last_claim_at": "2026-07-20T08:45:00.000Z",
      "formed_at": "2026-07-20T09:00:00.000Z",
      "is_fallback": false
    }
  ],
  "nextPage": null,
  "pagination": {
    "limit": 50,
    "count": 1
  }
}
```

Responses use `Cache-Control: private, max-age=60`.

### `GET /api/v1/claims`

With `query`, results use word matching and semantic similarity across the accessible corpus. `search.mode` is `hybrid`, or `lexical` when the embedding service is unavailable. `search.reranked` indicates whether evidence-based reranking succeeded; false means the base retrieval order is used. `search.truncated` indicates more matches than the result limit; `nextPage` is null for search. Search is a relevance-ranked retrieval aid, not an exhaustive export. Without `query`, the existing paginated listing is unchanged. No results means no matching reports under the selected filters, not that no event occurred.

List immutable, extracted Claims with article, Source, geography, and current
Signal assignment. Claims are ordered by numeric `id`.

| Parameter | Meaning |
| --- | --- |
| `query` | Search words and meaning (2–500 characters). Returns up to `limit` results ranked by relevance. No cursor is accepted with a query; narrow your query or filters when `search.truncated` is true. |
| `country` | Filter by ISO-2 country code. Repeat or comma-separate values to use OR. Matching ignores case. Omission uses your full country access. |
| `signal` | Filter by current Signal UUID. Invalid UUIDs return `400 invalid_parameter`. A valid UUID with no matches returns an empty list. |
| `since` | Filter by occurrence time (`occurred_on`) after this exclusive bound. Claims with a `null` `occurred_on` match only `all`. Defaults to `90d` unless you pass `since_extracted`. |
| `since_extracted` | Filter by `extracted_at` after this exclusive bound. Use this parameter for incremental polling. It also turns off the default `since` window. |
| `limit` | Set the page size. The default is 50; values above 500 are clamped to 500. |
| `cursor` | Continue with the opaque cursor from `nextPage`. Omit it to start; empty or invalid values return `400 invalid_cursor`. |

When both time filters are present, both must match. Claims do not support
rubric filters because rubric attributes belong to Signals. A new Claim may
not have a Signal assignment yet, so rubric filtering would omit it from a
poll.

To retrieve Claims under a current Topic, list its Signals with
`/signals?topic={id}`. Then query Claims by each Signal id.

| Field | Meaning |
| --- | --- |
| `id` | Immutable numeric id serialized as a string because database integers can exceed safe JSON integer ranges. |
| `claim` | Assertion extracted from the source. |
| `event_date` | Event date stated by the source in `YYYY-MM-DD` format, or `null`. |
| `occurred_on` | Canonical occurrence date: `event_date` when stated, else the article publish date. `null` means the occurrence time is unknown. This is the date `since` filters on. |
| `extracted_at` | Ingestion timestamp. Poll with `since_extracted`. |
| `country_code` | Country used to ingest and authorize access to the Claim. |
| `geo_scope` | `point`, `country`, `region`, or `null`. |
| `place_name` | Extracted place name, or `null`. |
| `place_country` | ISO-2 event-place country, or `null`. |
| `region_code` | UN M49 region code for a region-scoped Claim, or `null`. |
| `lat` / `lng` | Coordinates only for a point-scoped Claim when both are available; otherwise both `null`. |
| `signal_id` | Current Signal UUID, or `null` while assignment lags ingestion. |
| `article` | `{url, title, published_at}`, or `null` when source content is unavailable. |
| `source` | `{id, name, language, country_code}`, or `null` when the relationship is unavailable. |

Example:

```json
{
  "claims": [
    {
      "id": "5210",
      "claim": "Floodwater entered homes and interrupted access to a community water point.",
      "event_date": "2026-07-19",
    "occurred_on": "2026-07-19",
      "occurred_on": "2026-07-19",
      "extracted_at": "2026-07-20T08:45:00.000Z",
      "country_code": "CM",
      "geo_scope": "point",
      "place_name": "Mayo Crossing",
      "place_country": "CM",
      "region_code": null,
      "lat": 10.5912,
      "lng": 14.3157,
      "signal_id": null,
      "article": {
        "url": "https://cameroon-observer.example/far-north-flood-update",
        "title": "Communities report flood damage in Far North",
        "published_at": "2026-07-20T07:30:00.000Z"
      },
      "source": {
        "id": "a12e699c-cfa2-4ce9-8c01-bec72963ea91",
        "name": "Cameroon Observer",
        "language": "en",
        "country_code": "CM"
      }
    }
  ],
  "nextPage": "https://api.livingsignals.example/api/v1/claims?country=CM&limit=1&cursor=NTIxMA",
  "pagination": {
    "limit": 1,
    "count": 1
  }
}
```

`article` is present when the Claim retains a source-content URL. Its `title`
and `published_at` can independently be `null`. The API decodes common and
numeric HTML entities in `article.title`.

`source` is present when the Claim retains a Source id. Its `name`, `language`,
and `country_code` can independently be `null`.

Distinguish the country fields:

| Field | Meaning |
| --- | --- |
| `country_code` | The country used to ingest the Claim. This controls access and may differ from the event location. |
| `place_country` | The country where the described occurrence happened, when known. |
| `region_code` | The UN M49 region for a region-scoped Claim when resolved. |
| `source.country_code` | The outlet's headquarters country. It is often `null` and can differ from the other country fields. |

Geography values:

| Value | Meaning |
| --- | --- |
| `point` | A place-scoped Claim. Coordinates appear only when both are stored. |
| `country` | A country-scoped Claim. `place_country` identifies the country. |
| `region` | A regional Claim. `region_code` identifies the M49 region when resolution succeeds. |
| `null` | Geographic precision was not assigned. |

Country- and region-scoped Claims do not include coordinates. Do not display
them as point markers.

#### Availability and assignment timing

Claims rejected or failed during extraction are not available through the API.
`/countries` excludes them from Claim counts and coverage dates.

Extraction and Signal assignment happen separately. A new Claim appears in
`/claims` immediately with `signal_id: null`. A later processing run may assign
it to a Signal. Polling with `since_extracted` still returns the Claim. Retrieve
`GET /api/v1/claims/{id}` again to get its current Signal assignment.

#### Polling recipe

For incremental ingestion, record the poll time immediately before the
request. Send it on the next poll as
`since_extracted=<previous poll time>`. Overlap adjacent windows slightly and
deduplicate by Claim `id`. This handles boundary timing, retries, and
incomplete page walks.

Follow every non-null `nextPage` unchanged before you advance the poll time.
This keeps the same filters and server-issued cursor across the page walk.

Do not use `occurred_on` or `since` as an incremental cursor. `occurred_on`
describes source content. It may be old, null, or unreliable. `since` filters
occurrence time and is not a polling watermark.

Responses use `Cache-Control: private, max-age=30`.

### `GET /api/v1/claims/{id}`

Retrieve one Claim in the same shape as a list item. The response wraps it in
`{"claim": ...}`.

| Parameter | Meaning |
| --- | --- |
| `id` | Numeric Claim id. Non-numeric input returns `400 invalid_parameter`. |

Example:

```json
{
  "claim": {
    "id": "5210",
    "claim": "Floodwater entered homes and interrupted access to a community water point.",
    "event_date": "2026-07-19",
    "occurred_on": "2026-07-19",
    "extracted_at": "2026-07-20T08:45:00.000Z",
    "country_code": "CM",
    "geo_scope": "point",
    "place_name": "Mayo Crossing",
    "place_country": "CM",
    "region_code": null,
    "lat": 10.5912,
    "lng": 14.3157,
    "signal_id": "28eeab9d-47f6-4a2e-bd28-cafe6ad6ca19",
    "article": {
      "url": "https://cameroon-observer.example/far-north-flood-update",
      "title": "Communities report flood damage in Far North",
      "published_at": "2026-07-20T07:30:00.000Z"
    },
    "source": {
      "id": "a12e699c-cfa2-4ce9-8c01-bec72963ea91",
      "name": "Cameroon Observer",
      "language": "en",
      "country_code": "CM"
    }
  }
}
```

An existing Claim outside your country access returns
`403 country_not_entitled`. A missing, rejected, or failed Claim returns
`404 not_found`. Responses use `Cache-Control: private, max-age=60`.

### `GET /api/v1/sources`

List active Sources by country with recent Claim coverage. Each row represents
one `(source, country_code)` pair. A Source appears once for each country it
covers. Results are ordered by Source `id` and then `country_code`.

| Parameter | Meaning |
| --- | --- |
| `country` | Filter by ISO-2 country code. Repeat or comma-separate values to use OR. Matching ignores case. Omission uses your full country access. |
| `language` | Filter by a two- or three-letter language code. Repeat or comma-separate values to use OR. Matching lowercases input. |
| `limit` | Set the page size. The default is 100; values above 500 are clamped to 500. |
| `cursor` | Continue with the opaque cursor from `nextPage`. Omit it to start; empty or invalid values return `400 invalid_cursor`. |

Country and language filters combine with AND.

| Field | Meaning |
| --- | --- |
| `id` | Source UUID. |
| `name` | Source display name. |
| `url` | Canonical Source URL. |
| `domain` | Lowercase hostname without a leading `www.`, or `null` when the URL is invalid. |
| `language` | Two- or three-letter language code, or `null`. |
| `country_code` | Country membership represented by this row, not necessarily Source headquarters. |
| `type` | Source catalog type string, or `null`. |
| `claim_count_30d` | Claims collected for this Source and country during the previous 30 days. This value does not affect ordering. |

Example:

```json
{
  "sources": [
    {
      "id": "a12e699c-cfa2-4ce9-8c01-bec72963ea91",
      "name": "Cameroon Observer",
      "url": "https://cameroon-observer.example",
      "domain": "cameroon-observer.example",
      "language": "en",
      "country_code": "CM",
      "type": "newspaper_independent",
      "claim_count_30d": 143
    }
  ],
  "nextPage": null,
  "pagination": {
    "limit": 100,
    "count": 1
  }
}
```

Source type values:

| Value | Meaning |
| --- | --- |
| `blog` | A blog-format or lightly edited news publisher. |
| `broadcaster` | A television-first or multi-platform broadcast news outlet. |
| `gov_official` | An official government publisher or news outlet. |
| `magazine` | A magazine or periodical journalism outlet. |
| `news_aggregator` | A service that collects or curates reporting from other publishers. |
| `newspaper_independent` | An independently controlled newspaper or newsroom. |
| `newspaper_state` | A state-owned or state-controlled newspaper. |
| `radio` | A radio-first news outlet. |
| `wire` | A news agency or wire service that distributes reports to other outlets. |

`type` may contain any string or `null`. Preserve values you do not recognize.
Responses use `Cache-Control: private, max-age=300`.

### `GET /api/v1/docs` and `GET /api/v1/openapi.json`

Use these public endpoints to retrieve the Markdown and OpenAPI references.
They serve fixed documents, so query parameters do not change the response.

| Parameter | Meaning |
| --- | --- |
| None | |

| Endpoint | Content type | Meaning |
| --- | --- | --- |
| `GET /api/v1/docs` | `text/markdown; charset=utf-8` | Public Markdown API reference. |
| `GET /api/v1/openapi.json` | JSON | Public OpenAPI 3.1 route and schema document. |

Markdown response:

```markdown
# Living Signals REST API v1

Use the read-only Living Signals REST API to explore news-derived Claims,
corroborated Signals, and related Topics.
```

OpenAPI response excerpt:

```json
{
  "openapi": "3.1.0",
  "info": {
    "title": "Living Signals API",
    "version": "v1"
  },
  "paths": {
    "/api/v1/signals": {
      "get": {
        "summary": "List Signals"
      }
    },
    "/api/v1/claims": {
      "get": {
        "summary": "List Claims"
      }
    }
  }
}
```

Browse the OpenAPI document at `/reference`. Responses from `/docs` and
`/openapi.json` use `Cache-Control: public, max-age=3600`.

## Signal lifecycle and id stability

Claim ids are immutable. Signal ids are durable references. When a merge or
split retires an id, `GET /api/v1/signals/{id}` follows lineage transitively to
the primary active successor and returns its full payload with
`resolved_from`. Primary successors are selected by highest stored overlap,
then by the latest transition time.

Claims in the response always belong to the canonical Signal; the API does not
combine Claims from historical identities. When `resolved_from` appears,
update the stored reference to the returned `signal.id`.

If an id existed but no active successor remains, the detail endpoint returns
the structured `410 signal_retired` body with an empty `successors` array.
An id that never existed returns `404 not_found`.

A Signal can move to a different Topic without receiving a new Claim. Treat
`signal.topic` as current browsing context, not durable identity.

Signals do not expose `updated_at` or a `since_updated` watermark. Incremental
Claim polling cannot fully reveal relabels, merges, annotation updates, or
Topic changes. Periodically perform a complete Signal sync in addition to
polling Claims.

## Appendix: rubric vocabulary

The rubric assigns binary attributes to each Signal. Multiple attributes can
be true. Categories group related attributes. A Signal belongs to a category
when any member attribute is true, so category membership is derived and can
also have multiple values.

`GET /api/v1/rubric` is the machine-readable source of truth. Its category
order and member order match these tables.

### `violence_harm` — Violence & Harm

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_death` | Deaths/Fatalities | Reports deaths or fatalities |
| `involves_physical_injury` | Injuries | Reports physical injuries |
| `involves_sexual_violence` | Sexual Violence | Sexual violence or assault |
| `involves_domestic_violence` | Domestic Violence | Domestic or intimate partner violence |
| `involves_kidnapping` | Kidnapping | Kidnapping or hostage-taking |
| `involves_detention_arrest` | Detention/Arrest | Arrests or detentions |
| `involves_property_destruction` | Property Destruction | Destruction of property |
| `civilians_directly_targeted` | Civilians Targeted | Civilians directly targeted |

### `conflict_intensity` — Conflict & Unrest

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_tensions_unrest` | Tensions/Unrest | Social tensions without physical violence |
| `involves_protest_demonstration` | Protests | Protests or demonstrations |
| `involves_low_intensity_violence` | Skirmishes | Low-intensity violence, clashes |
| `involves_firearms` | Firearms Used | Use of firearms or gunfire |
| `involves_explosives` | Explosives/IEDs | Bombs, IEDs, explosions |

### `displacement` — Displacement & Migration

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_displacement` | Displacement | People displaced from homes |
| `affects_displaced_persons` | Affects Refugees/IDPs | Impacts on already-displaced |

### `natural_disasters` — Natural Disasters

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_flood` | Flooding | Floods, flash floods |
| `involves_drought` | Drought | Drought, water scarcity |
| `involves_storm` | Storms | Hurricanes, cyclones, severe weather |
| `involves_earthquake_geological` | Earthquakes | Earthquakes, geological events |
| `involves_wildfire` | Wildfires | Wildfires, bush fires |

### `health_humanitarian` — Health & Humanitarian

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_disease_outbreak` | Disease Outbreak | Epidemics, health emergencies |
| `involves_humanitarian_response` | Aid Response | Humanitarian aid delivery |

### `environmental` — Environmental

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_environmental_damage` | Environmental Damage | General environmental harm |
| `involves_deforestation` | Deforestation | Forest destruction, logging |
| `involves_water_contamination` | Contamination/Pollution | Water pollution or scarcity |
| `involves_extractive_industry` | Extractive Industry | Mining, oil/gas impacts |

### `actors` — Actors Involved

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_state_military` | State Military | National army, military forces |
| `involves_state_police` | Police/Security | Police, law enforcement |
| `involves_non_state_armed_group` | Armed Groups | Rebels, militias, insurgents |
| `involves_criminal_actors` | Criminal Actors | Gangs, cartels, bandits |

### `affected_populations` — Affected Populations

| Attribute id | Label | Description |
| --- | --- | --- |
| `affects_women_girls` | Women & Girls | Gender-specific impacts |
| `affects_children` | Children | Children as victims |
| `affects_marginalized_community` | Marginalized Groups | Vulnerable communities |

### `women_peace_security` — Women, Peace & Security

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_women_as_actors` | Women as Actors | Women leading, organizing, mediating, advocating |
| `involves_gender_policy_action` | Gender Policy Action | Government/institutional action on gender equality |
| `involves_gender_targeted_rhetoric` | Gender-Targeted Rhetoric | Gender-based hate speech or incitement |

### `structural` — Social Harms

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_discrimination` | Discrimination | Unequal treatment |
| `involves_marginalization` | Marginalization | Social exclusion |
| `involves_dispossession` | Dispossession | Loss of land/resources |
| `involves_chronic_deprivation` | Chronic Deprivation | Long-term poverty |

### `positive_signals` — Peace, Justice & Reform

| Attribute id | Label | Description |
| --- | --- | --- |
| `involves_peace_process` | Peace Process | Peace negotiations, ceasefires |
| `involves_mediation_dialogue` | Mediation/Dialogue | Conflict resolution efforts |
| `involves_accountability_justice` | Justice Mechanism | Trials, prosecutions, inquiries, transitional justice |
| `involves_policy_reform` | Institutional Change | New laws, policy changes, institutional reforms |

### Attributes without a category

These attributes do not belong to a category. You can still filter by them
with `attribute` and `attribute_any`. They also appear in `rubric.attributes`.
In `GET /api/v1/rubric`, they have `label: null` and an empty `categories`
array. The `section` field groups them by theme.

#### `harm_type`

| Attribute id | Description |
| --- | --- |
| `involves_harassment_abuse` | Harassment, threats, intimidation, psychological abuse, coercive control, exploitation |
| `involves_harmful_traditional_practice` | FGM/C, forced/child marriage, honor-based violence, widow inheritance |
| `involves_forced_immobility` | Trapped populations, movement restrictions, inability to evacuate, siege conditions |
| `involves_looting_theft` | Looting, armed robbery, highway robbery, theft, pillaging |
| `involves_economic_loss` | Business closures, job losses, crop failures, financial losses, fiscal crisis |
| `involves_service_disruption` | Power outages, water supply cut, healthcare unavailable, schools closed |
| `involves_reproductive_health` | Maternal mortality, reproductive health access, pregnancy complications, family planning barriers |
| `involves_psychological_trauma` | Trauma, hopelessness, fear, despair, mental health crisis, loneliness epidemic |
| `involves_air_pollution` | Air quality issues, toxic fumes, emissions, smog, respiratory hazards |
| `involves_soil_contamination` | Heavy metals, toxic waste, soil poisoning, desertification, erosion |
| `involves_biodiversity_loss` | Species loss, wildlife deaths, endangered species impacts, ecosystem collapse, bushmeat hunting |
| `involves_extreme_temperature` | Heat wave, extreme cold, temperature-related deaths or impacts |
| `involves_arson` | Arson, deliberate burning of buildings/property, fire as weapon |
| `involves_crop_failure` | Failed harvests, crop destruction, agricultural damage, farm losses |
| `involves_livestock_loss` | Animal deaths, livestock disease, cattle raiding, herder losses |
| `involves_market_disruption` | Market closures, supply shortages, price spikes, food scarcity |

#### `actors`

| Attribute id | Description |
| --- | --- |
| `involves_terrorist_designation` | Groups explicitly labeled as terrorist by governments or international bodies |
| `involves_civilian_perpetrators` | Mob violence, individual attackers, community violence |
| `involves_foreign_actors` | Foreign governments, international forces, cross-border actors |
| `involves_corporate_actors` | Companies, extractive industries, private contractors |
| `involves_conservation_actors` | Eco-guards, park rangers, conservation officers, environmental police |
| `involves_poachers_illegal_extractors` | Poachers, illegal loggers, artisanal/illegal miners, smugglers of natural resources |
| `involves_traditional_authorities` | Chiefs, traditional councils, customary leaders, elders |
| `involves_civil_society` | NGOs, community organizations, advocacy groups, humanitarian organizations |
| `involves_international_organizations` | UN agencies, international NGOs, peacekeepers, international monitors |

#### `affected_population`

| Attribute id | Description |
| --- | --- |
| `affects_general_population` | Broad impact not targeting specific demographics |
| `affects_specific_ethnic_group` | Ethnic targeting, inter-ethnic violence, ethnic minorities affected |
| `affects_specific_religious_group` | Religious targeting, sectarian violence, religious minorities affected |
| `affects_elderly` | Elderly/aged as victims or disproportionately affected |
| `affects_journalists_activists` | Press freedom, civil society targeting, activist persecution |
| `affects_healthcare_workers` | Attacks on health workers, hospitals targeted, medical staff affected |

#### `causation`

| Attribute id | Description |
| --- | --- |
| `cause_natural` | Weather, geological events, natural disasters, disease (natural origin) |
| `cause_intentional_human` | Deliberate attack, planned action, intentional harm |
| `cause_unintentional_human` | Accidents, negligence, unintended consequences, human error |
| `cause_institutional_systemic` | Policy failures, structural issues, systemic neglect, institutional causes |
| `cause_infrastructure_failure` | Road collapse, dam failure, building collapse, utility failure |
| `involves_bureaucratic_delay` | Administrative delays, permit denials, bureaucratic obstruction, slow response |

#### `sector`

| Attribute id | Description |
| --- | --- |
| `affects_healthcare_system` | Hospitals, clinics, health services, medical supplies, healthcare access |
| `affects_education_system` | Schools closed, education disrupted, students affected |
| `affects_water_sanitation` | Water access, clean water, sanitation systems, WASH |
| `affects_food_security` | Food access, agriculture, food prices, hunger, nutrition |
| `affects_shelter_housing` | Homelessness, housing destruction, shelter needs |
| `affects_transportation` | Roads, bridges, airports, transit systems, mobility |
| `affects_communications` | Internet, phones, media, communications blackouts |
| `affects_livelihoods` | Jobs, businesses, economic activities, income sources |
| `affects_governance` | Elections, government function, political institutions, rule of law |
| `involves_reintegration` | DDR programs, ex-combatant reintegration, refugee return, community reintegration |
| `involves_community_resilience` | Local coping strategies, community-led response, self-organization, mutual aid |

#### `verification`

| Attribute id | Description |
| --- | --- |
| `official_source_cited` | Government statements, official reports, institutional data |
| `multiple_sources_mentioned` | Corroboration from multiple sources mentioned in the content |
| `firsthand_account` | Direct witness testimony, on-the-ground reports, first-person accounts |
| `numbers_explicitly_estimated` | Numbers qualified as estimates, approximations, or uncertain |

## Appendix: annotation layers

Annotation layers map Signals to attributed, independent classification
vocabularies. Each layer is versioned. Annotation layers appear only on Signals
and do not affect how Signals or Topics form.

Use `GET /api/v1/countries` to discover available layer ids in
`annotation_layers`. Use `GET /api/v1/annotation-layers` to retrieve each
layer's version, attribution, and complete code vocabulary. Customer keys
receive granted layers. Team keys receive every registered layer.

For a customer key, interpret annotations this way:

| Layer granted? | Signal value | Meaning |
| --- | --- | --- |
| Yes | Layer key with non-empty `codes` | Graded and stored with the applicable codes. |
| Yes | Layer key with `codes: []` | Graded and stored, but no code applies. |
| Yes | Layer key absent | Grading is not yet stored; treat it as pending. |
| No | Layer key absent | The key is not entitled, so the layer is not exposed. |

For a team key, omitting `include_annotations`, sending an empty value, or
sending `include_annotations=false` omits all annotations. An absent layer then
says nothing about its grading state. Send `include_annotations=true` before
you use the table as an all-layer view.

Annotation example:

```json
{
  "annotations": {
    "glide_v1": {
      "codes": ["fl"],
      "rationale": "The Signal describes river flooding."
    }
  }
}
```

API codes use lowercase `snake_case`. GLIDE uses lowercase two-letter codes.
The `_v1` suffix pins the vocabulary version. Discover codes from
`/annotation-layers` instead of assuming that versions share the same values.

### `glide_v1` — GLIDE disaster event types

GLIDE is the GLobal IDEntifier number disaster-event taxonomy. API values are
lowercase; canonical GLIDE codes are uppercase two-letter codes.

| API code | Canonical GLIDE value | Meaning |
| --- | --- | --- |
| `cw` | `CW` — Cold Wave | Cold-wave events, including cold spells, cold snaps, extreme cold, freezing temperatures, and severe frost. |
| `ht` | `HT` — Heat Wave | Heat-wave events, including extreme, record, scorching, or prolonged heat. |
| `ce` | `CE` — Complex Emergency | A multifaceted or protracted humanitarian crisis, including conflict-driven emergencies or combined shocks. |
| `dr` | `DR` — Drought | Prolonged dry conditions such as rainfall deficit, water scarcity, crop failure, or a failed rainy season. |
| `eq` | `EQ` — Earthquake | Seismic events including earthquakes, quakes, tremors, and aftershocks. |
| `ep` | `EP` — Epidemic | A disease epidemic or outbreak, including rapid transmission and public-health emergencies. |
| `ec` | `EC` — Extratropical Cyclone | A mid-latitude or temperate cyclone, frontal storm, winter cyclone, or intense low-pressure system. |
| `fr` | `FR` — Fire | A fire event such as a structure, urban, building, or industrial fire. |
| `ff` | `FF` — Flash Flood | Sudden, rapid-onset flooding, often associated with torrential rain or runoff. |
| `fl` | `FL` — Flood | General flooding or inundation, including river overflow and seasonal flooding. |
| `in` | `IN` — Insect Infestation | An agricultural insect or pest outbreak, including locust swarms and armyworm infestation. |
| `sl` | `SL` — Slide | Ground, soil, earth, or slope movement and instability classified as a slide. |
| `ls` | `LS` — Land Slide | A landslide or land slide, including slope failure, hillside collapse, and debris slide. |
| `ms` | `MS` — Mud Slide | A mudslide, mudflow, or saturated debris flow, including rain-triggered mud movement. |
| `av` | `AV` — Snow Avalanche | A snow avalanche, snow-slide, slab release, or snowpack collapse. |
| `st` | `ST` — Severe Local Storm | A severe local or convective storm, including thunderstorms, hailstorms, and damaging windstorms. |
| `ac` | `AC` — Technological Disaster | A technological or industrial accident such as a chemical/oil spill, radiological incident, infrastructure failure, or dam failure. |
| `to` | `TO` — Tornado | A tornado or tornadic vortex, including funnel-cloud and touchdown events. |
| `tc` | `TC` — Tropical Cyclone | A tropical cyclone, hurricane, typhoon, or tropical storm, including landfall. |
| `ss` | `SS` — Storm Surge | Storm-driven coastal surge or inundation, including high storm tide and seawater intrusion. |
| `ts` | `TS` — Tsunami | A tsunami or seismic sea wave, including coastal wave run-up and inundation. |
| `vo` | `VO` — Volcano | A volcanic eruption or activity, including ash, lava, pyroclastic flow, and tephra fall. |
| `wf` | `WF` — Wild Fire | A wildfire or uncontrolled forest, bush, vegetation, or grass fire. |
| `vw` | `VW` — Violent Wind | Destructive high-wind events, including damaging gusts and gale-force winds. |
| `ot` | `OT` — Other | An emergency, humanitarian situation, or security incident not classified under another GLIDE event type. |

### `acled_v1` — ACLED event sub-types

ACLED is the Armed Conflict Location & Event Data Project taxonomy. API codes
are slugified forms of ACLED's canonical sub-event type names. The event-type
family in each meaning also comes from the registered definition.

| API code | Canonical ACLED sub-event type | Meaning |
| --- | --- | --- |
| `armed_clash` | Armed clash | A `Battles` sub-event recording an armed clash. |
| `government_regains_territory` | Government regains territory | A `Battles` sub-event in which government forces regain territory. |
| `non_state_actor_overtakes_territory` | Non-state actor overtakes territory | A `Battles` sub-event in which a non-state actor takes territory. |
| `chemical_weapon` | Chemical weapon | An `Explosions/Remote violence` sub-event involving a chemical weapon. |
| `air_or_drone_strike` | Air/drone strike | An `Explosions/Remote violence` sub-event involving an air or drone strike. |
| `suicide_bomb` | Suicide bomb | An `Explosions/Remote violence` sub-event involving a suicide bomb. |
| `shelling_artillery_missile` | Shelling/artillery/missile attack | An `Explosions/Remote violence` sub-event involving shelling, artillery, or a missile attack. |
| `remote_explosive_landmine_ied` | Remote explosive/landmine/IED | An `Explosions/Remote violence` sub-event involving a remote explosive, landmine, or IED. |
| `grenade` | Grenade | An `Explosions/Remote violence` sub-event involving a grenade. |
| `sexual_violence` | Sexual violence | A `Violence against civilians` sub-event specifically involving sexual violence; it does not mean restrictions on women's rights. |
| `attack` | Attack | A `Violence against civilians` event using ACLED's general Attack sub-event. |
| `abduction_forced_disappearance` | Abduction/forced disappearance | A `Violence against civilians` sub-event involving abduction or forced disappearance. |
| `peaceful_protest` | Peaceful protest | A `Protests` sub-event recording a peaceful protest. |
| `protest_with_intervention` | Protest with intervention | A `Protests` sub-event recording intervention in a protest. |
| `excessive_force_against_protesters` | Excessive force against protesters | A `Protests` sub-event recording excessive force against protesters. |
| `violent_demonstration` | Violent demonstration | A `Riots` sub-event recording a violent demonstration. |
| `mob_violence` | Mob violence | A `Riots` sub-event recording mob violence. |
| `agreement` | Agreement | A `Strategic developments` sub-event recording an agreement. |
| `arrests` | Arrests | A `Strategic developments` sub-event recording arrests. |
| `change_to_group_or_activity` | Change to group/activity | A `Strategic developments` sub-event recording a change to a group or its activity. |
| `disrupted_weapons_use` | Disrupted weapons use | A `Strategic developments` sub-event for physical weapon seizure or interception, not arms-control talks or agreements. |
| `headquarters_or_base_established` | Headquarters or base established | A `Strategic developments` sub-event recording establishment of a headquarters or base. |
| `looting_property_destruction` | Looting/property destruction | A `Strategic developments` sub-event involving looting or property destruction. |
| `non_violent_transfer_of_territory` | Non-violent transfer of territory | A `Strategic developments` sub-event recording a non-violent territorial transfer. |
| `other_strategic_development` | Other | ACLED's other `Strategic developments` sub-event. |

### `iasc_v1` — IASC humanitarian coordination clusters

IASC is the Inter-Agency Standing Committee humanitarian coordination
standard. API codes map to its canonical domain names.

| API code | Canonical IASC value | Meaning |
| --- | --- | --- |
| `protection` | Protection | Safety, dignity, and rights of affected people; child protection, GBV response, mine action, housing/land/property, and rule of law. |
| `health` | Health | Disease outbreaks, medical care, health facilities and workers, immunization, and mental health. |
| `wash` | Water, Sanitation and Hygiene | Safe-water access, sanitation, hygiene, and disease-vector control. |
| `shelter` | Shelter and NFI | Emergency shelter, housing, and non-food relief items. |
| `food_security` | Food Security | Food access and availability, agriculture, livelihoods, and market disruption affecting food. |
| `nutrition` | Nutrition | Malnutrition, therapeutic feeding, and infant and young-child feeding. |
| `education` | Education | Access to schooling, attacks on education, and learning continuity in emergencies. |
| `cccm` | Camp Coordination and Camp Management | Management of displacement sites and camps, site services, and population tracking. |
| `logistics` | Logistics | Supply chains, transport, storage, and access constraints for relief. |
| `early_recovery` | Early Recovery | Restoration of basic services, livelihoods, governance, and infrastructure after crisis. |
| `emergency_telecommunications` | Emergency Telecommunications | Communications infrastructure, connectivity, and data services for response, including telecom or internet blackouts. |

### `iep_positive_peace_v1` — IEP Positive Peace pillars

Positive Peace is the Institute for Economics & Peace framework describing the
attitudes, institutions, and structures that sustain peaceful societies. API
codes map to its pillar names.

This layer describes the societal condition a Signal speaks to, not what
happened. Positive Peace is defined against the absence of violence, which IEP
measures separately through the Global Peace Index, so violent events, natural
hazards, and their humanitarian impacts carry no pillar and return an empty
`codes` array. Use `glide_v1` and `acled_v1` to classify those.

A pillar names the condition under discussion, never a verdict that the
condition is in good health. An anti-corruption prosecution and an embezzlement
scandal both carry `low_levels_of_corruption`. Read the code together with
`directions`, which records which way the evidence points for each assigned
code:

| Direction | Meaning |
| --- | --- |
| `strengthening` | The Signal shows the condition being built up, improved, or upheld. |
| `eroding` | The Signal shows it being weakened, damaged, or failing. |
| `mixed` | The Signal shows both, or the evidence does not point either way. |

So a report of extrajudicial killings returns
`well_functioning_government` with direction `eroding` — the Signal is about the
rule of law, and the rule of law is failing.

To filter on direction, attach it to the pillar it applies to:

    ?iep_positive_peace_v1=well_functioning_government:eroding

The direction is scoped to that pillar, so a Signal whose rule of law is
strengthening does not match, even if another of its pillars is eroding. Pillars
in the same request are independent — `well_functioning_government:eroding,low_levels_of_corruption`
narrows the first and leaves the second matching in any direction. To ask the
looser question across the whole layer, use `iep_positive_peace_v1_direction`
on its own.

A pillar is recorded only where the Signal reveals something about how the
condition stands. Routine administration — procurement, workshops, ceremonies,
appointments, internal meetings — and internal political party business such as
primaries and campaign strategy carry no pillar on their own.

| API code | Canonical IEP value | Meaning |
| --- | --- | --- |
| `well_functioning_government` | Well-Functioning Government | Public service delivery, political stability, rule of law, courts and administration, elections and transfers of power, and trust in governing institutions. |
| `sound_business_environment` | Sound Business Environment | Economic conditions and the institutions supporting private enterprise, including regulation, banking, competitiveness, and productive economic activity. |
| `equitable_distribution_of_resources` | Equitable Distribution of Resources | Fairness of access to health care, education, and income, and disputes over how public resources are shared between groups or regions. |
| `acceptance_of_the_rights_of_others` | Acceptance of the Rights of Others | Formal rights and the norms behind them, including treatment of minorities, women, migrants, and marginalised groups, discrimination, group grievance, and social exclusion. |
| `good_relations_with_neighbours` | Good Relations with Neighbours | Relations with other countries and between ethnic, religious, and cultural groups, including diplomacy, cross-border cooperation and trade, and reconciliation. |
| `free_flow_of_information` | Free Flow of Information | Media independence and reach, press freedom and treatment of journalists, censorship, internet and telecommunications access, and disinformation. |
| `high_levels_of_human_capital` | High Levels of Human Capital | Education systems and attainment, health and life expectancy, youth employment and training, and research capacity. |
| `low_levels_of_corruption` | Low Levels of Corruption | Bribery, embezzlement and public-sector theft, procurement fraud, entrenched or factionalised elites, and anti-corruption investigations and prosecutions. |

The Positive Peace Index indicators beneath these pillars measure countries over
years rather than classify events, and are not part of this layer. No IEP index
values are reproduced through this API.

### `geospatial_v1` — Living Signals geospatial imagery suitability

This focused operational layer records whether a Signal describes already
observed physical effects that may support a useful 10 m/pixel optical
satellite chip. It is independent of the rubric. Signals about ceremonies,
inaugurations, commissionings, openings, financing approvals, plans, appeals,
or statements about physical things receive an empty `codes` array.

| API code | Canonical value | Meaning |
| --- | --- | --- |
| `visible_landscape_change` | Visible landscape change | The event has already altered the land surface with a footprint plausibly visible in 10 m/pixel optical imagery, such as flooding extent, a burn scar, a landslide, large-scale destruction, active land clearing, or a mass displacement site. |
| `wide_area` | Wide area | The physically affected area spans roughly a hectare or more, such as multiple city blocks, a neighborhood, or a stretch of river or forest, rather than one building, compound, or vehicle. |
| `persistent_trace` | Persistent trace | The visible trace will remain on the ground for two weeks or more, rather than an ephemeral effect such as smoke, crowds, or a temporary closure. |
