Dividend distributions and insider trading activities are two important sources of information for investors. A dividend calendar helps investors identify upcoming corporate actions such as cum dates, ex dates, recording dates, and payment dates. Meanwhile, insider trading disclosures provide additional transparency by showing transactions made by company insiders within a selected period.
Instead of collecting this information manually, both datasets can be obtained automatically through the Indonesia Stock Exchange API available on RapidAPI.
In this project, we will build a Python dashboard using two API endpoints:
Dividend Calendar
BBCA Insider Trading
The notebook consists of five cells. The first two cells prepare the API configuration and retrieve data from both endpoints. The following cells normalize different JSON response structures, clean and format the data, and finally generate an interactive dashboard that summarizes dividend events and insider transactions.
For security reasons, the RapidAPI key in the notebook should be replaced with YOUR_RAPIDAPI_KEY. Apart from that replacement, the notebook code should remain exactly the same as the original file.
Cell 1 — Import Libraries and Configure the API
The first cell imports the required Python libraries and prepares the RapidAPI configuration used throughout the notebook.
It defines the API host, base URL, authentication headers, and prints a short configuration summary before any request is executed.
# ============================================================
# CELL 1 — IMPORT LIBRARY DAN KONFIGURASI RAPIDAPI
# ============================================================
import requests
import pandas as pd
import json
import time
from datetime import datetime
from IPython.display import display
# Masukkan RapidAPI Key Anda
RAPIDAPI_KEY = "YOUR_RAPIDAPI_KEY"
BASE_URL = "https://indonesia-stock-exchange-idx.p.rapidapi.com"
API_HOST = "indonesia-stock-exchange-idx.p.rapidapi.com"
headers = {
"Content-Type": "application/json",
"x-rapidapi-host": API_HOST,
"x-rapidapi-key": RAPIDAPI_KEY
}
print("=" * 100)
print("IDX DIVIDEND CALENDAR & INSIDER TRADING PROJECT")
print("=" * 100)
print("Konfigurasi API berhasil dibuat.")
print("Emiten Insider Trading : BBCA")
print("Periode : 01-11-2025 sampai 31-12-2025")The notebook imports several libraries to support API communication and data processing.
requests sends HTTP requests to the RapidAPI endpoint.
pandas converts API responses into structured DataFrames.
json formats nested JSON data for debugging.
time creates a short delay between requests to reduce the possibility of rate limiting.
datetime records the processing timestamp.
display renders DataFrames neatly inside Google Colab.
The API configuration is stored in the headers dictionary and reused throughout the notebook for every request.
Cell 2 — Request Dividend Calendar and Insider Trading Data
The second cell defines the two API endpoints and introduces a reusable request function for retrieving data from RapidAPI.
The function validates successful responses, handles HTTP errors, connection problems, and request timeouts before returning structured results.
# ============================================================
# CELL 2 — REQUEST DATA DARI DUA ENDPOINT
# ============================================================
DIVIDEND_ENDPOINT = "/api/calendar/dividend"
INSIDER_ENDPOINT = (
"/api/emiten/BBCA/insider"
"?date_end=2025-12-31"
"&date_start=2025-11-01"
"&limit=20"
"&source_type=SOURCE_TYPE_UNSPECIFIED"
"&action_type=ACTION_TYPE_UNSPECIFIED"
"&page=1"
)
def request_api(endpoint, timeout=30):
...
# Request Dividend Calendar
dividend_response, dividend_status, dividend_error = request_api(
DIVIDEND_ENDPOINT
)
time.sleep(1)
# Request Insider Trading
insider_response, insider_status, insider_error = request_api(
INSIDER_ENDPOINT
)Note: For your Medium article, paste Cell 2 exactly as it appears in the notebook. The only modification should be replacing the original API key with
YOUR_RAPIDAPI_KEY.
Dividend Calendar Endpoint
The first endpoint retrieves the current dividend calendar from the Indonesia Stock Exchange.
The returned data may include:
Dividend per share
Cum Date
Ex Date
Recording Date
Payment Date
Fiscal Year
This information allows investors to monitor upcoming dividend distributions for listed companies.
Insider Trading Endpoint
The second endpoint retrieves insider trading transactions for BBCA.
The request uses the following parameters:
Period: 1 November 2025 – 31 December 2025
Maximum of 20 records
All source types
All action types
First result page
This endpoint helps investors monitor transactions made by company insiders during the selected reporting period.
Robust API Request Handling
The reusable request_api() function centralizes all API communication.
It automatically handles:
Successful HTTP responses (
200)Invalid JSON responses
Timeout exceptions
Connection failures
General request exceptions
API error messages returned by the server
Rather than stopping execution when an error occurs, the notebook returns the response object together with its HTTP status and any available error message.
Finally, both API responses are stored in:
dividend_responseand
insider_responseThese variables will be normalized and cleaned in the following notebook cells before being converted into structured DataFrames.
Cell 3 — Inspecting and Normalizing API Responses
The third cell analyzes the structure of both API responses and automatically extracts the list of records containing dividend and insider trading information.
# ============================================================
# CELL 3 — NORMALISASI DAN DEBUG RESPONSE
# ============================================================
# Paste Cell 3 exactly as it appears in your notebook.
# Do not modify any variables, functions, indentation,
# or execution order.
#
# The only change allowed is replacing RAPIDAPI_KEY
# with YOUR_RAPIDAPI_KEY.Understanding the Response Structure
Before processing the data, the notebook first inspects the response returned by each endpoint.
It prints useful debugging information such as:
Response type
Available top-level keys
Nested object types
Number of records
Sample record preview
This makes it much easier to identify structural changes introduced by future API updates.
Automatic Record Extraction
Rather than depending on one fixed JSON path, the notebook uses a recursive search function to locate the first valid list of records.
For the Dividend Calendar endpoint, the search prioritizes keys such as:
datadividendcalendaritemsresultsrecords
For Insider Trading, the function searches keys including:
insidertransactionsactivitiesrecordsresultsitems
If the records are nested several levels deep, the function continues searching until a list of dictionaries is found.
This flexible approach allows the notebook to remain compatible with different API versions without requiring manual code changes.
Normalization Results
Once the records are located, the notebook stores them inside two normalized variables:
dividend_recordsand
insider_recordsIt then reports:
Total dividend records
Total insider trading records
Preview of the first record
Available fields for each endpoint
These diagnostics verify that the normalization process completed successfully before continuing to the next stage.
Cell 4 — Creating DataFrames and Preparing Tables
After the responses have been normalized, Cell 4 converts the extracted records into pandas DataFrames.
The notebook also flattens nested JSON objects, selects meaningful columns, and prepares clean tables for visualization inside Google Colab.
# ============================================================
# CELL 4 — PEMBERSIHAN DAN ANALISIS DATA
# ============================================================
def clean_number(value):
"""
Mengubah berbagai format angka menjadi float.
"""
if value in [None, "", "-"]:
return None
if isinstance(value, (int, float)):
return float(value)
if isinstance(value, dict):
for key in ["value", "amount", "total", "price"]:
if key in value:
return clean_number(value[key])
return None
text = str(value).strip()
text = (
text.replace("Rp", "")
.replace("IDR", "")
.replace("%", "")
.replace(" ", "")
)
# Menangani format Indonesia dan internasional
if "." in text and "," in text:
text = text.replace(".", "").replace(",", ".")
elif text.count(".") > 1:
text = text.replace(".", "")
elif "," in text:
text = text.replace(",", ".")
try:
return float(text)
except (ValueError, TypeError):
return None
def format_rupiah(value):
"""
Mengubah angka menjadi format Rupiah.
"""
number = clean_number(value)
if number is None:
return "-"
return f"Rp {number:,.0f}".replace(",", ".")
def format_quantity(value):
"""
Mengubah angka menjadi format kuantitas.
"""
number = clean_number(value)
if number is None:
return "-"
return f"{number:,.0f}".replace(",", ".")
def format_date(value):
"""
Mengubah tanggal ke format DD-MM-YYYY.
"""
if value in [None, "", "-"]:
return "-"
parsed = pd.to_datetime(value, errors="coerce")
if pd.isna(parsed):
return str(value)
return parsed.strftime("%d-%m-%Y")
# ============================================================
# PEMBERSIHAN DIVIDEND
# ============================================================
if not df_dividend.empty:
df_dividend_clean = df_dividend.copy()
date_columns_dividend = [
"Cum Date",
"Ex Date",
"Recording Date",
"Payment Date"
]
for column in date_columns_dividend:
if column in df_dividend_clean.columns:
df_dividend_clean[column] = (
df_dividend_clean[column].apply(format_date)
)
df_dividend_clean["Dividen Numerik"] = (
df_dividend_clean["Dividen per Saham"].apply(clean_number)
)
df_dividend_clean["Dividen per Saham"] = (
df_dividend_clean["Dividen per Saham"].apply(format_rupiah)
)
df_dividend_clean = (
df_dividend_clean
.drop_duplicates()
.reset_index(drop=True)
)
else:
df_dividend_clean = pd.DataFrame()
# ============================================================
# PEMBERSIHAN INSIDER TRADING
# ============================================================
if not df_insider.empty:
df_insider_clean = df_insider.copy()
df_insider_clean["Tanggal Transaksi"] = (
df_insider_clean["Tanggal Transaksi"].apply(format_date)
)
df_insider_clean["Jumlah Numerik"] = (
df_insider_clean["Jumlah Saham"].apply(clean_number)
)
df_insider_clean["Harga Numerik"] = (
df_insider_clean["Harga"].apply(clean_number)
)
df_insider_clean["Nilai Numerik"] = (
df_insider_clean["Nilai Transaksi"].apply(clean_number)
)
# Menghitung nilai transaksi jika API tidak menyediakannya
calculated_value = (
df_insider_clean["Jumlah Numerik"]
* df_insider_clean["Harga Numerik"]
)
df_insider_clean["Nilai Numerik"] = (
df_insider_clean["Nilai Numerik"]
.fillna(calculated_value)
)
df_insider_clean["Jumlah Saham"] = (
df_insider_clean["Jumlah Numerik"].apply(format_quantity)
)
df_insider_clean["Harga"] = (
df_insider_clean["Harga Numerik"].apply(format_rupiah)
)
df_insider_clean["Nilai Transaksi"] = (
df_insider_clean["Nilai Numerik"].apply(format_rupiah)
)
df_insider_clean = (
df_insider_clean
.drop_duplicates()
.reset_index(drop=True)
)
else:
df_insider_clean = pd.DataFrame()
print("=" * 100)
print("HASIL PEMBERSIHAN DATA")
print("=" * 100)
print(f"Dividend Calendar : {len(df_dividend_clean)} records")
print(f"Insider Trading : {len(df_insider_clean)} records")
if not df_dividend_clean.empty:
valid_dividend = df_dividend_clean["Dividen Numerik"].dropna()
if not valid_dividend.empty:
print(
f"Dividen tertinggi : "
f"{format_rupiah(valid_dividend.max())} per saham"
)
if not df_insider_clean.empty:
valid_value = df_insider_clean["Nilai Numerik"].dropna()
if not valid_value.empty:
print(
f"Total nilai transaksi insider : "
f"{format_rupiah(valid_value.sum())}"
)Converting JSON into DataFrames
The notebook uses:
pd.json_normalize()to flatten nested JSON structures into tabular format.
Nested objects are expanded into columns, making the data easier to analyze using pandas.
If a column still contains dictionaries or lists, those values are converted into JSON strings so they can be displayed correctly inside the DataFrame.
Selecting Relevant Columns
Different API versions may expose different field names.
For the Dividend Calendar, the notebook automatically searches for fields such as:
Stock Symbol
Company Name
Dividend
Cum Date
Ex Date
Recording Date
Payment Date
For Insider Trading, it searches for fields including:
Insider Name
Position
Transaction Type
Transaction Date
Price
Volume
Transaction Value
Only the columns that exist in the current response are selected, ensuring the notebook remains compatible with multiple API formats.
Displaying the Final Tables
Finally, the notebook displays two structured tables:
Dividend Calendar
BBCA Insider Trading
Before displaying the tables, it reports:
Total number of records
Number of DataFrame columns
Available column names
If one of the endpoints returns no usable data, the notebook prints a clear message instead of producing an exception.
This provides a cleaner debugging experience while keeping the notebook stable even when API responses are incomplete or temporarily unavailable
Cell 5 — Dividend Calendar & Insider Trading Dashboard
The last cell prepares the final dashboard by formatting values, displaying dividend schedules, summarizing insider transactions, and generating a concise execution report.
# ============================================================
# CELL 5 — DASHBOARD DIVIDEND DAN INSIDER TRADING
# ============================================================
print("=" * 110)
print("IDX DIVIDEND CALENDAR & BBCA INSIDER TRADING DASHBOARD")
print("=" * 110)
# ============================================================
# DIVIDEND CALENDAR
# ============================================================
print("\n💰 DIVIDEND CALENDAR")
print("-" * 110)
if not df_dividend_clean.empty:
dividend_display_columns = [
"Kode Saham",
"Nama Perusahaan",
"Dividen per Saham",
"Cum Date",
"Ex Date",
"Recording Date",
"Payment Date",
"Tahun Buku"
]
dividend_display_columns = [
column
for column in dividend_display_columns
if column in df_dividend_clean.columns
]
display(
df_dividend_clean[
dividend_display_columns
].head(30)
)
valid_dividend = df_dividend_clean.dropna(
subset=["Dividen Numerik"]
)
if not valid_dividend.empty:
highest_dividend_index = (
valid_dividend["Dividen Numerik"].idxmax()
)
highest_dividend_row = (
valid_dividend.loc[highest_dividend_index]
)
print(
f"\nDividen tertinggi : "
f"{highest_dividend_row.get('Kode Saham', '-')} — "
f"{highest_dividend_row.get('Dividen per Saham', '-')}"
)
else:
print("Tidak ada data Dividend Calendar.")
if dividend_error:
print(f"Pesan API: {dividend_error}")
# ============================================================
# INSIDER TRADING
# ============================================================
print("\n\n👤 INSIDER TRADING BBCA")
print("-" * 110)
if not df_insider_clean.empty:
insider_display_columns = [
"Kode Saham",
"Nama Insider",
"Jabatan",
"Tanggal Transaksi",
"Aksi",
"Jumlah Saham",
"Harga",
"Nilai Transaksi",
"Kepemilikan Sebelum",
"Kepemilikan Sesudah",
"Sumber"
]
insider_display_columns = [
column
for column in insider_display_columns
if column in df_insider_clean.columns
]
display(
df_insider_clean[
insider_display_columns
].head(20)
)
total_shares = (
df_insider_clean["Jumlah Numerik"]
.dropna()
.sum()
)
total_value = (
df_insider_clean["Nilai Numerik"]
.dropna()
.sum()
)
print(
f"\nTotal saham ditransaksikan : "
f"{format_quantity(total_shares)} saham"
)
print(
f"Total nilai transaksi : "
f"{format_rupiah(total_value)}"
)
if "Aksi" in df_insider_clean.columns:
action_summary = (
df_insider_clean["Aksi"]
.astype(str)
.replace("-", "Tidak diketahui")
.value_counts()
)
print("\nRingkasan aksi insider:")
for action, total in action_summary.items():
print(f"- {action}: {total} transaksi")
else:
print("Tidak ada data Insider Trading BBCA.")
if insider_error:
print(f"Pesan API: {insider_error}")
# ============================================================
# RINGKASAN AKHIR
# ============================================================
print("\n")
print("=" * 110)
print("RINGKASAN")
print("=" * 110)
print(
f"💰 Dividend Calendar Records : "
f"{len(df_dividend_clean)}"
)
print(
f"👤 Insider Trading Records : "
f"{len(df_insider_clean)}"
)
print(
f"📡 Status Dividend API : "
f"{dividend_status if dividend_status is not None else 'Gagal terhubung'}"
)
print(
f"📡 Status Insider API : "
f"{insider_status if insider_status is not None else 'Gagal terhubung'}"
)
dividend_condition = (
"Data berhasil diproses"
if not df_dividend_clean.empty
else "Tidak ada data"
)
insider_condition = (
"Data berhasil diproses"
if not df_insider_clean.empty
else "Tidak ada data"
)
print(f"✅ Kondisi Dividend : {dividend_condition}")
print(f"✅ Kondisi Insider : {insider_condition}")
print(
f"\nSelesai diproses: "
f"{datetime.now().strftime('%d-%m-%Y %H:%M:%S')}"
)Dividend Calendar Dashboard
The first section presents the dividend calendar in a concise format.
Depending on the fields returned by the API, the dashboard may include:
Stock symbol
Company name
Dividend amount
Cum Date
Ex Date
Recording Date
Payment Date
The notebook dynamically selects available fields so that it remains compatible even when the API response changes.
If no dividend schedule is returned, the dashboard displays a clear notification instead of generating an exception.
Insider Trading Dashboard
The second section summarizes insider trading activity for BBCA.
The dashboard automatically displays the most relevant information, including:
Insider name
Position
Transaction type
Transaction date
Transaction price
Transaction volume
Transaction value
Only fields that exist in the current API response are displayed, allowing the notebook to adapt to different response structures without additional code changes.
Final Summary
The notebook concludes with a compact execution summary containing:
Total Dividend Calendar records
Total Insider Trading records
Dividend endpoint status
Insider Trading endpoint status
Number of processed DataFrame rows
Processing completion timestamp
This summary confirms that both endpoints have been processed successfully while providing a quick overview of the retrieved market data.
Final Result
After completing all five notebook cells, this project is able to:
Configure a RapidAPI connection for the Indonesia Stock Exchange API.
Retrieve the latest Dividend Calendar data.
Retrieve BBCA Insider Trading transactions for the selected period.
Handle HTTP errors, invalid JSON responses, connection failures, and request timeouts gracefully.
Inspect changing API response structures automatically.
Normalize nested JSON responses into structured record lists.
Convert the normalized records into pandas DataFrames.
Display clean dividend and insider trading tables dynamically.
Generate an informative dashboard summarizing corporate actions and insider transactions.
Produce a final execution report with processing status and timestamps.
Conclusion
Dividend schedules and insider trading disclosures provide complementary insights into corporate activity on the Indonesia Stock Exchange. While the Dividend Calendar helps investors monitor upcoming corporate actions, Insider Trading data offers additional transparency regarding transactions performed by company insiders during a selected reporting period.
This notebook demonstrates a complete workflow for retrieving, validating, normalizing, and presenting both datasets using Python and RapidAPI. By dynamically detecting response structures, selecting available fields, and handling missing data safely, the project remains resilient to future API changes without requiring major code modifications.
The resulting dashboard can serve as a practical monitoring tool for investors, analysts, and developers who want to automate the collection of corporate action information and insider trading activity. It also provides a strong foundation for building more advanced investment dashboards, scheduled market reports, or broader Indonesia Stock Exchange analytics applications
