BUILD WITH HAZE
Use Singapore haze data
in your app.
Fetch regional PSI, PM2.5, hourly trends and source-dated guidance through a public JSON API. Add mask and air-purifier product links when they’re useful to your readers.
API version 1 · Guide updated
Get your first regional reading
Use the public HTTPS endpoint below. You don’t need a Haze account, API key or location permission. Choose a reporting region yourself; the API does not accept precise coordinates.
curl --fail-with-body 'https://haze.danielk.am/api/v1/readings?region=central'A successful request returns JSON. Check the source timestamps and status before displaying a reading as current. The API discovery endpoint lists the available routes, and the OpenAPI specification describes their response schemas.
For an AI assistant, use the same data through the Haze MCP server.
Choose an endpoint
All REST endpoints below use GET. They also support HEAD and browser CORS requests. Query values are case-sensitive. Omitted parameters use the defaults shown.
| Path | What it returns and accepted parameters |
|---|---|
/api/v1 | Endpoint discovery, provenance, affiliate disclosure and access policy. |
/api/v1/readings | 24-hour PSI and 1-hour PM2.5. region: all (default), north, south, east, west or central. |
/api/v1/history | Hourly records for a fixed last-24-hour window. region: central (default), all or one of the five regions. metric: all (default), psi or pm25. |
/api/v1/products | Affiliate catalogue entries. category: masks (default) or purifiers. limit: integer 1–10 (default 2). offset: integer 0–10 (default 0). |
/api/v1/guide | Structured PM2.5 bands and PSI forecast health-advisory guidance, with source date and caveats. No query parameters. This is reference guidance, not a live forecast. |
Unknown parameters, repeated parameters and invalid values return 400. Arbitrary dates, user-supplied URLs and coordinates are not accepted.
curl --fail-with-body 'https://haze.danielk.am/api/v1/history?region=west&metric=pm25'
curl --fail-with-body 'https://haze.danielk.am/api/v1/products?category=purifiers&limit=2&offset=0'Show the data’s time and condition
Readings come from NEA through data.gov.sg. The 24-hour PSI is a measured index; 1-hour PM2.5 is a concentration in µg/m³. Neither is a live health-advisory forecast.
- Keep source timestamps visible. A response’s fetch time tells you when Haze retrieved it, not when NEA measured the air. PSI and PM2.5 can have different source times.
- Preserve missing values. A
nullreading or history point means no usable value is available. Don’t replace it with zero or draw a continuous trend across a gap. - Respect status and warnings. Delayed, cached, partial or unavailable data needs a clear label. A successful HTTP response alone does not establish freshness.
- Keep the units and period. Label “24-hour PSI” and “1-hour PM2.5 (µg/m³)” separately. The response includes regional severity labels and emoji as display cues.
Use the air-quality guide for definitions and source-dated guidance. Keep its measured-reading and forecast distinction wherever you reuse it.
Upstream data is cached for 10 minutes. Poll each data endpoint no more than once every 10 minutes. This is a pull service, with no push notifications or availability guarantee.
Embed products with affiliate links
The products endpoint returns masks or air purifiers from the Shopee Affiliate API via 10.sg. Each entry can include its name, image, displayed price and priceCheckedAt. Show that price time, and tell readers to confirm the chosen variant and final price on Shopee.
Use the returned affiliateUrl as the individual product link. It is an existing 10.sg redirect to Shopee. url and catalogueUrl lead to the category catalogue. Don’t construct a Shopee URL from an ID or treat a catalogue link as a product link.
Use has_more and next_offset to page through the available entries, up to 10 per category. The count can be lower than 10, and an upstream failure can leave a warning or no products. Preserve the disclosure returned by the API.
The following browser example adds product links to an element with id="haze-products" and the disclosure to id="haze-affiliate-note". It uses text nodes for catalogue text.
const response = await fetch(
'https://haze.danielk.am/api/v1/products?category=masks&limit=2'
);
if (!response.ok) throw new Error(`Haze request failed: ${response.status}`);
const data = await response.json();
document.querySelector('#haze-affiliate-note').textContent =
data.affiliateDisclosure;
const list = document.querySelector('#haze-products');
list.replaceChildren();
for (const product of data.products) {
if (!product.affiliateUrl) continue;
const item = document.createElement('li');
const link = document.createElement('a');
link.href = product.affiliateUrl;
link.textContent = product.name;
link.target = '_blank';
link.rel = 'sponsored noopener noreferrer';
item.append(link);
list.append(item);
}Affiliate disclosure: product links may earn us a commission. These commercial links are separate from the government air-quality data and do not imply NEA endorsement. See the N95 catalogue and air-purifier catalogue.
Access, attribution and troubleshooting
The API is public and read-only. Browser requests use Access-Control-Allow-Origin: * without cookies or credentials. Don’t put a private token in your request. Haze’s hosting providers process standard web request information, such as IP addresses and request paths.
Attribute readings to NEA via data.gov.sg, retain their actual data dates and link to the Singapore Open Data Licence. Responses include provenance and the independent-project disclaimer. Product catalogue data has separate commercial provenance.
The public API and MCP endpoint share a limit of 60 requests per minute per network address seen by the server. Shared networks, reverse proxies and gateways may share that allowance. If you receive 429, wait for the number of seconds in Retry-After; don’t retry in a tight loop.
- 400 · Invalid request
- Check spelling, permitted values and integer ranges. Remove duplicate or unknown query parameters.
- 405 · Method not allowed
- Use
GETorHEADfor data. The MCP endpoint uses a different protocol; follow the MCP HTTP example. - 429 · Too many requests
- Respect
Retry-After, reduce retries and use the 10-minute polling interval. - 503 · Temporarily unavailable
- Back off and try later. Keep an existing value only with its original source time and a visible delayed label. Never display an error as a zero reading.
For the exact machine-readable fields and error responses, use OpenAPI. For MCP transport errors such as 403, 406, 413 or 415, see MCP troubleshooting.