Checking IDX-listed companies for annual reports using Deno
I needed to check whether 102 Basic Materials companies listed on the Indonesian Stock Exchange have published their annual financial reports. I forked an existing IDX API wrapper, wrote a custom check script, and got blocked by Cloudflare.
I had a simple question: do these 102 companies in the Basic Materials sector have annual financial reports on the IDX website? I needed to check for the years 2022 to 2025. Not download the PDFs, not read the reports. Just a yes or no per company per year.
Why I needed this
I was working with a list of Indonesian stock market emiten. The list had 102 companies, all in the Basic Materials sector. Each one had a ticker code, a name, and a sector classification. Something like this:
103;AVIA;Avian Tbk;Basic Materials
104;AYLS;Agro Yasa Lestari Tbk;Basic Materials
105;BAJA;Saranacentral Bajatama Tbk;Basic Materials
I wanted to know which of these companies have published their annual financial reports. The IDX website has a page for each company with financial reports going back years. But checking 102 companies manually, for 4 years each, would have taken forever. I needed to automate it.
Finding an existing API wrapper
I searched around and found a repo called IDX-API by NeaByteLab. It is a Deno project that wraps the IDX API endpoints. It handles session cookies, retries on failure, and stores data in a local SQLite database using Drizzle ORM.
The repo already had a CompanyModule class with a method called getFinancialReports. It takes a company code, a year, and a period like 'audit' (for annual reports) or 'TW1' (quarter one). It calls the IDX API and returns the report metadata.
I forked the repo and started looking at the code.
How the API works
The IDX API endpoint for financial reports looks like this:
https://www.idx.co.id/primary/ListedCompany/GetFinancialReport
?kodeEmiten=BBCA
&year=2024
&periode=audit
&pageSize=100
&reportType=rdf
The API returns JSON with a Results array. If the company has a report for that year, ResultCount is 1 and the Results array contains the attachment metadata. If not, ResultCount is 0.
But here is the catch. The IDX website uses Cloudflare. You cannot just call the API directly. The existing wrapper handles this by first visiting the main IDX page to get a session cookie, then making a validation request, and only then calling the actual API.
The BaseClient class in the repo does all of this. It stores the session cookie and adds it to every request. It also has retry logic with exponential backoff.
Writing the check script
Since the repo already had the API client, I did not need to write much. I just needed a script that reads my CSV file, loops through the companies, and calls getFinancialReports for each year.
Here is the core of the script:
import IDXClient from '@app/index.ts'
const client = new IDXClient()
const years = [2022, 2023, 2024, 2025]
for (const emiten of emitenList) {
for (const year of years) {
const reports = await client.company.getFinancialReports(
emiten.code, year, 'audit'
)
if (reports && reports.length > 0) {
// YES, this company has a report for this year
} else {
// NO, no report found
}
}
}
I wrote the results to a CSV file so I could stop the script at any time and still have the data. The output looked like this:
No;Code;Name;2022;2023;2024;2025;Total
1;AVIA;Avian Tbk;YES;YES;YES;YES;4/4
2;AYLS;Agro Yasa Lestari Tbk;YES;YES;YES;YES;4/4
3;BAJA;Saranacentral Bajatama Tbk;YES;YES;YES;NO;3/4
Getting blocked by Cloudflare
The first run failed after about 17 requests. Cloudflare started returning a block page instead of the API response. My script was too fast.
The existing wrapper had retry logic for server errors, but it did not handle Cloudflare blocks. When Cloudflare returns an HTML page saying "Sorry, you have been blocked," the JSON parser fails and the function returns null. So my script marked those as "NO" even though the company might actually have a report.
I needed to slow down the requests. A lot, apparently.
I added random delays between each request. Between 1 and 2.5 seconds for each year check, and 3 to 5 seconds between companies. I also added a 15 second pause every 10 companies. And if the script detected a rate limit response (503, 403, or Cloudflare-specific errors), it would wait 30 seconds and retry once.
function randomDelay(min: number, max: number): Promise<void> {
const ms = Math.floor(Math.random() * (max - min + 1)) + min
return new Promise((r) => setTimeout(r, ms))
}
With these delays, the script ran for about 15 minutes to check all 102 companies. Not fast, but it worked. No more Cloudflare blocks.
The results
Out of 102 companies in the Basic Materials sector:
- 63 companies had all 4 years of reports (2022 through 2025). That is about 62 percent.
- 32 companies had partial data. Some years missing, usually the earlier ones.
- 7 companies had little to no data. Some like KBRI (Kertas Basuki Rachmat Indonesia) and SIMA (Siwani Makmur) had zero reports for the entire period.
The full results are in the CSV file. Nothing surprising, but it confirmed what I needed for my work.
The code
You can find the script in my fork of the repo: anantafaturdev/IDX-API. The check script is check-emiten-reports.ts and the results are in emiten-report-check.csv.