Viable logo

API access

Run emissions pathway projections from your own tools. This walkthrough covers authentication, a minimal request payload, standard vs extended responses, and scenario analysis — with copyable and downloadable examples.

API keys are issued to approved customers and partners. This site never embeds a key — use placeholders like YOUR_API_KEY until yours is issued.

1. Authenticate

Protected endpoints expect an API key (or a Cognito JWT for app sessions). Prefer an environment variable — never commit keys to source control or notebooks shared publicly.

Header options

  • Authorization: ApiKey YOUR_API_KEY
  • X-API-Key: YOUR_API_KEY
  • or Authorization: Bearer <JWT> / JWT <token>

Base URL: https://app.viablepathway.net

Request an API key to get access

2. Build the payload

Send a complete organisation payload as JSON with datastructureversion: 2. Organisation-wide profile and targets sit at the top in meta and target; activity data and emission factors live inside each org unit’s activitydata map. The mini example below uses one org unit, three activities, and two internal actions — enough to exercise BAU, EXT, and INT pathways without a full inventory dump. Stamp standard names on each row so external trends can attach.

Organisation and org units

Split your payload into two levels. At the organisation level you describe the company as a whole — name, reporting currency, growth assumptions, projection years, and emissions targets. Under orgunits you place one or more org units: the parts of the business where inventory actually lives.

Use as many org units as match your reporting lines. A bakery group might split into head office, logistics, and retail stores; a smaller operator can put everything in a single org unit if that is simpler. Each org unit carries its own geography, historic emissions, internal actions, and activitydata — the activity quantities and emission factors that roll up to organisation-level pathways.

Example: Sunrise Bakery Group

The bakery describes the whole group once in meta and sets its near-term target at the same organisation level. Inventory rows sit inside each org unit’s activitydata map — not in meta.

Organisation level — meta + target

{
  "organisationid": "sunrise_bakery",
  "datastructureversion": 2,
  "meta": {
    "name": "Sunrise Bakery Group",
    "currency": "aud",
    "hqcountry": "australia",
    "industrytype": "food_manufacturing",
    "employees": 450,
    "revenue": 85,
    "desiredgrowth": 3,
    "projectionstartyear": 2025,
    "projectionendyear": 2050,
    "electricityapproach": "location"
  },
  "target": {
    "neartermsbtitarget": "1.5",
    "neartermsbtitargetyear": 2030
  },
  "orgunits": { … }
}

Org unit level — activitydata (excerpt)

"orgunits": {
  "orgunit_retail": {
    "name": "Retail stores",
    "country": "australia",
    "region": "au-nsw",
    "activitydata": {
      "utilities_electricity_grid@@au-nsw": {
        "activityname": "Store electricity",
        "catalogactivityid": "utilities_electricity_grid",
        "activitydatavalue": 1200000,
        "activityuom": "kWh",
        "emissionfactorlabel": "NSW grid electricity",
        "emissionfactortrendid": "utilities_electricity_grid",
        "emissionfactorvalue": 0.68,
        "emissionfactorunit": "kgCO2e"
      }
    }
  }
}

You can model the whole bakery in one org unit if you do not need a split — the structure is the same; you simply have a single key under orgunits instead of three.

Payload structure

Use datastructureversion: 2. Organisation profile and projection settings live under meta; science-based and custom targets under target. Inventory, geography, and actions live under orgunits.

Activity data (v2)

Each key is a unique instance id (often catalogActivity@@region). Required v2 fields include activityname, catalogactivityid, emissionfactorlabel (display only — not emissionfactorname), and emissionfactortrendid. Use catalog ids as exact standard names so external trends can attach. Stamp datastructureversion: 2 on the root and each org unit.

Internal actions

The object key (e.g. useraction_efficiency_2027) becomes actionid in outputs. actionname is a label only. multiplier is a fractional change (e.g. -0.15 = −15%). Optional balancingactivity shifts quantity to another activity (e.g. petrol → EV).

Market-based Scope 2 (optional)

Set meta.electricityapproach: "market" and add per-org-unit recPurchase with a ramp schedule (entries + pricePerKwh). Not required for the mini example below.

BAU → EXT → INT

BAU grows inventory with meta.desiredgrowth. EXT applies external market trends (e.g. grid decarbonisation) when ids match standard names. INT layers your internalactions on top of EXT. See BAU, EXT, and INT for AD vs AD2.

minimal-payload.json JSON
{
  "organisationid": "DEMO_CO",
  "datastructureversion": 2,
  "meta": {
    "name": "Demo Company",
    "currency": "aud",
    "desiredgrowth": 2,
    "projectionstartyear": 2025,
    "projectionendyear": 2050,
    "electricityapproach": "location"
  },
  "target": {
    "neartermsbtitarget": "1.5",
    "neartermsbtitargetyear": 2030
  },
  "orgunits": {
    "orgunit_sydney": {
      "name": "Sydney Office",
      "orgunitid": "orgunit_sydney",
      "country": "australia",
      "region": "au-nsw",
      "currency": "aud",
      "employees": 120,
      "datastructureversion": 2,
      "activitydata": {
        "utilities_electricity_grid@@au-nsw": {
          "activityname": "Grid electricity — NSW",
          "catalogactivityid": "utilities_electricity_grid",
          "category": "utilities",
          "region": "au-nsw",
          "activitydatavalue": 500000,
          "activityuom": "kWh",
          "ghgpscope": 2,
          "ghgpscopecategory": "2-1 Indirect Emissions from Purchased Electricity",
          "emissionfactorlabel": "NSW grid electricity (location)",
          "emissionfactortrendid": "utilities_electricity_grid",
          "emissionfactorvalue": 0.68,
          "emissionfactorvalue_location": 0.68,
          "emissionfactorunit": "kgCO2e",
          "emissionfactorsource": "dcceew-aus"
        },
        "companyvehicle_car_petrol@@au-nsw": {
          "activityname": "Fleet — petrol cars",
          "catalogactivityid": "companyvehicle_car_petrol",
          "category": "companyvehicle",
          "region": "au-nsw",
          "activitydatavalue": 250000,
          "activityuom": "km",
          "ghgpscope": 1,
          "ghgpscopecategory": "1-2 Direct Emissions from Mobile Combustion",
          "emissionfactorlabel": "Petrol passenger cars",
          "emissionfactortrendid": "companyvehicle_car_petrol",
          "emissionfactorvalue": 0.192,
          "emissionfactorunit": "kgCO2e",
          "emissionfactorsource": "desnz-uk"
        },
        "companyvehicle_car_bev@@au-nsw": {
          "activityname": "Fleet — battery electric cars",
          "catalogactivityid": "companyvehicle_car_bev",
          "category": "companyvehicle",
          "region": "au-nsw",
          "activitydatavalue": 10000,
          "activityuom": "km",
          "ghgpscope": 2,
          "ghgpscopecategory": "2-1 Indirect Emissions from Purchased Electricity",
          "emissionfactorlabel": "Battery electric cars",
          "emissionfactortrendid": "companyvehicle_car_bev",
          "emissionfactorvalue": 0.05,
          "emissionfactorunit": "kgCO2e",
          "emissionfactorsource": "desnz-uk"
        }
      },
      "internalactions": {
        "useraction_efficiency_2027": {
          "actionname": "Office energy efficiency",
          "actiontype": "Energy Efficiency",
          "activity": "utilities_electricity_grid@@au-nsw",
          "startyear": 2027,
          "multiplier": -0.15,
          "absolutechange": 0,
          "balancingactivity": "",
          "balancingmultiplier": 0,
          "capitalcost": 80000,
          "capitalcostcalculationmethod": "absolute",
          "description": "Lighting and HVAC upgrades reducing grid electricity use by 15% from 2027."
        },
        "useraction_fleet_ev_2028": {
          "actionname": "Switch petrol fleet to EV",
          "actiontype": "Company Vehicles",
          "activity": "companyvehicle_car_petrol@@au-nsw",
          "startyear": 2028,
          "multiplier": -0.5,
          "absolutechange": 0,
          "balancingactivity": "companyvehicle_car_bev@@au-nsw",
          "balancingmultiplier": 1,
          "capitalcost": 150000,
          "capitalcostcalculationmethod": "absolute",
          "description": "Replace half of petrol car kilometres with battery electric from 2028."
        }
      },
      "historicemissionsyears": [
        2023,
        2024
      ],
      "historicemissions": {
        "1": [
          48.2,
          46.1
        ],
        "2": {
          "location": [
            340,
            325.5
          ],
          "market": [
            340,
            325.5
          ]
        },
        "3": [
          12,
          11.5
        ]
      }
    }
  }
}

3. Standard names and external trends

EXT is not automatic for every inventory row. The engine applies library trends only when payload ids match our catalog exactly.

The EXT pathway accounts for external trends that are expected to influence an organisation’s operations and emissions profile over time — change outside the company’s control. There are dozens of these trends, and they provide “free” decarbonisation that lowers the effective baseline from which internal actions are planned. Methodology: External trends pathway.

There are two types of trends:

  • Emission factor trends are expected changes in emission factors. The best example is decarbonisation of the electricity grid: users of electricity benefit from lower emissions even if they consume the same power every year. Browse public examples on Emission trends.
  • Activity data trends are expected shifts in use patterns in society. The best example is employees adopting electric vehicles in place of ICE vehicles, which reduces commuting (and similar fleet) activity on petrol or diesel rows without an internal action.

Standard names are the match keys

Fetch the catalog with GET /api/data/standardnames. Use the snake_case ids in activity and emissionfactorname (for example utilities_electricity_grid), not display labels such as “Grid electricity”, and not the instance id on the payload map key (that may include @@region). Matching is exact.

The engine only applies a library trend when the payload field equals that catalog id. Geography still comes from the org unit’s country and region (and optional per-row region).

emissionfactortrendid Unlock emission factor trends. Set it to the standard name that matches emissionFactorsEXTtrends.emissionfactorname — usually the same id as catalogactivityid (e.g. utilities_electricity_grid). A custom display label in emissionfactorlabel does not count. An empty string opts out of EF trends for that row.
catalogactivityid Unlock activity data trends. Set it to the standard name that matches activityDataEXTtrends.activity (e.g. companyvehicle_car_petrol or commuting_car_petrol). activityname is display-only and is not used for matching.

Worked example (from the mini payload)

Grid electricity uses catalogactivityid and emissionfactortrendid both set to utilities_electricity_grid. That is why EXT can apply grid decarbonisation to kWh even when BAU holds quantity on the growth path. Petrol and BEV fleet rows use companyvehicle_car_petrol and companyvehicle_car_bev the same way, so vehicle-related activity and factor trends can attach.

If you invent ids such as electricity or NSW_grid, calculate still runs, but those rows stay on a flat factor / untrended activity path — EXT looks like BAU for that row.

Inspect the library (optional ?country= and region= filters): GET /api/data/emissionFactorsEXTtrends and GET /api/data/activityDataEXTtrends.

4. Internal actions

Model the decarbonisation measures your organisation can actually implement — efficiency, fuel switching, fleet electrification — inside each org unit’s internalactions map.

internalactions are the decarbonisation levers your organisation can control. They sit inside each org unit and layer on top of the EXT pathway to produce INT — see BAU, EXT, and INT. Each action points at an existing activity row, applies a fractional multiplier from a startyear, and can optionally shift quantity to a balancingactivity.

Balancing activities

When an action replaces one fuel or technology with another — petrol to EV, gas ovens to electric, diesel vans to battery electric — set balancingactivity to the target row’s flattened instance id and balancingmultiplier to how much quantity moves across (often 1 when units match). The engine does not create that row for you: add it yourself in activitydata with activitydatavalue: 0 but full emission-factor fields, a matching catalogactivityid, and an operationalcostperunit for the price you pay per unit of that activity.

When the balancing row uses a different activityuom than the primary row, add balancingunitconversion on the action — the factor that converts removed primary units into balancing units. For example, electrifying equipment billed in GJ but balanced to grid kWh might use 0.0036 (1 kWh = 0.0036 GJ of energy content).

Operational cost, capex, and opex

operationalcostperunit on each activitydata row is what you actually pay per unit of that activity — $/km for diesel vans, $/kWh for grid electricity, $/GJ for gas. The engine uses these rates (not just catalog defaults) to work out running-cost savings when an action shifts quantity between rows. Set it on both the primary row and any balancing row you add.

capitalcost on the action is the upfront installation or purchase cost — new ovens, EVs, or LED fittings. opex is an optional fixed annual fee for running the action itself (for example a maintenance contract). None of the bakery examples below need opex: the bakery is not paying a third party to operate the ovens or vans.

Payback comes from the combination of capitalcost and the change in operational spend. When diesel at $1/km shifts to electric at $0.50/km, the per-kilometre saving contributes every year alongside the upfront capex. The same logic applies when gas spend falls but grid electricity rises.

Sunrise Bakery examples

Continuing the Sunrise Bakery split across head office, logistics, and retail. Each example shows the activity rows you need, then the internalactions entry. The same three actions appear on the MAC curve chart in section 8 when you call calculate with extended=true. Costs are illustrative — sized so payback is roughly one year (LED), five years (EV vans), or six years (electric ovens).

Head office · orgunit_hq

Replace old lights with LED

A dedicated lighting row — no balancing activity. One hundred 50 W fittings running 16 hours a day for 350 days a year is 28,000 kWh at operationalcostperunit: 0.25 ($/kWh); the LED retrofit cuts that load by 35% from 2026.

~1 year payback · capex $15,000

Activity row + action JSON
// activitydata (excerpt)
"utilities_electricity_lighting@@au-nsw": {
  "activityname": "Electricity - lighting",
  "catalogactivityid": "utilities_electricity_grid",
  "activitydatavalue": 28000,
  "activityuom": "kWh",
  "operationalcostperunit": 0.25,
  "emissionfactortrendid": "utilities_electricity_grid",
  "emissionfactorvalue": 0.68,
  "emissionfactorlabel": "NSW grid electricity — lighting",
  "ghgpscope": 2,
  "ghgpscopecategory": "2-1 Indirect Emissions from Purchased Electricity"
}

// internalactions
"action_led_lighting_2026": {
  "actionname": "Replace old lights with LED",
  "actiontype": "Energy Efficiency",
  "activity": "utilities_electricity_lighting@@au-nsw",
  "startyear": 2026,
  "multiplier": -0.35,
  "balancingactivity": "",
  "balancingmultiplier": 0,
  "capitalcost": 15000,
  "capitalcostcalculationmethod": "absolute",
  "description": "Replace 100 lights (labour and materials). 100 × 50 W × 16 h/day × 350 days = 28,000 kWh/year."
}

Logistics · orgunit_logistics

Switch delivery vans to electric

Reduce diesel van kilometres and move the same distance to a battery-electric row you add with zero starting quantity. Both rows need full emission-factor detail and an operationalcostperunit — $1/km for diesel, $0.50/km for electric.

~5 year payback · capex $280,000

Activity rows + action JSON
// activitydata (excerpt)
"freightinggoods_road_hgvdiesel@@au-nsw": {
  "activityname": "Delivery vans — diesel",
  "catalogactivityid": "freightinggoods_road_hgvdiesel",
  "activitydatavalue": 1600000,
  "activityuom": "km",
  "operationalcostperunit": 1,
  "emissionfactortrendid": "freightinggoods_road_hgvdiesel",
  "emissionfactorvalue": 0.89,
  "emissionfactorlabel": "HGV diesel — road freight",
  "ghgpscope": 1,
  "ghgpscopecategory": "1-2 Direct Emissions from Mobile Combustion"
},
"companyvehicle_hgv_bev@@au-nsw": {
  "activityname": "Delivery vans — battery electric",
  "catalogactivityid": "companyvehicle_hgv_bev",
  "activitydatavalue": 0,
  "activityuom": "km",
  "operationalcostperunit": 0.5,
  "emissionfactortrendid": "companyvehicle_hgv_bev",
  "emissionfactorvalue": 0.05,
  "emissionfactorlabel": "Battery electric HGV",
  "ghgpscope": 2,
  "ghgpscopecategory": "2-1 Indirect Emissions from Purchased Electricity"
}

// internalactions
"action_ev_vans_2027": {
  "actionname": "Switch delivery vans to electric",
  "actiontype": "Fleet Electrification",
  "activity": "freightinggoods_road_hgvdiesel@@au-nsw",
  "startyear": 2027,
  "multiplier": -1.0,
  "balancingactivity": "companyvehicle_hgv_bev@@au-nsw",
  "balancingmultiplier": 1,
  "capitalcost": 280000,
  "capitalcostcalculationmethod": "absolute",
  "description": "Replace diesel delivery fleet with battery electric vans."
}

Retail · orgunit_retail

Electrify ovens and stoves

Retire store natural gas for cooking (GJ) and balance the load to grid electricity (kWh). The instance id (the key in activitydata) can be specific to your inventory — naturalgas_kitchen_ovens@@au-nsw — while catalogactivityid and emissionfactortrendid must match standard names so trends attach. Because the units differ, set balancingunitconversion on the action.

~6 year payback · capex $240,000

Activity rows + action JSON
// activitydata (excerpt)
"naturalgas_kitchen_ovens@@au-nsw": {
  "activityname": "Store cooking — natural gas ovens",
  "catalogactivityid": "fuelgaseous_stationary_naturalgas(pipeline)metro",
  "activitydatavalue": 648,
  "activityuom": "GJ",
  "operationalcostperunit": 10,
  "emissionfactortrendid": "fuelgaseous_stationary_naturalgas(pipeline)metro",
  "emissionfactorvalue": 0.18,
  "emissionfactorlabel": "Natural gas — kitchen ovens",
  "ghgpscope": 1,
  "ghgpscopecategory": "1-1 Direct Emissions from Stationary Combustion"
},
"electricity_ovens_kitchens@@au-nsw": {
  "activityname": "Store cooking — electric ovens (balancing)",
  "catalogactivityid": "utilities_electricity_grid",
  "activitydatavalue": 0,
  "activityuom": "kWh",
  "operationalcostperunit": 0.25,
  "emissionfactortrendid": "utilities_electricity_grid",
  "emissionfactorvalue": 0.68,
  "emissionfactorlabel": "Grid electricity — kitchen ovens",
  "ghgpscope": 2,
  "ghgpscopecategory": "2-1 Indirect Emissions from Purchased Electricity"
}

// internalactions
"action_electric_ovens_2028": {
  "actionname": "Electrify ovens and stoves",
  "actiontype": "Fuel Switching",
  "activity": "naturalgas_kitchen_ovens@@au-nsw",
  "startyear": 2028,
  "multiplier": -1.0,
  "balancingactivity": "electricity_ovens_kitchens@@au-nsw",
  "balancingmultiplier": 0.85,
  "balancingunitconversion": 0.0036,
  "capitalcost": 240000,
  "capitalcostcalculationmethod": "absolute",
  "description": "Replace gas ovens and stoves with electric equivalents across retail stores."
}

How the pieces fit together

activity Flattened instance id of the row you are reducing — the activitydata map key (can be your own label; does not have to match catalogactivityid)
multiplier Fractional change — -0.35 is −35%, -1.0 removes the row entirely
balancingactivity Target row that gains quantity; add it yourself with activitydatavalue: 0
balancingmultiplier How much quantity moves per unit removed — adjust when efficiency differs between fuels
balancingunitconversion When balancing activityuom differs from the primary row — e.g. 0.0036 for GJ → kWh
operationalcostperunit On each activitydata row — the price you pay per unit ($/km, $/kWh, $/GJ, …)
capitalcost Upfront investment ($) on the action; drives MAC bubble size and payback in extended responses
opex Optional annual fee for operating the action — omit when there is no separate running cost

5. BAU, EXT, and INT

Every calculate response is three stacked pathways, not three alternatives. Read the names this way before mapping fields to charts.

BAU

Business as usual

Inventory quantities grown with meta.desiredgrowth. No market decarbonisation, no company actions. The counterfactual if nothing else changes.

ghgBAUsumperyear, projectedBAUAD

EXT

+ External trends

Market-driven change outside the organisation’s control — grid greening, vehicle electrification, material intensity — applied on top of BAU. Min/max bands are the trend uncertainty range. Trends attach only when payload ids match standard names.

ghgEXTsumperyear, projectedEXTAD2

INT

+ Internal actions

Your internalactions on top of EXT: efficiency, fleet switches, procurement. This is the pathway the organisation can steer.

ghgINTsumperyear, projectedINTAD2

Each layer includes the one before it. INT is not “actions only” — it is BAU + external trends + your actions. Chart labels “+ External Trends” and “+ Internal Actions” match that stacking. More on the methodology: What are different pathways?

AD means activity data

AD is the physical inventory — kWh, km, tonnes — not emissions. GHG series such as projectedEXTGHG are those quantities × emission factors. Chart activity quantities from AD arrays; chart tCO₂e from GHG arrays.

Why the 2 in AD2

A first pass applies each trend or action in isolation. That is not enough when activities interact: electrifying a petrol fleet must raise EV kilometres; several actions can hit the same row; a primary activity that reaches zero should stop transferring quantity to a balancing activity.

The 2 is the second pass. projectedEXTAD2 and projectedINTAD2 replay the pathway with all other activity-data actions, balancing effects, and external trends in the same year-by-year loop — including zero-stops so balancing cannot run on after the primary activity is gone.

That second pass is where the magic happens. Interactions across activities — other actions, balancing transfers, and external trends — are what make this modelling as realistic as it can be. Use the AD2 arrays (and the GHG arrays built from them) for charts and integration, not the first-pass AD series.

6. Call calculate

POST /api/calculate is Calculation API v1 (frozen methodology — existing integrations keep working). Current methodology is POST /api/v2/calculate. Switch by inserting /v2 after /api. New integrations should call v2. Always pass savePayload=false so the call stays compute-only. Organisation input is stored by the Viable Pathway web app, not by partner API keys.

Endpoint (stable v1): https://app.viablepathway.net/api/calculate?savePayload=false

Endpoint (current v2): https://app.viablepathway.net/api/v2/calculate?savePayload=false

curl — calculate (standard) bash
#!/usr/bin/env bash
# Replace YOUR_API_KEY with the key issued after approval.
# savePayload=false keeps API calls compute-only (do not persist org input).

curl -sS -X POST \
  "https://app.viablepathway.net/api/calculate?savePayload=false" \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -d @minimal-payload.json
Python — calculate (standard) python
"""Minimal Viable Pathway calculate example.

Set VIABLE_API_KEY in your environment — never commit the key.
"""

import json
import os
import urllib.request

API_KEY = os.environ["VIABLE_API_KEY"]
BASE = "https://app.viablepathway.net"
URL = f"{BASE}/api/calculate?savePayload=false"

with open("minimal-payload.json", encoding="utf-8") as f:
    payload = json.load(f)

req = urllib.request.Request(
    URL,
    data=json.dumps(payload).encode("utf-8"),
    headers={
        "Content-Type": "application/json",
        "Authorization": f"ApiKey {API_KEY}",
    },
    method="POST",
)

with urllib.request.urlopen(req) as resp:
    result = json.load(resp)

print("projectionyears:", result.get("projectionyears"))
print("BAU pathway (tCO2e):", result.get("ghgBAUsumperyear"))
print("EXT pathway (tCO2e):", result.get("ghgEXTsumperyear"))
print("INT pathway (tCO2e):", result.get("ghgINTsumperyear"))

7. Read the response

The standard response is the shape most integrators need first: organisation-level pathway totals, target pathways, waterfall, contributions, and trimmed orgunitResults.

Standard response map

Default POST /api/calculate returns a compact calculatedData-style object: pathway totals, targets, waterfall, and trimmed per-unit results (no full activity arrays). The charts below use illustrative dummy data in the same shape as the app.

projectionyears X-axis for pathway charts
ghgBAUsumperyear BAU total tCO₂e per year
ghgEXTsumperyear (+ Min/Max) External-trend pathway (± band)
ghgINTsumperyear (+ Min/Max) Internal-actions pathway (± band)
sbtiPathwayValues / targetyears Science-based target line
waterfallMultiYear Bridge from BAU → EXT → INT
internalActionsContributions Abatement by actionid
orgunitResults[] Per-unit GHG sums (trimmed)
transitionPlan / methodology Narrative / methodology blocks
Pathway totals

Historic stacked bars plus BAU / external-trends / internal-actions lines and the SBTi target. Arrays align to projectionyears (target line to targetyears). Download the Chart.js mapping and replace SAMPLE_RESPONSE with your calculate JSON.

Waterfall to 2030

Floating bars from waterfallMultiYear.waterfall2030: base-year emissions, growth, external trends, internal actions, and the SBTi target.

Charts use dummy data through 2040 so the series look like a live calculation. The JSON sample below is shortened for readability.

response-standard.json (illustrative) JSON
{
  "organisationid": "DEMO_CO",
  "name": "Demo Company",
  "neartermsbtitarget": "1.5",
  "neartermsbtitargetyear": 2030,
  "historicemissionsyears": [
    2023,
    2024
  ],
  "historicemissions": {
    "1": [
      48.2,
      46.1
    ],
    "2": [
      340,
      325.5
    ],
    "3": [
      12,
      11.5
    ]
  },
  "projectionyears": [
    2025,
    2026,
    2027,
    2028,
    2029,
    2030
  ],
  "targetyears": [
    2025,
    2026,
    2027,
    2028,
    2029,
    2030
  ],
  "orgUnits": [
    {
      "id": "orgunit_sydney",
      "name": "Sydney Office"
    }
  ],
  "ghgBAUsumperyear": [
    390,
    397.8,
    405.8,
    413.9,
    422.2,
    430.6
  ],
  "ghgEXTsumperyear": [
    390,
    385,
    372,
    358,
    345,
    332
  ],
  "ghgEXTsumperyearMin": [
    390,
    380,
    365,
    348,
    332,
    318
  ],
  "ghgEXTsumperyearMax": [
    390,
    390,
    380,
    368,
    358,
    346
  ],
  "ghgINTsumperyear": [
    390,
    385,
    340,
    280,
    265,
    250
  ],
  "ghgINTsumperyearMin": [
    390,
    380,
    330,
    268,
    250,
    235
  ],
  "ghgINTsumperyearMax": [
    390,
    390,
    350,
    292,
    280,
    265
  ],
  "sbtiPathwayValues": [
    390,
    365,
    340,
    315,
    290,
    265
  ],
  "userDefinedPathwayValues": null,
  "waterfallMultiYear": {
    "waterfall2030": {
      "projectionstartyear": 2025,
      "projectionstartyearemissions": 390,
      "totalGrowthContribution": 40.6,
      "totalEXTContribution": -98.6,
      "totalMeasuresContribution": -82,
      "targetvalue_sbti": 265,
      "targetvalue_custom": null,
      "waterfallValue": 250
    }
  },
  "internalActionsContributions": [
    {
      "actionid": "useraction_efficiency_2027",
      "actionname": "Office energy efficiency",
      "abatementByYear": {
        "2027": 18.5,
        "2030": 22
      }
    },
    {
      "actionid": "useraction_fleet_ev_2028",
      "actionname": "Switch petrol fleet to EV",
      "abatementByYear": {
        "2028": 22,
        "2030": 28
      }
    }
  ],
  "methodology": {
    "note": "Illustrative stub — real responses include methodology statement text"
  },
  "transitionPlan": {
    "note": "Illustrative stub — real responses include structured transition-plan sections"
  },
  "orgunitResults": [
    {
      "id": "orgunit_sydney",
      "orgunitid": "orgunit_sydney",
      "ghgBAUsumperyear": [
        390,
        397.8,
        405.8,
        413.9,
        422.2,
        430.6
      ],
      "ghgEXTsumperyear": [
        390,
        385,
        372,
        358,
        345,
        332
      ],
      "ghgINTsumperyear": [
        390,
        385,
        340,
        280,
        265,
        250
      ],
      "internalActionsContributions": []
    }
  ]
}

Numbers above are shortened for documentation. Real responses use full projectionyears ranges (often through 2050).

8. Extended vs standard

Add extended=true for per-activity time series, scope breakdowns, and MAC curve points used by pathway and activity charts.

curl — calculate (extended) bash
#!/usr/bin/env bash
# Extended response includes per-activity AD/GHG arrays, scope emissions, and MAC curve points.

curl -sS -X POST \
  "https://app.viablepathway.net/api/calculate?extended=true&savePayload=false" \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -d @minimal-payload.json

What ?extended=true adds

The web client uses the extended payload. API integrators often start with the standard response, then request extended when they need activity detail, scope stacks, or MAC curves. Each chart below ships with downloadable Chart.js starter code.

Scope stacked pathways

bauScopeEmissions, extScopeEmissions, intScopeEmissions — Scope 1 / 2 / 3 arrays aligned to projectionyears. Snapshot at 2030.

Activity quantity pathways

projectedEXTAD2 and projectedINTAD2 — second-pass activity data (all actions, balancing, and trends). Join on activity. Solid = EXT, dashed = INT. See why AD2.

Per-activity emissions

projectedBAUGHG, projectedEXTGHG, projectedINTGHG — same shape as AD arrays, in tCO₂e. Example: grid electricity.

MAC curve

maccByEndpoint[2030] — Sunrise Bakery internal actions from section 4: LED lighting (profitable), EV vans, and electric ovens. X is abatement to 2030; Y is marginal cost (/tCO₂e, NPV). Bubble colour is profitable / low / high MAC.

response-extended-excerpt.json (illustrative) JSON
{
  "_comment": "Excerpt of fields added (or expanded) when calling POST /api/calculate?extended=true&savePayload=false. Arrays are shortened for readability.",
  "projectionyears": [
    2025,
    2026,
    2027,
    2028,
    2029,
    2030
  ],
  "bauScopeEmissions": {
    "1": [
      48,
      49,
      50,
      51,
      52,
      53
    ],
    "2": [
      330,
      336,
      342,
      348,
      355,
      362
    ],
    "3": [
      12,
      12.8,
      13.8,
      14.9,
      15.2,
      15.6
    ]
  },
  "extScopeEmissions": {
    "1": [
      48,
      47,
      45,
      43,
      41,
      39
    ],
    "2": [
      330,
      326,
      315,
      303,
      292,
      281
    ],
    "3": [
      12,
      12,
      12,
      12,
      12,
      12
    ]
  },
  "intScopeEmissions": {
    "1": [
      48,
      47,
      45,
      28,
      26,
      24
    ],
    "2": [
      330,
      326,
      283,
      240,
      227,
      214
    ],
    "3": [
      12,
      12,
      12,
      12,
      12,
      12
    ]
  },
  "extScopeEmissionsMin": {
    "1": [],
    "2": [],
    "3": []
  },
  "extScopeEmissionsMax": {
    "1": [],
    "2": [],
    "3": []
  },
  "intScopeEmissionsMin": {
    "1": [],
    "2": [],
    "3": []
  },
  "intScopeEmissionsMax": {
    "1": [],
    "2": [],
    "3": []
  },
  "sbtiPathwayByScope": {
    "1": [
      48,
      45,
      42,
      39,
      36,
      33
    ],
    "2": [
      330,
      308,
      286,
      264,
      242,
      220
    ],
    "3": [
      12,
      12,
      12,
      12,
      12,
      12
    ]
  },
  "userDefinedPathwayByScope": null,
  "projectedEXTAD2": [
    {
      "activity": "utilities_electricity_grid@@au-nsw",
      "values": [
        500000,
        510000,
        520200,
        530604,
        541216,
        552040
      ]
    },
    {
      "activity": "companyvehicle_car_petrol@@au-nsw",
      "values": [
        250000,
        255000,
        260100,
        265302,
        270608,
        276020
      ]
    }
  ],
  "projectedINTAD2": [
    {
      "activity": "utilities_electricity_grid@@au-nsw",
      "values": [
        500000,
        510000,
        442170,
        451013,
        460034,
        469234
      ]
    },
    {
      "activity": "companyvehicle_car_petrol@@au-nsw",
      "values": [
        250000,
        255000,
        260100,
        132651,
        135304,
        138010
      ]
    },
    {
      "activity": "companyvehicle_car_bev@@au-nsw",
      "values": [
        10000,
        10200,
        10404,
        143055,
        145866,
        148732
      ]
    }
  ],
  "projectedBAUGHG": [
    {
      "activity": "utilities_electricity_grid@@au-nsw",
      "values": [
        340,
        346.8,
        353.7,
        360.8,
        368,
        375.4
      ]
    }
  ],
  "projectedEXTGHG": [
    {
      "activity": "utilities_electricity_grid@@au-nsw",
      "values": [
        340,
        336,
        325,
        313,
        302,
        291
      ]
    }
  ],
  "projectedINTGHG": [
    {
      "activity": "utilities_electricity_grid@@au-nsw",
      "values": [
        340,
        336,
        276,
        245,
        232,
        219
      ]
    }
  ],
  "maccByEndpoint": {
    "2030": [
      {
        "actionid": "action_led_lighting_2026",
        "actionname": "Replace old lights with LED",
        "abatement": 18,
        "marginalCost": -32
      },
      {
        "actionid": "action_ev_vans_2027",
        "actionname": "Switch delivery vans to electric",
        "abatement": 112,
        "marginalCost": 42
      },
      {
        "actionid": "action_electric_ovens_2028",
        "actionname": "Electrify ovens and stoves",
        "abatement": 54,
        "marginalCost": 76
      }
    ],
    "2040": [],
    "2050": []
  }
}

9. Scenario analysis

POST /api/scenarioanalysis reuses the organisation payload and overlays climate-scenario emission-factor narratives (for example NGFS, AASB, Arup, TCFD-inspired sets).

scenarioSet Narrative source slug (e.g. ngfs_climate_scenarios)
selectedOrgUnit Required when the org spans multiple countries
baselineEmissions Baseline pathway context in the response
narrativeSummaries[] Per-narrative GHG pathway sums + years
sections Narrative report text blocks
scenario-request.json JSON
{
  "organisationid": "DEMO_CO",
  "datastructureversion": 2,
  "scenarioSet": "ngfs_climate_scenarios",
  "selectedOrgUnit": "orgunit_sydney",
  "meta": {
    "name": "Demo Company",
    "currency": "aud",
    "desiredgrowth": 2,
    "projectionstartyear": 2025,
    "projectionendyear": 2050,
    "electricityapproach": "location"
  },
  "target": {
    "neartermsbtitarget": "1.5",
    "neartermsbtitargetyear": 2030
  },
  "orgunits": {
    "orgunit_sydney": {
      "name": "Sydney Office",
      "orgunitid": "orgunit_sydney",
      "country": "australia",
      "region": "au-nsw",
      "currency": "aud",
      "employees": 120,
      "datastructureversion": 2,
      "activitydata": {
        "utilities_electricity_grid@@au-nsw": {
          "activityname": "Grid electricity — NSW",
          "catalogactivityid": "utilities_electricity_grid",
          "category": "utilities",
          "region": "au-nsw",
          "activitydatavalue": 500000,
          "activityuom": "kWh",
          "ghgpscope": 2,
          "ghgpscopecategory": "2-1 Indirect Emissions from Purchased Electricity",
          "emissionfactorlabel": "NSW grid electricity (location)",
          "emissionfactortrendid": "utilities_electricity_grid",
          "emissionfactorvalue": 0.68,
          "emissionfactorvalue_location": 0.68,
          "emissionfactorunit": "kgCO2e",
          "emissionfactorsource": "dcceew-aus"
        },
        "companyvehicle_car_petrol@@au-nsw": {
          "activityname": "Fleet — petrol cars",
          "catalogactivityid": "companyvehicle_car_petrol",
          "category": "companyvehicle",
          "region": "au-nsw",
          "activitydatavalue": 250000,
          "activityuom": "km",
          "ghgpscope": 1,
          "ghgpscopecategory": "1-2 Direct Emissions from Mobile Combustion",
          "emissionfactorlabel": "Petrol passenger cars",
          "emissionfactortrendid": "companyvehicle_car_petrol",
          "emissionfactorvalue": 0.192,
          "emissionfactorunit": "kgCO2e",
          "emissionfactorsource": "desnz-uk"
        }
      },
      "internalactions": {},
      "historicemissionsyears": [
        2023,
        2024
      ],
      "historicemissions": {
        "1": [
          48.2,
          46.1
        ],
        "2": {
          "location": [
            340,
            325.5
          ],
          "market": [
            340,
            325.5
          ]
        },
        "3": [
          12,
          11.5
        ]
      }
    }
  }
}
curl — scenario analysis bash
#!/usr/bin/env bash
# Scenario analysis reuses the organisation payload and adds scenarioSet.
# Multi-country orgs should also set selectedOrgUnit to an org-unit key.

curl -sS -X POST \
  "https://app.viablepathway.net/api/scenarioanalysis" \
  -H "Content-Type: application/json" \
  -H "Authorization: ApiKey YOUR_API_KEY" \
  -d @scenario-request.json

10. Try it yourself

Download the samples, set your key in the environment, and call the API from your machine or notebook. We share a Jupyter (Colab-ready) notebook after your key is issued — it is not published here so keys never appear on this site.

Ready to call the live API?

Request an API key and we will send access details plus the Jupyter notebook walkthrough.

Request an API key

11. OpenAPI

Include the schema file if you generate clients or validate payloads. Do not treat it as a full Swagger dump of the production API.

OpenAPI schema

Download the OpenAPI 3.1 schema document for codegen, IDE validation, and payload checks. It includes CalculationInputs, activity rows, and internal actions.

  • Most HTTP operations — including POST /api/calculate — are merged at runtime. This file is not a complete path catalog.
  • We do not publish the live assembled spec. That document includes admin, billing, and demo routes that are not part of the partner calculate API.
  • Prefer nested meta / target and datastructureversion: 2 as on this page if the YAML still describes flat root fields.

Download openapi-base.yml

12. FAQ and frequent errors

HTTP statuses returned to API keys, then the payload mistakes and modelling questions we see most often.

HTTP status codes

These are the codes partner keys see on calculate, data, methodology, and scenario analysis. Error bodies are JSON with at least error and message. Validation failures also include details[].

Code Meaning When you see it
200 OK The request succeeded. Calculate, data, methodology, and scenario analysis return JSON on success.
400 Bad Request Invalid JSON, OpenAPI schema failure (body has details[]), unknown meta.externalTrendsVersion, or the engine could not run the payload (error is often Calculation failed).
401 Unauthorized Missing or malformed API key. Use Authorization: ApiKey … or X-API-Key — not Bearer with the API key.
403 Forbidden This key is limited to calculate, scenario analysis, data, and methodology. Admin, billing, and organisation-save routes return 403.
404 Not Found GET /api/data/:name is not a known catalog, or ?version= is not a published external-trends pack id.
415 Unsupported Media Type POST body is not Content-Type: application/json. Do not send form-encoded or text/plain JSON.
500 Internal Server Error Unexpected server failure. Retry with savePayload=false. If it continues, contact us with the UTC timestamp — never paste the API key.
503 Service Unavailable Lock timeout while another write held the organisation. Rare on compute-only calls; wait and retry savePayload=false.

Errors and status codes

401 Unauthorized

Send the key as Authorization: ApiKey YOUR_API_KEY or X-API-Key: YOUR_API_KEY. A Cognito JWT uses Authorization: Bearer — do not put the API key in a Bearer header. Keep the key in an environment variable; this site never embeds one.

400 with a details array (OpenAPI validation)

Request-body failures return 400 with error, message, and a details array. Read details[].summary first — it includes the JSON path and allowed values. Typical causes: missing ActivityDataEntryV2 fields such as emissionfactorlabel; target.neartermsbtitarget not one of 1.5, WB2, 2, or null; orgunits not an object keyed by unit id. capitalcost and capitalcostperunit may be null.

400 Calculation failed (no details array)

The body passed schema checks but the engine rejected it. The message is the first place to look — common causes are empty orgunits, a projection window that cannot be built, or an unknown meta.externalTrendsVersion (that variant also returns latestId and availableIds). Fix the payload and retry with savePayload=false.

Noisy oneOf / deletedAt errors on an action

Internal actions are validated as either a real action or a library deletion marker. If a required action field fails, the validator also tries the deletion-marker schema and reports originalLibraryIndex / deletedAt. Fix the first real field error (often activity, startyear, or multiplier); ignore the deletion-marker cascade.

403 Forbidden

Issued API keys can call POST /api/calculate and POST /api/v2/calculate, POST /api/scenarioanalysis and POST /api/v2/scenarioanalysis, GET /api/data, and POST /api/methodology and POST /api/v2/methodology. Other routes (admin, billing, organisation input save) return 403. That is expected — organisation input is stored by the web app, not by partner keys. Existing keys work on v2 without reissue. /api/calculate is frozen v1; /api/v2/calculate is current methodology.

404 on GET /api/data/…

Use a name from GET /api/data (for example standardnames, template, emissionFactorsEXTtrends, activityDataEXTtrends). An unknown name returns 404. Passing ?version= with an unpublished pack id also 404s; omit version to use the latest pack, or list packs at GET /api/data/externalTrendsVersions.

415 Unsupported Media Type

POST /api/calculate expects Content-Type: application/json and a JSON object body. Form posts, multipart, and text/plain with a JSON string are rejected.

500 or a timeout on a large organisation

Very large inventories can exceed the compute window. Retry with savePayload=false, fewer org units, or a shorter projectionendyear while you test. If it persists, include the UTC timestamp when you contact us — do not paste API keys into tickets.

Payload and modelling

What are BAU, EXT, and INT?

BAU is the growth-only pathway. EXT applies market and grid trends. INT applies your internal actions on top of EXT. Activity time series after the second pass are the AD2 arrays — full explanation.

EXT looks the same as BAU for a row

External trends apply only when payload ids match GET /api/data/standardnames exactly. Set emissionfactortrendid to the catalog id for emission-factor trends (for example utilities_electricity_grid), and catalogactivityid to the catalog id for activity-data trends. Do not use display labels or the @@region instance id as those match keys. See standard names and external trends.

400 because emissionfactorlabel is missing

datastructureversion 2 requires emissionfactorlabel on each activity row (a display string). Trend matching uses emissionfactortrendid, not the label. The old emissionfactorname field is not a substitute for either.

An action does not move the activity I expected

internalactions.*.activity (and balancingactivity) must be the flattened instance id — the same activity string on projected AD arrays. With regional rows that is often catalogActivity@@region (for example utilities_electricity_grid@@au-nsw), not the catalog id alone. multiplier is a fraction (for example -0.15 for −15%), not a percentage point value.

historicemissions for scope 2 is rejected

Scope 2 may be a number array (legacy) or an object { "location": number[], "market": number[] } aligned to historicemissionsyears. Prefer the nested shape, especially when meta.electricityapproach is market.

Country or region does not match any trends

Org-unit country tokens must match the catalog after normalisation (lowercase, no spaces): australia, uk — not Australia or unitedkingdom unless that is the catalog token. Region values look like au-nsw. Mismatched geography still calculates, but library trends for that place will not attach.

Charts or activity series are missing

The standard calculate response is totals only. Activity pathways, scope stacks, and MAC points need POST /api/calculate?extended=true&savePayload=false. Chart activity quantities from projectedEXTAD2 / projectedINTAD2, not a first-pass AD array — why AD2.

What units are in the response?

Activity-data arrays are in the row’s activityuom (kWh, km, and so on). GHG totals such as ghgBAUsumperyear are tonnes of CO2e. Emission-factor values on the payload are typically kgCO2e per activity unit. Do not mix those scales when charting.

Flat root fields vs nested meta and target

Prefer datastructureversion: 2 with nested meta and target. Flat keys still work. If both are present, nested wins. Calculate responses stay flat (ghgBAUsumperyear, and similar).

How do I pin an external-trends pack?

Set meta.externalTrendsVersion to a published pack id (for example 2026Q3). Omit it to use the latest pack. List packs with GET /api/data/externalTrendsVersions. An unknown id is 400 on calculate (with availableIds) and 404 on GET /api/data/…?version=.

When should I call scenario analysis instead of calculate?

POST /api/calculate returns BAU, EXT, and INT for your payload. POST /api/scenarioanalysis overlays named climate narratives (set scenarioSet, for example ngfs_climate_scenarios) on the same organisation. Use calculate for pathway charts; use scenario analysis for narrative stress tests — section 8.

Access and files

What is the difference between /api/calculate and /api/v2/calculate?

POST /api/calculate is Calculation API v1 (frozen). POST /api/v2/calculate is current methodology. Existing API keys work on both. Switch by inserting /v2 after /api. New integrations should use v2 — section 5.

Should I set savePayload=false?

Always, for API keys. Default savePayload=true exists for the web app. Partner calculate calls should stay compute-only with savePayload=false. Do not persist organisation input through the API.

Where is the OpenAPI file?

Download the schema document from this page (OpenAPI). It covers CalculationInputs and related components. It is not a dump of the live assembled spec, which includes internal admin and billing routes.

Where is the Jupyter notebook?

It is sent after your key is issued so keys never appear on this site. Request a key and we will include the notebook with access details.

How do I get an API key?

Request access from this page. Keys are issued to approved customers and partners. Store the key in an environment variable and never commit it, put it in a public notebook, or paste it into a support ticket.