Wildvine BDS API Reference

Endpoint reference for the Biodiversity Dynamics Simulation (BDS) module + related v3 endpoints used by the Explore page.

Base URL: https://api.wildvine.kotaksakti.com
Auth: all endpoints sit behind Cognito (same as the main app). Hit them from the FE with the user's bearer token. Hectare options: 2 (Pasoh, location_id=1) or 50 (the other plot, location_id=2).

The BDS module has two harvest-calculation modes. Pick the one you're building:

Auto →

Auto Calculate Tree Selection — Heavy / Medium / Light regimes computed for you in one call, each with residual stand, logging damage, and a 30-year projection.

Manual →

Trend → BDS → Manual — the page-by-page flow where the user hand-picks trees from a map/list, then saves & compares selections as projects.

This page documents the shared setup endpoints that both modes (and the Explore page) depend on.

Quick reference — shared endpoints

GET/v3/bds/compartments

Static list of valid compartment sizes for the Hectare dropdown.

Parameters

None.

Example request

curl "https://api.wildvine.kotaksakti.com/v3/bds/compartments"

Example response

{
  "data": [
    { "compartment_hectare": 2,  "location_id": 1 },
    { "compartment_hectare": 50, "location_id": 2 }
  ]
}

GET/v3/bds/years

Available census years for a given compartment. Use this to populate the Year dropdown so it never offers a year with no data.

Parameters

NameTypeRequiredDescription
compartment_hectareintegerrequired2 or 50

Example request

curl "https://api.wildvine.kotaksakti.com/v3/bds/years?compartment_hectare=50"

Example response

{
  "data": [1986, 1990, 1995, 2000, 2005, 2011],
  "compartment_hectare": 50
}
50ha has census data through 2011; 2ha through 2021. The UI's 2025 mock isn't backed by real data — restrict the dropdown to what this endpoint returns.

Error response

{ "detail": "compartment_hectare=99 is not valid. Valid options: [2, 50]" }

GET/v3/bds/simulation

Full data for BDS Page 1 (Pre-Felling): tree-density metrics with comparison percentages, distribution charts, and Candidate Trees summary + per-species breakdown. This is the shared entry screen before the user branches into Auto or Manual.

Parameters

NameTypeRequiredDescription
yearintegerrequiredCensus year, e.g. 2021
compartment_hectareintegerrequired2 or 50

Example request

curl "https://api.wildvine.kotaksakti.com/v3/bds/simulation?year=2021&compartment_hectare=50"

Response structure

{
  "pre_felling": {
    "tree_density":               { "value": integer, "change_pct": integer },
    "tree_density_per_ha":        { "value": float,   "change_pct": integer },
    "volume_m3_per_ha":           { "value": float,   "change_pct": integer },
    "basal_area_m2_per_ha":       { "value": float,   "change_pct": integer },
    "total_biomass_t":             { "value": float,   "change_pct": integer },
    "total_carbon_t":              { "value": float,   "change_pct": integer },
    "dipterocarp_density_per_ha":  { "value": float,   "change_pct": integer }
  },
  "distributions": {
    "species_group_density": [
      { "group": "Non-dipterocarp", "value": integer }
    ],
    "dbh_size_class_density": [
      { "class": "30-45cm" | "45-60cm" | "60-75cm" | "75-90cm" | ">90cm", "value": integer }
    ]
  },
  "candidate_trees": {
    "summary": {
      "tree_density":        integer,
      "tree_density_per_ha": float,
      "volume_m3":           float,
      "basal_area_m2":       float,
      "total_biomass_t":     float,
      "total_carbon_t":      float,
      "price_rm":            float
    },
    "species_breakdown": [
      {
        "species_group":           "Dipterocarp" | "Non-dipterocarp" | "Chengal",
        "cutting_limit_cm":        float,
        "tree_density":            integer,
        "tree_density_per_ha":     float,
        "volume_m3":               float,
        "volume_m3_per_ha":        float,
        "basal_area_m2":           float,
        "basal_area_m2_per_ha":    float,
        "total_biomass_t":         float,
        "total_biomass_t_per_ha":  float,
        "total_carbon_t":          float,
        "total_carbon_t_per_ha":   float
      }
    ]
  },
  "meta": {
    "compartment_hectare": integer,
    "year":                integer,
    "updated_at":          "2026-06-04T08:00:00Z"
  }
}

Example response (abbreviated, 50ha 2021)

{
  "pre_felling": {
    "tree_density":               { "value": 3750,   "change_pct": 91 },
    "tree_density_per_ha":        { "value": 75.0,   "change_pct": 91 },
    "volume_m3_per_ha":           { "value": 138.52, "change_pct": 86 },
    "basal_area_m2_per_ha":       { "value": 15.26,  "change_pct": 49 },
    "total_biomass_t":             { "value": 267.46, "change_pct": 63 },
    "total_carbon_t":              { "value": 125.71, "change_pct": 63 },
    "dipterocarp_density_per_ha":  { "value": 21.1,   "change_pct": 28 }
  },
  "distributions": {
    "species_group_density": [
      { "group": "Non-dipterocarp", "value": 2631 },
      { "group": "Dipterocarp",     "value": 983 },
      { "group": "Chengal",         "value": 72 },
      { "group": "Pioneer",         "value": 64 }
    ],
    "dbh_size_class_density": [
      { "class": "30-45cm", "value": 2307 },
      { "class": "45-60cm", "value": 794 },
      { "class": "60-75cm", "value": 320 },
      { "class": "75-90cm", "value": 167 },
      { "class": ">90cm",   "value": 162 }
    ]
  },
  "candidate_trees": {
    "summary": {
      "tree_density": 700, "tree_density_per_ha": 14.0,
      "volume_m3": 4255.64, "basal_area_m2": 366.22,
      "total_biomass_t": 6323.56, "total_carbon_t": 2972.07,
      "price_rm": 365386.78
    },
    "species_breakdown": [
      { "species_group": "Dipterocarp",     "cutting_limit_cm": 65, "tree_density": 262, "...": "..." },
      { "species_group": "Non-dipterocarp", "cutting_limit_cm": 55, "tree_density": 396, "...": "..." },
      { "species_group": "Chengal",         "cutting_limit_cm": 70, "tree_density": 42,  "...": "..." }
    ]
  },
  "meta": { "compartment_hectare": 50, "year": 2021, "updated_at": "2026-06-04T08:00:00Z" }
}
change_pct = percentage of the PF metric (DBH≥30cm) relative to all trees in the dataset. E.g. tree_density.change_pct = 91 means DBH≥30 trees are 91% of all trees.

GET/v3/species-options

Options for the Scientific Name dropdown on the Trend / Overview pages, scoped to the selected compartment + year. Returns only species that actually have records in that selection, so the user can't pick a name that yields an empty result. Each option carries its species_group, so the FE can auto-fill the Species Group filter when a name is chosen.

Why this exists. The global species list has ~600+ entries; most aren't present in any one compartment/year. Sourcing the dropdown from this endpoint kills the "No data available" picks and lets you cascade the Species Group filter from the chosen name (use the species_group field on the selected option).

Parameters

NameTypeRequiredDescription
location_idsinteger[]optional1 = 2ha plot, 2 = 50ha plot. Comma-separated or repeated.
yearsinteger[]optionale.g. ?years=2011. Comma-separated or repeated.

Omitting both returns every species across the whole dataset. In practice pass the compartment's location_id and the selected year to scope it.

Example request

curl "https://api.wildvine.kotaksakti.com/v3/species-options?location_ids=2&years=2011"

Example response

{
  "data": [
    {
      "species_id":      "ACTIM1",
      "scientific_name": "Actinodaphne macrophylla",
      "species_group":   "Non-dipterocarp",
      "tree_count":      4
    },
    {
      "species_id":      "CANAPA",
      "scientific_name": "Canarium patentinervium",
      "species_group":   "Non-dipterocarp",
      "tree_count":      14
    }
    // ... sorted alphabetically by scientific_name
  ],
  "params": { "location_ids": "2", "years": "2011" }
}
Empty selection → []. A compartment/year with no rows returns { "data": [], "params": {...} }.
Group caveat: species_group is the species' visual group (group_new). A few species carry values outside the 5 dropdown options (e.g. Non-commercial, Partially commercial) — decide on the FE how to map those before cascading.

GET/v3/filter-options

Faceted filter values for the Trend / Explore filter bar — returns only the options that still have data given the current selection, so the FE can collapse each dropdown to what's actually available. Scope is location_ids + years; pass the user's current picks to narrow the rest.

How it narrows. Each dimension is computed with the other active filters applied but not its own (standard faceting), so a dropdown always still lists every value reachable given the rest of the selection — you can change a pick without it collapsing to one option. Example: choose a Scientific Name and Species Group / DBH Size Class / Protected Tree collapse to exactly what that species has.

Parameters

NameTypeRequiredDescription
location_idsinteger[]optionalCompartment scope. 1 = 2ha, 2 = 50ha.
yearsinteger[]optionalCensus year scope, e.g. 2011.
scientific_namestring[]optionalCurrent Scientific Name pick(s).
species_idsstring[]optionalCurrent species code pick(s).
species_groupsstring[]optionalCurrent Species Group pick(s). All/empty = no filter.
dbh_size_classesstring[]optionalCurrent DBH Size Class pick(s). All/empty = no filter.
protected_treestring[]optionalCurrent Protected Tree pick(s) (IUCN categories). All/empty = no filter.

Example request

curl "https://api.wildvine.kotaksakti.com/v3/filter-options?location_ids=2&years=2011&scientific_name=Shorea%20pauciflora"

Example response

{
  "data": {
    "scientific_names": [
      { "species_id": "...", "scientific_name": "...", "species_group": "...", "tree_count": integer }
    ],
    "species_groups":   ["Dipterocarp"],
    "dbh_size_classes": ["0_15", "15_30", "30_45", "45_60", "60_75", "75_90"],
    "protected_tree":   ["Least Concern (LC)"],          // IUCN categories present
    "protected_binary": { "has_protected": true, "has_unprotected": false }
  },
  "params": { "location_ids": "2", "years": "2011", "scientific_name": "Shorea pauciflora", ... }
}
dbh_size_classes come back in canonical order (0_15 … 90). protected_tree lists the IUCN category labels present; protected_binary tells you whether protected and/or unprotected (no category) trees exist, in case the dropdown also offers a plain protected/unprotected toggle. With no selection, every facet returns the full set available for that compartment + year.

GET/v3/flora

Paginated per-tree records for the Explore page. Also the source of points/rows for the Manual map & list. Supports filtering by year, location, species, DBH class, and protection status.

Parameters

NameTypeRequiredDescription
yearsinteger[]optionale.g. ?years=2017&years=2021
location_idsinteger[]optional1 = 2ha plot, 2 = 50ha plot
species_idsstring[]optionalSpecies codes from species.csv
species_groupsstring[]optionalDipterocarp, Non-dipterocarp, Chengal, Pioneer
dbh_size_classesstring[]optional0_15, 15_30, 30_45, 45_60, 60_75, 75_90, 90
protected_treestring[]optionaltrue / false (or 1 / 0). Binary only — no IUCN categories.
scientific_namestring[]optionalPartial match on scientific name
pageintegeroptional1-based page number (default 1)
page_sizeintegeroptionalDefault 10, max 5000

Example request

curl "https://api.wildvine.kotaksakti.com/v3/flora?years=2021&location_ids=2&dbh_size_classes=30_45&protected_tree=true&page_size=20"

Response structure

{
  "data": [
    {
      "tag":             string,
      "species":         string,           // species code
      "species_name":    string,
      "species_group":   string,
      "redlist":         "0" | "1",        // 1 = protected
      "timber_group":    string,
      "quad":            string,
      "xco":             float, "yco": float,
      "longitude":       float, "latitude": float,
      "year":            integer,
      "data":            float,            // DBH in cm
      "dbh_size_class":  string,
      "extrapolated":    bool,
      "basal_area":      float,
      "height":          float,
      "volume":          float,
      "agb":             float,
      "bgb":             float,
      "biomass":         float,
      "carbon_stock":    float,
      "price_rm":        float,
      "location_id":     integer,

      // per-hectare equivalents (value / the tree's plot area). Sum across a
      // plot = the totals-row per-ha. biomass/carbon are in t/ha and tC/ha.
      "area_ha":              float,
      "basal_area_m2_per_ha": float,
      "volume_m3_per_ha":     float,
      "biomass_t_per_ha":     float,
      "carbon_tC_per_ha":     float
    }
  ],
  "area_ha": float,            // total area (ha) of the plots in this result
  "pagination": {
    "page":         integer,
    "page_size":    integer,
    "total":        integer,
    "total_pages":  integer,
    "has_next":     bool,
    "has_prev":     bool
  },
  "params": {
    "years":            "2021",
    "species_groups":   null,
    "location_ids":     "2",
    "species_ids":      null,
    "dbh_size_classes": "30_45",
    "protected_tree":   "true",
    "scientific_name":  null
  }
}
protected_tree: backend only stores binary 0/1 (not IUCN categories like CR/EN/VU). Sending true returns trees with redlist=1; false returns redlist=0.

Notes

Last updated 2026-06-09 · Hosted on Cloudflare Pages