Tutorial10 min
Ingest the report feed
- Endpoint
GET /reports- Auth
X-API-Key- Query
cursor, pageSize, filters- Returns
200 { data, pageSize, nextCursor }
Before you start
A Core API base URL
Pick the environment you are building against. Base URLs has the details.
- ProductionProvided at onboardingMutual TLS required
- Sandbox
https://staging-api.emergentriskinternational.com/api/v1Same contract as production - Local
http://127.0.0.1:3001/api/v1apps/backend on your machine
An API Key
Organization-scoped, sent in the
X-API-Keyheader and issued by ERI support. See Authentication.Running the backend locally
Bootstrap the first key from
apps/backend. The script prints the raw key once, so store it outside the repository.pnpm migrations:uppnpm api-keys:bootstrapSomewhere to store reports
A table or bucket keyed by report
id. You write each report byid, so a re-run updates rows instead of duplicating them.
Step 1: Request the first page
Call
/reportswithout a cursor. Reports come back newest first, bypublicationDate.pageSizeis a request, not a promise. Leave it out and the server uses its default; ask for more than its maximum and it clamps. The size it actually used comes back aspageSize.List items carry the metadata only. The Markdown body and images come from Retrieve a report.
Terminalexport CORE_API_BASE_URL="http://127.0.0.1:3001/api/v1"export ERI_API_KEY="<raw API Key from bootstrap or ERI support>"- cURL
- Python
- Node.js
curl -G "$CORE_API_BASE_URL/reports" \-H "X-API-Key: $ERI_API_KEY" \--data-urlencode "pageSize=2"import os, requestsresponse = requests.get(f"{os.environ['CORE_API_BASE_URL']}/reports",headers={"X-API-Key": os.environ["ERI_API_KEY"]},params={"pageSize": 2},)page = response.json()const url = new URL(`${process.env.CORE_API_BASE_URL}/reports`);url.searchParams.set("pageSize", "2");const response = await fetch(url, {headers: { "X-API-Key": process.env.ERI_API_KEY },});const page = await response.json();200 OK{"data": [{"id": "01a04100-0000-7000-8000-000000000001","title": "Daily Risk Forecast - 2026-09-01","publicationDate": "2026-09-01T10:53:16.189Z","region": "Global","reportType": "Daily Risk Forecast","regions": ["Global"],"countries": [],"topics": [],"sectors": []},{"id": "01a04100-0000-7000-8000-000000000002","title": "South China Sea - Supply Chain Advisory","publicationDate": "2026-08-31T10:53:16.189Z","region": "East & Southeast Asia","reportType": "Supply Chain Report","regions": ["East & Southeast Asia"],"countries": ["China"],"topics": [],"sectors": ["Maritime"]}],"pageSize": 2,"nextCursor": "eyJwdWJsaWNhdGlvbkRhdGUiOiIyMDI2LTA4LTMxVDEwOjUzOjE2LjE4OVoiLCJpZCI6IjAxYTA0MTAwLTAwMDAtNzAwMC04MDAwLTAwMDAwMDAwMDAwMiJ9"}Step 2: Follow the cursor
Send
nextCursorback ascursorto get the next page. Keep going untilnextCursorisnull: that page is the last one.The cursor is opaque. Pass it back exactly as received and never build or edit one. A cursor the API cannot decode returns
422; start again from the first page.- cURL
- Python
- Node.js
curl -G "$CORE_API_BASE_URL/reports" \-H "X-API-Key: $ERI_API_KEY" \--data-urlencode "pageSize=2" \--data-urlencode "cursor=<nextCursor from the previous page>"def ingest(store):cursor = Nonewhile True:params = {"pageSize": 100}if cursor:params["cursor"] = cursorresponse = requests.get(f"{os.environ['CORE_API_BASE_URL']}/reports",headers={"X-API-Key": os.environ["ERI_API_KEY"]},params=params,)response.raise_for_status()page = response.json()for report in page["data"]:store.upsert(report["id"], report)cursor = page["nextCursor"]if cursor is None:breakasync function ingest(store) {let cursor = null;do {const url = new URL(`${process.env.CORE_API_BASE_URL}/reports`);url.searchParams.set("pageSize", "100");if (cursor) url.searchParams.set("cursor", cursor);const response = await fetch(url, {headers: { "X-API-Key": process.env.ERI_API_KEY },});if (!response.ok) throw new Error(`GET /reports failed: ${response.status}`);const page = await response.json();for (const report of page.data) await store.upsert(report.id, report);cursor = page.nextCursor;} while (cursor !== null);}Step 3: Narrow with filters
Filter by
contentType,region,country,topicandsector. Repeat a parameter for several values, up to 50 each.Values are OR within one parameter and AND across parameters. The example below reads reports tagged Europe or MENA that are also about Cyberthreat.
Values come from the platform taxonomy and match exactly, case included. A value outside it returns
400rather than an empty page, so a typo never looks like a quiet day.Keep the same filters on every page you request with a cursor.
Region and content type values at the time of writing
The taxonomy can grow. If a value starts failing with
400, check it against the latest list from ERI.- region: Asia Pacific, Central & South America, China & Taiwan, East & Southeast Asia, Europe, Global, MENA, North America, South & Central Asia, Sub-Saharan Africa
- contentType: Artificial Intelligence Risk Monthly, Country Reports, Daily Risk Forecast, Geostrategic Trade Monitor, Global Risk Digest, Intelligence Monitoring Room, Rapid Reaction Brief, Strategic Conflict Outlook, Strategic Risk Forecast, Supply Chain Report
Countries, topics and sectors are longer lists. Use the names exactly as reports carry them, for example
Estonia,CyberthreatorMaritime.- cURL
- Python
- Node.js
curl -G "$CORE_API_BASE_URL/reports" \-H "X-API-Key: $ERI_API_KEY" \--data-urlencode "region=Europe" \--data-urlencode "region=MENA" \--data-urlencode "topic=Cyberthreat"response = requests.get(f"{os.environ['CORE_API_BASE_URL']}/reports",headers={"X-API-Key": os.environ["ERI_API_KEY"]},params={"region": ["Europe", "MENA"], "topic": "Cyberthreat"},)const url = new URL(`${process.env.CORE_API_BASE_URL}/reports`);url.searchParams.append("region", "Europe");url.searchParams.append("region", "MENA");url.searchParams.append("topic", "Cyberthreat");const response = await fetch(url, {headers: { "X-API-Key": process.env.ERI_API_KEY },});Step 4: Handle the response
Errors share one JSON envelope. Domain errors carry a stable
codeto branch on; validation errors list the failing parameters inerrors. Pick a status to see the exact body.200OK- When
- A page of reports, newest first. An empty data array is a valid page, not an error.
- What to do
- Store the reports, then request the next page while nextCursor is not null.
Response body{"data": [{"id": "01a04100-0000-7000-8000-000000000004","title": "Baltic Corridor - Cyber Threat Bulletin","publicationDate": "2026-08-29T10:53:16.189Z","region": "Europe","reportType": "Rapid Reaction Brief","regions": ["Europe"],"countries": ["Estonia", "Latvia", "Lithuania"],"topics": ["Cyberthreat"],"sectors": ["Maritime"]}],"pageSize": 50,"nextCursor": null}
Your pipeline reads the feed
Every report your Client Account can read now reaches your store, page by page.