Pagination
GET/v1/outages and GET/v1/suburbs return a page at a time. Every page ends with a
pagination object:
GET /v1/suburbs?q=cranb&limit=2{
"data": [
{
"id": 7904,
"name": "Cranbourne",
"postcode": "3977",
"state": "vic",
"url": "https://aussieoutagetracker.com.au/locations/vic/power/cranbourne-3977/",
"networks": [
"ausnet"
]
},
{
"id": 7903,
"name": "Cranbourne East",
"postcode": "3977",
"state": "vic",
"url": "https://aussieoutagetracker.com.au/locations/vic/power/cranbourne-east-3977/",
"networks": [
"ausnet"
]
}
],
"pagination": {
"has_more": true,
"next_cursor": "eyJzIjoibmFtZSIsInYiOiJjcmFuYm91cm5lIGVhc3QiLCJpIjo3OTAzfQ",
"limit": 2
},
"meta": {
"data_as_of": "2026-10-20T02:28:26.234Z"
}
}A real response, captured from the API. Arrays are shortened to two items.
has_moreistruewhen there is at least one more page.next_cursoris what you send ascursorto get it. It isnullon the last page.limitis the page size in use: 100 unless you ask, and at most 500.
Walking to the end
Send the same filters and sort with each page's cursor, and stop when has_more is false:
let cursor;
const all = [];
do {
const params = new URLSearchParams({ state: 'nsw', from: '2026-10-01T00:00:00+10:00', limit: '100' });
if (cursor) params.set('cursor', cursor);
const res = await fetch(`https://api.aussieoutagetracker.com.au/v1/outages?${params}`, {
headers: { Authorization: `Bearer ${process.env.AOT_API_KEY}` },
});
const page = await res.json();
all.push(...page.data);
cursor = page.pagination.next_cursor;
} while (cursor);Each page is a call, so a larger limit walks the same list in fewer calls.
Longer than a month
One query of GET/v1/outages reads at most 31 days of outage start
times, so the archive is read a month at a time rather than in one sweep:
fromandtomay be at most 31 days apart;fromalone reads the 31 days after it, andtoalone the 31 days before it;- with neither, the window is the last 31 days.
There is no window for active_at (one instant), for changed_since (which itself reaches back at most
31 days), or for a status of only active or scheduled, which are live. A
window that is too wide is refused with 400 invalid_parameter, naming the parameter.
No date is out of reach. To read further back, walk one window to its end, then the next. For years at once, or a whole state, request a bulk export in Settings › API: we quote it and send the file.
Cursors, not page numbers
The cursor says "the rows after this one", by the sort value and id of the last row you received. That is
a different promise from page=7, and a better one for a program:
- It costs the same on page 400 as on page 1. An offset makes the database re-read every row it skips, so deep pages get slower and slower. A cursor is a single index seek.
- It is stable while the archive grows. New outages are recorded every few minutes. With offsets, a row inserted at the head shifts everything after it, so a walk sees one outage twice and silently misses another. A cursor carries on from exactly where it stopped.
- There is no total count. Counting a filtered archive is the expensive half of any query, and a
program walking to the end does not need it. For totals, use GET
/v1/stats/outages.
Rules
- A cursor is opaque. It is base64url text; do not build, edit or store meaning in one.
- A cursor belongs to its sort. One issued for
sort=-startand sent withsort=updatedis refused with400 invalid_parameterrather than silently paging in the wrong order. Start again without a cursor when you change the sort. - Keep the filters the same. The cursor records where you are in the sort, not which filters you used. Change a filter mid-walk and the remaining pages answer the new question from the old position.
- Outages arriving during a walk. A walk newest-first (
-start) will not see an outage that starts after it began. To pick up everything that changed, follow up withchanged_since— see Keeping a copy in sync.