Using the API
Pagination
Lists page with an opaque cursor, never with an offset: page two thousand costs what page one costs, and rows are neither skipped nor repeated when the data moves underneath you.
The envelope
A paged list answers:
{
"data": [
"…"
],
"pagination": {
"has_more": true,
"next_cursor": "eyJzIjoic3Vic2lkaWVzOnVwZGF0ZWQiLCJrIjoi…",
"limit": 50
},
"meta": {
"request_id": "req_…",
"warnings": []
}
}has_moreis a fact, not a guess: the API reads one row more than you asked for. There is no total count.next_cursoris set only whenhas_moreis true. Pass it back ascursor, with the same parameters, for the next page. The change feed is the exception: it returns a cursor on every page, the last included, so you can poll on from there (see following the feed).limitis the page size that was used.
curl "https://api.subsido.be/v1/subsidies?status=open&sort=updated&limit=100&cursor=eyJzIjoic3Vic2lkaWVzOnVwZGF0ZWQiLCJrIjoi…" \ -H "Authorization: Bearer sb_live_..."Page sizes
limit defaults to 50. Above your plan’s maximum it is an error, 400 invalid_parameter, never a silent truncation: quietly serving 20 rows to someone who asked for 100 is how a client comes to believe it has read a whole list when it has read its first page.
| Plan | Maximum limit |
|---|---|
| Developer | 100 |
| Pro, Business | 200 |
| Enterprise | 500 |
The same figures are in max_page_size of GET /v1/plans and GET /v1/key. A key whose account has no plan gets 402 subscription_required on every list, whatever the limit.
Cursors
- A cursor carries the last row’s sort key and id. Treat it as opaque: its format can change.
- A cursor belongs to the listing and sort order it came from. Replayed against another (a different
sort, another route) it is400 invalid_cursor, and you start again without one; the API never silently restarts at page one. - Keep the filters the same while you page. The cursor does not remember them, so changing them mid-way pages through a different list from where the old one stopped.
- Page one request at a time. Parallel page fetches do not make a cursor walk faster and do spend your per-second rate limit.
Sort orders
| List | Order |
|---|---|
| GET /v1/subsidies | sort=relevance (best text match; the default with q), deadline (soonest first, no deadline last; the default without q), updated (most recently changed first) or title. Relevance without a searchable q falls back to updated. |
| GET /v1/changes | Oldest first, by seq. See following the feed. |
| GET /v1/watchlists/{id}/companies | By reference. |
| GET /v1/webhooks | Oldest first. |
Which lists page
| Route | Paging |
|---|---|
| GET /v1/subsidies, /v1/changes, /v1/watchlists/{id}/companies, /v1/webhooks | limit and cursor, as above. |
| GET /v1/watchlists | Every watchlist in one page (at most 50); has_more is always false. |
| GET /v1/watchlists/{id}/matches | No cursor: limit from 1 to 5,000 (default 500), filtered by company. |
| GET /v1/subsidies/{id}/versions | Every version in one answer, newest first. |
| GET /v1/sources | Every source in one answer. |
| GET /v1/webhooks/{id}/deliveries | The last 50. |
| GET /v1/export/subsidies.ndjson | The whole catalogue in one streamed download (Business and above). |
Walking a whole list
import os, requests
def all_open_measures():
params = {"status": "open,continuous,forthcoming", "sort": "updated", "limit": 100}
headers = {"Authorization": f"Bearer {os.environ['SUBSIDY_API_KEY']}"}
while True:
r = requests.get("https://api.subsido.be/v1/subsidies", params=params, headers=headers, timeout=30)
r.raise_for_status()
page = r.json()
yield from page["data"]
if not page["pagination"]["has_more"]:
return
params["cursor"] = page["pagination"]["next_cursor"]Every row, every time
Every field of every row is present on every page,
null where it does not apply, so a row has the same keys on page one and on page four hundred. For the whole catalogue at once, the export is cheaper than paging.