A food or agri business can price its inputs from published series. Narwhal returns monthly wheat, rice, sugar, coffee and palm oil prices with unit and currency, and palm oil producer schedules, so the monthly change is a short calculation.
Read the monthly benchmark prices
curl -s "https://api.narwhalapi.com/v1/commodities/physical/series/worldbank-cmo-wheat-us-hrw/observations?start=2026-07-01&end=2026-10-01" \
-H "Authorization: Bearer $NARWHAL_API_KEY"{
"series_id": "worldbank-cmo-wheat-us-hrw",
"currency": "USD",
"unit": "metric_ton",
"unit_key": "tonne",
"unit_quantity": "1",
"attribution": "The World Bank: World Bank Commodity Price Data (The Pink Sheet), CC BY 4.0",
"observations": [
{
"period_start": "2026-07-01",
"period_end": "2026-07-31",
"price": "310.0",
"revision": 1
},
{
"period_start": "2026-08-01",
"period_end": "2026-08-31",
"price": "330.0",
"revision": 1
},
{
"period_start": "2026-09-01",
"period_end": "2026-09-30",
"price": "347.0",
"revision": 1
}
],
"next_cursor": null
}Each observation is one month. price is a decimal string, period_start and period_end give the month and revision is 1 on these rows. The unit and currency are on the response, USD per metric_ton for wheat. Sugar and coffee are quoted per kilogram, so read the unit on every series before you compare two of them.
The ids follow the pattern worldbank-cmo- plus the commodity: worldbank-cmo-rice-thai-5, worldbank-cmo-sugar-world, worldbank-cmo-coffee-arabica and worldbank-cmo-palm-oil. GET /v1/commodities/physical/series with category=grains, softs or oils-oilseeds lists the others.
Show the monthly change in code
- Request three months The script asks for July to September 2026 for five series. The
enddate is exclusive, so 2026-10-01 stops at September. - Compute each change The change is the month's price divided by the previous month's price, minus one, printed as a percent beside the unit.
- Read the output Sugar rose most in August and September. Coffee fell in September while wheat, rice and palm oil rose.
import os import requests API = "https://api.narwhalapi.com" HEAD = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"} series = [ "worldbank-cmo-wheat-us-hrw", "worldbank-cmo-rice-thai-5", "worldbank-cmo-sugar-world", "worldbank-cmo-coffee-arabica", "worldbank-cmo-palm-oil", ] for sid in series: r = requests.get(f"{API}/v1/commodities/physical/series/{sid}/observations", params={"start": "2026-07-01", "end": "2026-10-01"}, headers=HEAD, timeout=15) r.raise_for_status() body = r.json() obs = body["observations"] line = [f"{sid} ({body['currency']} per {body['unit']})", f"{obs[0]['period_start'][:7]} {obs[0]['price']}"] for prev, cur in zip(obs, obs[1:]): change = 100 * (float(cur["price"]) / float(prev["price"]) - 1) line.append(f"{cur['period_start'][:7]} {cur['price']} ({change:+.1f}%)") print(" | ".join(line))
| Series | Unit | July | August | September |
|---|---|---|---|---|
| Wheat, US HRW | USD per metric_ton | 310.0 | 330.0 (+6.5%) | 347.0 (+5.2%) |
| Rice, Thai 5% | USD per metric_ton | 467.0 | 471.0 (+0.9%) | 482.0 (+2.3%) |
| Sugar, world | USD per kilogram | 0.34 | 0.38 (+11.8%) | 0.41 (+7.9%) |
| Coffee, arabica | USD per kilogram | 7.91 | 7.97 (+0.8%) | 7.26 (-8.9%) |
| Palm oil | USD per metric_ton | 1133 | 1132 (-0.1%) | 1180 (+4.2%) |
Sugar is quoted to two decimals, so a one cent move on 0.34 is a change of nearly three percent. Compute changes from the published price strings and round only the final percent. For a buyer with a fixed monthly budget, the September column is the one to set against the budget, with July kept as the base.
Read palm oil producer prices
GET /v1/commodities/palmoil/physical lists palm oil price series that are not the world benchmark. Filters combine, and product is ffb for fresh fruit bunches or cpo for crude palm oil. A filter for fresh fruit bunches under purchase agreements returned one series.
curl -s "https://api.narwhalapi.com/v1/commodities/palmoil/physical?product=ffb&producer_arrangement=purchase_agreement" \
-H "Authorization: Bearer $NARWHAL_API_KEY"{
"series": [
{
"frequency": "weekly",
"coverage_start": "2026-09-02",
"coverage_end": "2026-10-27",
"series_id": "id-ri-oil-palm-ffb-smallholder-purchase-agreement",
"country": "IDN",
"area": "ID-RI",
"currency": "IDR",
"product": "ffb",
"observation_type": "age_schedule",
"price_type": "official_purchase_schedule",
"period_type": "effective",
"unit": "kilogram",
"unit_key": "kilogram",
"producer_arrangement": "purchase_agreement"
}
],
"next_cursor": null
}The series is Indonesian, quoted in IDR per kilogram. Its observation_type is age_schedule and its price_type is official_purchase_schedule, so a week holds a list of prices by tree age instead of one number. Read the observations with the /observations route.
curl -s "https://api.narwhalapi.com/v1/commodities/palmoil/physical/id-ri-oil-palm-ffb-smallholder-purchase-agreement/observations?start=2026-10-14&end=2026-11-03" \
-H "Authorization: Bearer $NARWHAL_API_KEY"{
"series_id": "id-ri-oil-palm-ffb-smallholder-purchase-agreement",
"currency": "IDR",
"unit": "kilogram",
"unit_key": "kilogram",
"observations": [
{
"period_start": "2026-10-14",
"period_end": "2026-10-20",
"revision": 1,
"prices": [
{
"tree_age_min_years": 3,
"tree_age_max_years": 3,
"price": "2786.13"
},
{
"tree_age_min_years": 4,
"tree_age_max_years": 4,
"price": "3110.23"
},
{
"tree_age_min_years": 10,
"tree_age_max_years": 20,
"price": "3565.06"
}
]
},
{
"period_start": "2026-10-21",
"period_end": "2026-10-27",
"revision": 1,
"prices": [
{
"tree_age_min_years": 3,
"tree_age_max_years": 3,
"price": "2846.36"
},
{
"tree_age_min_years": 4,
"tree_age_max_years": 4,
"price": "3176.13"
},
{
"tree_age_min_years": 10,
"tree_age_max_years": 20,
"price": "3640.55"
}
]
}
],
"next_cursor": null
}Each prices entry has tree_age_min_years, tree_age_max_years and price. The band from 10 to 20 years is one entry, so the lookup matches an age against the range.
Compute the weekly change for one tree age
- List and read The script finds the series with the producer filter and reads September 30 to November 3 2026.
- Pick the age band For a 12 year old tree it takes the entry whose age range contains 12, which is the 10 to 20 band.
- Compare weeks It prints the price for each week with the change from the week before.
import os import requests API = "https://api.narwhalapi.com" HEAD = {"Authorization": f"Bearer {os.environ['NARWHAL_API_KEY']}"} listing = requests.get(f"{API}/v1/commodities/palmoil/physical", params={"product": "ffb", "producer_arrangement": "purchase_agreement"}, headers=HEAD, timeout=15).json()["series"] sid = listing[0]["series_id"] r = requests.get(f"{API}/v1/commodities/palmoil/physical/{sid}/observations", params={"start": "2026-09-30", "end": "2026-11-03"}, headers=HEAD, timeout=15) r.raise_for_status() body = r.json() def price_for_age(row, age): return next(float(p["price"]) for p in row["prices"] if p["tree_age_min_years"] <= age <= p["tree_age_max_years"]) previous = None for row in body["observations"]: price = price_for_age(row, 12) change = f" ({100 * (price / previous - 1):+.1f}%)" if previous else "" print(row["period_start"], row["period_end"], f"{price:.2f} {body['currency']} per {body['unit']}{change}") previous = price
| Week | Price, IDR per kilogram | Change |
|---|---|---|
| 2026-09-30 to 2026-10-06 | 3826.84 | none |
| 2026-10-07 to 2026-10-13 | 3694.68 | -3.5% |
| 2026-10-14 to 2026-10-20 | 3565.06 | -3.5% |
| 2026-10-21 to 2026-10-27 | 3640.55 | +2.1% |
Malaysian crude palm oil series use one price per day in MYR per metric_ton, with a price_month field. The latest value of mys-cpo-local-delivered was 4388.00 MYR for the day starting 2026-10-30. The Indonesian reference series idn-cpo-reference-price is monthly in USD per metric_ton. Each series defines its own price, so name the series in the report.
Which palm oil price should I use?
It depends on what you buy. The world benchmark in worldbank-cmo-palm-oil suits a monthly price in USD. The producer schedules fit a buyer who pays growers in Indonesia and wants the IDR price by tree age. Do not mix the two in one change calculation, since they differ in currency, period and definition.
Does a price change need a revision check?
The rows above all carry revision 1. Store the revision with each price so that a later revision to a month can be explained. Compute a change only from two prices of the same series and the same unit. A change between a revised and an unrevised month mixes two vintages, so store the revision beside each price in your own table, and recompute the change whenever a stored revision differs from the one the API now returns.
Can I get rice or coffee in my own currency?
These series are quoted in USD. For a conversion, see get a commodity price in your currency and keep both the original USD price and the rate you used, so the result can be traced.
Published .