Back to challenges

Tech.Earth for Developers

A versioned REST API and an MCP server for licensed partners to query UK environmental challenge intelligence - read-only, evidence-cited.

Getting access

Access is by API key, issued to named partners - there is no self-service signup in this version. Contact your Tech.Earth account manager to request a key; they will confirm which scopes and rate limit tier are appropriate for your use case and share the key with you through a secure channel.

Every request - REST or MCP - must include Authorization: Bearer <your key>. Keys are scoped (below) and rate-limited per key, per tier.

Scopes & rate limits

Every key is issued with one or more scopes. Ask your account manager for the scopes your integration needs - a key with only challenges:read cannot call cluster or brief endpoints, for example.

Scopes govern what you can call. Which records you see is a separate, non-negotiable boundary: endpoints that return your own submissions, ownership claims or research runs are always filtered to your tenant, whatever scopes your key holds. Another tenant's record returns not-found, never a permission error.

ScopeGates
regions:read/v1/regions, the list_regions MCP tool
challenges:read/v1/challenges, /v1/challenges/[id], /v1/organisations, /v1/organisations/[id], and the challenge.* MCP tools
coverage:read/v1/coverage, the get_coverage_report MCP tool
clusters:read/v1/clusters, the cluster.get MCP tool
briefs:read/v1/challenges/[id]/brief (cache-read only), the get_cached_brief and evidence.get MCP tools
research:initiate/v1/research-runs (GET and POST), /v1/research-runs/[id], the research.start and research.status MCP tools
submissions:create/v1/submissions (GET and POST), /v1/submissions/[id], the submission.create MCP tool
ownership:request/v1/ownership-claims (GET and POST), the ownership.request MCP tool
tenant-data:readDefined but not yet gating any endpoint - reserved for a future tenant-scoped read surface. A key carrying it gains nothing today.
reviews:decideDefined but not yet gating any endpoint - reserved for a future delegated review role. A key carrying it gains nothing today.

Rate limit tiers: standard (60 requests/min - the default), elevated (300/min), and internal_partner (1000/min). Limits are per key, shared across REST and MCP calls made with the same key. A request over the limit returns 429.

REST API - /v1

Base URL: https://tech.earth/api/v1. Everything returns JSON. This is a versioned, stable contract - once you integrate against it, the response shape is frozen (additive changes only).

Most endpoints are read-only GETs. Three accept a POST - research runs, submissions and ownership claims - and each of those creates a reviewable draft or request, never a published record or an approved claim. Nothing you can call here publishes a Challenge or grants ownership; a human reviewer does that, always.

Send an Idempotency-Key header with any POST. It is optional but strongly recommended: retrying with the same key returns the original result instead of creating a second record. Reusing a key with a different body is rejected with 409, as is retrying while the first request is still in flight.

EndpointParamsReturns
/regions-The country + 12 UK regions: code, name, level, countryCode, countryName
/challenges
  • type?
  • kind? (challenge | cluster)
  • updatedSince? (ISO 8601)
  • search?
  • limit? (default 50, max 200)
  • offset? (default 0)
Published Challenges and Clusters, ranked by impact score, plus a pagination object ({ total, limit, offset, hasMore }). Omit kind for the merged, cluster-deduplicated list; pass kind=challenge to list every published Challenge individually (including ones inside a cluster), or kind=cluster for clusters alone. See "Challenges vs. clusters" below for the response shape.
/challenges/{id}-A single published Challenge or Cluster by id. A Challenge comes back as the full record - see "The Challenge record" below.
/challenges/{id}/brief
  • lens (required)
The cached brief for a challenge, if one exists. Never generates a new one.
/coverage
  • windowHours? (default 168)
Data coverage by region and dataset
/clusters
  • challengeType?
  • region?
  • limit? (default 50, max 200)
  • offset? (default 0)
Public, aggregate-only demand clusters, each with a demandState of committed / expressed / potential (potential means the shared demand is inferred from comparable evidence, not confirmed by the organisations), plus a pagination object ({ total, limit, offset, hasMore })
/organisations
  • organisationType?
  • q? (name search)
Organisations on the public record. Requires challenges:read.
/organisations/{id}-A single organisation by id
POST /research-runs
  • targets[] (required: sourceRegistryId, url, title, sourceType)
Starts an on-demand research run against sources already registered and licence-approved, attributed to your tenant. 201 with the run. Requires research:initiate.
/research-runs
  • status?
  • triggerType?
Your tenant's own research runs. Another tenant's runs are never listed.
/research-runs/{id}-Status of one run. Returns not-found for another tenant's run, rather than a permission error.
POST /submissions
  • organisationId
  • submitterUserId
  • title
  • societalImpactStatement
  • submitterDeclaration
  • placeScope?
  • populationScope?
  • suggestedWording?
  • evidenceDescription?
Creates a draft Challenge submission. 201 with the submission. submitterUserId must be an institutional user of your tenant AND a verified member of organisationId - otherwise 403. Requires submissions:create.
/submissions-Your tenant's submissions
/submissions/{id}-A single submission by id
POST /ownership-claims
  • organisationId
  • challengeId
  • claimantUserId
  • membershipCheck?
  • scope?
  • startDate?
  • endDate?
Raises a pending ownership claim for review. 201 with the claim - never an approved claim. Requires ownership:request.
/ownership-claims-Ownership claims raised by your tenant, with their review state

lens on the brief endpoint must be one of challenge, ecosystem, venture_brief. The brief endpoint only briefs Challenges (kind: "challenge") - Cluster ids return 404 there.

Paging and incremental sync. Use updatedSince to pull only what has changed rather than re-reading the whole register; on a Challenge it compares against updatedAt, on a Cluster against the date it was last reviewed. Results are ordered by impact score with a stable id tiebreak, so offset paging will not duplicate or skip rows. The merged list (no kind) caps offset + limit at 1000, because it has to read that far into both sources before it can merge them - to page deeper, ask for one kind at a time, which has no such limit.

Every /v1 response carries an x-correlation-id header. Quote it when reporting a problem - it is the same id our request log records for your call.

Example request

$ curl https://tech.earth/api/v1/regions \
    -H "Authorization: Bearer <your key>"

{
  "regions": [
    { "code": "GB", "name": "United Kingdom", "level": "country", "countryCode": "GB", "countryName": "United Kingdom" },
    { "code": "GB-LDN", "name": "London", "level": "region", "countryCode": "GB", "countryName": "United Kingdom" },
    { "code": "GB-SCT", "name": "Scotland", "level": "region", "countryCode": "GB", "countryName": "United Kingdom" }
  ]
}

Errors

Every error response is a structured object, not a bare string:

{
  "error": {
    "category": "validation",
    "code": "validation_failed",
    "message": "limit must be an integer between 1 and 200"
  }
}

Branch on category - it is a closed set: authentication, scope, policy, validation, conflict, rate, review_state, not_found, connector, internal. code is a stable machine-readable reason within a category; message is human-readable and may be reworded, so don't match on it.

Authentication failures are deliberately undifferentiated. An invalid key, a revoked key, an expired key, a suspended key and a key whose tenant is not approved all return the same 401 with the same message - the response never tells you which, so don't try to distinguish them. Retrying will not clear any of them.

StatusWhen
400The request was malformed - an unknown filter value, an out-of-range limit, a bad timestamp, or a missing required field on a POST
401Bearer token missing or malformed, or the key is invalid, revoked, expired, suspended, or its tenant is not approved
403The key is valid but lacks the required scope, or the request breaks a policy rule (e.g. the submitter is not a verified member of the organisation)
404The requested resource doesn't exist, isn't published, or belongs to another tenant - these are deliberately indistinguishable
409An Idempotency-Key was reused with a different body, a request with that key is still in flight, or the record is in a review state that forbids this change
429The key's rate limit tier has been exceeded - retry after a short backoff
500Unexpected server error - safe to retry; contact your account manager if it persists

Challenges vs. clusters

/v1/challenges and /v1/challenges/{id} return a mix of two kinds, told apart by kind: a standalone "challenge" (a single declared Challenge - status, origin, and a full impact sub-score breakdown) or a "cluster" (several related Challenges grouped together - a shared pattern summary and an organisation-type breakdown instead of per-Challenge fields). Every entry - either kind - carries regions[], a list of { code, name } pairs, since a Challenge can span multiple regions. Don't hardcode region codes; call GET /v1/regions to discover them.

{
  "challenges": [
    {
      "kind": "challenge",
      "id": "3f9a1c2e-...",
      "title": "Elevated PM2.5 across East London",
      "challengeType": "air_quality",
      "impactScore": 72,
      "impactConfidence": "High",
      "regions": [{ "code": "GB-LDN", "name": "London" }],
      "evidenceCount": 14,
      "status": "active",
      "origin": "org_report",
      "createdAt": "2026-01-15T09:12:00.000Z",
      "updatedAt": "2026-03-10T16:04:00.000Z"
    },
    {
      "kind": "cluster",
      "id": "9b2e7a10-...",
      "title": "Recurring surface-water flooding, North West",
      "challengeType": "floods",
      "impactScore": 58,
      "regions": [{ "code": "GB-NW", "name": "North West" }],
      "organisationCount": 5,
      "isStrategic": true
    }
  ],
  "pagination": { "total": 214, "limit": 50, "offset": 0, "hasMore": true }
}

The Challenge record

GET /v1/challenges/{id} returns everything a list row carries, plus the blocks below. All of it is drawn from the exact version a reviewer approved for publication - never a draft, and never a newer revision still under review.

FieldWhat it is
versionSnapshotThe approved version's own recorded content. Its shape varies with how the Challenge was authored, so read it defensively rather than assuming fixed keys.
publishedAtWhen this version was approved for publication - distinct from createdAt and updatedAt.
domains[] / domainSourceThe research domain(s) this Challenge sits in, from a closed set of six: climate resilience, energy & buildings, water, transport & accessibility, public health & wellbeing, nature & environmental quality. Air quality, flood risk and heat are topics under one of these, never domains of their own - read challengeType for the topic. domainSource is "research_run" when the domain was declared by the research that produced this version, or "undeclared" with an empty domains[] when the Challenge came from an organisation submission or was created manually. We never infer a domain from a Challenge's own wording, so an empty list means nobody declared one - not that it has none.
organisations[]Organisations with an approved, evidenced relationship to this Challenge, each with a role, dates and confidence.
owners[] / ownershipDisclaimerOrganisations with an approved ownership claim. Ownership means accountability for the Challenge record, not responsibility for causing the problem - the disclaimer text says so, and must be shown alongside owners.
clusters[]Clusters this Challenge belongs to, each already past its own approval and anonymity gates.
dimensions[] / gaps[]The six explainable dimensions (scale, severity, urgency, persistence, geographic coverage, shared demand potential) with band, rationale and confidence. Anything not yet assessed is reported in gaps[] rather than silently omitted. There is no single composite challenge score.
solutionCategories[]Approved categories of intervention this Challenge calls for.
sourceLinks[]Upstream source URLs whose licence permits redistribution. Sources we hold under a restricted or unresolved licence are omitted, so this is a subset of the evidence behind the Challenge, not all of it.
evidenceProfileWhat kind of evidence stands behind this Challenge, which origin does not tell you. Counts of source formats (document/dataset/feed/api), publisher tiers (national/regional/authority/methodology), reviewer-set authority levels (High/Medium/Low, or "unrated" where the source carries no registered authority), and claim types (fact/metric/assertion/quote). observationBasis is the one to read first: observed vs projected vs unknown. "unknown" means the source never stated which - it is never folded into "observed", so do not treat it as measured fact. Counts cover all usable evidence; publishers[] names only the sources we may redistribute, so it is deliberately shorter than sourceCount.
evidence / contradictions[]Evidence count and date range, plus any open contradictions between sources. Contradictions are surfaced, never resolved silently in our favour.
mode / modeReasons[]Whether this record is backed by live, hybrid or mock data, and why. Never present a mock or hybrid record as verified live data.

Not returned, by design: internal working notes, named individuals, and any AI-suggested relationship a reviewer has not confirmed.

MCP server (for AI agents)

Claude, Cursor, ChatGPT, or your own agent can call this same data as MCP tools over Streamable HTTP, using the same API key and scopes as the REST API - one key works across both surfaces, with one shared rate-limit budget.

{
  "mcpServers": {
    "tech-earth": {
      "url": "https://tech.earth/api/mcp",
      "headers": { "Authorization": "Bearer <your key>" }
    }
  }
}

Tools are filtered by your key's scopes: a tool you lack the scope for is not listed and cannot be called. The tools read the same data as the REST endpoints, through the same gates - an agent cannot reach anything a REST caller with the same key could not.

Read tools

ToolArgumentsWhat it returns
list_regions-The country + 12 region codes, names and levels
challenge.searchtype?, kind?, updatedSince?, search?, limit?, offset?Published challenges and clusters plus a pagination object - the same read path as GET /v1/challenges
challenge.getchallengeIdOne published challenge or cluster, as the full record described above
challenge.comparechallengeIdA, challengeIdBBoth entries and a field-by-field diff. Compares ranked fields only, not narrative blocks.
cluster.getchallengeType?, region?, limit?, offset?Aggregate-only demand clusters plus pagination. "potential" demandState is inferred from comparable evidence, not confirmed by the organisations.
evidence.getclaimIds[]Publicly citable evidence claims. Ids that do not qualify are omitted, never flagged as denied - so a short result is normal.
get_coverage_reportwindowHours?Data source coverage by region and dataset
get_cached_briefchallengeId, lensA cached brief for a published challenge, or null. Never triggers generation, so null means "not cached", not "not applicable".
research.statusresearchRunIdStatus of one of your tenant's research runs

Write tools

Each of these creates something a human then reviews. None of them publishes a Challenge, approves a claim, or changes a public record - there is no tool on this server that can.

ToolArgumentsWhat it creates
research.starttargets[], reasonAn on-demand research run against already-registered, licence-approved sources. reason is required and recorded for audit - the REST equivalent does not take one.
submission.createorganisationId, submitterUserId, title, societalImpactStatement, submitterDeclaration, placeScope?, populationScope?, suggestedWording?, evidenceDescription?A draft Challenge submission awaiting review
ownership.requestorganisationId, challengeId, claimantUserIdA pending ownership claim awaiting review

A failed tool call returns the error as text with an error flag, rather than the JSON envelope the REST API uses. Scope denials and rate limits surface the same way - check the message, not a status code.

Questions? See About for how Tech.Earth's data is sourced and scored, or contact your account manager.