# Indonesia BPOM Registered Products (cekbpom) (`a-g-d/id-bpom`) Actor

Products registered with Indonesia's food and drug agency BPOM (cekbpom.pom.go.id) as English-keyed JSON: registration number (NIE), product, brand, class, status, approval and expiry dates, registrant, importer and ingredients. Cosmetics, food, medicines, supplements and more.

- **URL**: https://apify.com/a-g-d/id-bpom.md
- **Developed by:** [Aloever Dulay](https://apify.com/a-g-d) (community)
- **Categories:** E-commerce, Other
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $1.00 / 1,000 product registrations

This Actor is paid per event. You are not charged for the Apify platform usage, but only a fixed price for specific events.

Learn more: https://docs.apify.com/actors/running/actors-in-store.md#pay-per-event

## What's an Apify Actor?

An Actor is a serverless cloud program that runs on the Apify platform. It has two run modes.
In Batch mode, an Actor accepts a well-defined JSON input, performs an action which can take anything from a few seconds to a few hours,
and optionally produces a well-defined JSON output, datasets with results, or files in key-value store.
In Standby mode, an Actor provides a web server which can be used as a website, API, or an MCP server.

Apify vocabulary and the platform model are defined once, in the agent quickstart at https://apify.com/agents.md.

## How to integrate an Actor?

If asked about integration, you help developers integrate Actors into their projects.
You adapt to their stack and deliver integrations that are safe, well-documented, and production-ready.

Do not guess an integration path. Every one of them is in the agent quickstart at https://apify.com/agents.md: the Apify MCP server, Agent Skills with the Apify CLI, the JavaScript and Python clients, the REST API, and the account-free path for an agent with no human to sign in. It also carries the rule on stating cost before the first paid run.

For examples already wired to this Actor's own input schema, see the [API](#api) section below.

Each client library has reference documentation the quickstart does not restate: [JavaScript/TypeScript](https://docs.apify.com/api/client/js/docs.md) (`npm install apify-client`) and [Python](https://docs.apify.com/api/client/python/docs.md) (`pip install apify-client`).

# README

## Indonesia BPOM Registered Products Actor (cekbpom.pom.go.id)

Every product registered or notified with Indonesia's food and drug agency, **BPOM**
(Badan Pengawas Obat dan Makanan), from its public search at
[cekbpom.pom.go.id](https://cekbpom.pom.go.id), as clean, English-keyed JSON: registration
number (NIE), product, brand, packaging, form, class, status, submission, approval and expiry
dates, registrant, importer, ingredients where published, e-label leaflets for medicines and
a link to the product's detail card.

Classes: cosmetics (about 372,000 notifications), processed food (about 250,000), medicines
(about 25,000), traditional medicines (about 21,000, with the jamu, phytopharmaceutical,
standardised-herbal and other-natural sub-lists), health supplements (about 4,900) and
quasi-drugs (about 1,300), or all of them at once (about 674,000). Counts from 2026-09-26.

### Who needs this

- **Marketplaces and sellers** checking that listed cosmetics, food and supplements carry a
  valid BPOM number before they go live, or auditing a catalogue in bulk.
- **Brands, distributors and importers** watching competitors' new approvals, or their own
  registrations that are about to expire.
- **Regulatory and market-entry consultants** sizing a category: who registers what, how
  many new products a month, which companies import.
- **Researchers and journalists** studying Indonesia's consumer-product market.
- **AI agents and automations** that need a stable schema rather than a jQuery DataTables
  page in Indonesian.

### Input

| Field                | Type    | Default       | Description                                                                                                                                                                                                    |
| -------------------- | ------- | ------------- | -------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `productClass`       | string  | `"cosmetics"` | `cosmetics`, `processed-food`, `medicines`, `traditional-medicines`, `jamu`, `phytopharmaceuticals`, `standardised-herbal-medicines`, `other-natural-medicines`, `health-supplements`, `quasi-drugs` or `all`. |
| `productName`        | string  | `""`          | Product name contains this text (case-insensitive), e.g. `serum`, `paracetamol`.                                                                                                                               |
| `brand`              | string  | `""`          | Brand (merk) contains this text, e.g. `wardah`.                                                                                                                                                                |
| `registrationNumber` | string  | `""`          | Registration number contains this text: a full NIE (`NA18261206575`) or a prefix (`NA1826`).                                                                                                                   |
| `registrant`         | string  | `""`          | Registrant's company name contains this text. The site calls this filter "manufacturer" (`manufacturer_name`), but it matches the registrant (pendaftar).                                                      |
| `status`             | string  | `""` (any)    | Exact status, e.g. `Berlaku`, `Berlaku (Khusus Ekspor)`, `Berlaku (dengan Mencabut Produk)`. See [Statuses](#statuses).                                                                                        |
| `approvedSince`      | string  | `""`          | Issued on or after this date (`YYYY-MM-DD`).                                                                                                                                                                   |
| `approvedUntil`      | string  | `""`          | Issued on or before this date (`YYYY-MM-DD`).                                                                                                                                                                  |
| `expiringBefore`     | string  | `""`          | Validity ends between today (Jakarta time) and this date (`YYYY-MM-DD`): registrations about to expire.                                                                                                        |
| `maxItems`           | integer | `100`         | Stop after this many registrations (1-100,000), newest approval first.                                                                                                                                         |
| `proxyConfiguration` | object  | no proxy      | Optional Apify Proxy, if the site is unreachable from Apify's network.                                                                                                                                         |

Text filters are case-insensitive "contains" matches done by the site. Filters combine with
AND.

**Order.** The site always returns registrations newest approval (`approvedAt`) first and
ignores sort parameters, so an empty query gives the newest approvals, and a daily run with
`approvedSince` set to yesterday gives the day's new registrations.

#### Statuses

The status filter matches exactly. Values the site's form offers or its data showed on
2026-09-26: `Berlaku` (valid), `Berlaku (Bulk)`, `Berlaku (Khusus Ekspor)` (export only),
`Berlaku (dengan Mencabut Produk)` (valid, replaces an earlier registration),
`Berlaku (dengan Mencabut Produk sesuai revisionId)`, and four `Tidak Berlaku (…)` (no longer
valid) reasons. Every record sampled while building this actor was a valid (`Berlaku…`)
registration: the public list appears to show only those.

### Output

One dataset item per registration, newest approval first. Keys are English; product names,
addresses, forms and statuses stay in Indonesian exactly as published (HTML entities
decoded, surrounding spaces trimmed, placeholders such as `-` and `N/A` turned into `null`).
Dates are ISO dates (`YYYY-MM-DD`): the site publishes calendar dates without a time, in
Western Indonesia Time (WIB, UTC+7). `fetchedAt` is a UTC timestamp. The Output tab on this page documents every field.

A real item (cosmetics, captured 2026-09-26):

```json
{
  "registrationNumber": "NA11261301617",
  "productId": "MAZA-3583675460",
  "productName": "Staylock Lip Matte 14   Peony",
  "brand": "WARDAH",
  "packaging": "Botol, Dus 2.7 g",
  "dosageForm": "Cairan Kental",
  "productClass": { "id": "12", "name": "Cosmetics", "nameId": "Kosmetika" },
  "productSubclass": null,
  "category": "Kosmetika",
  "status": "Berlaku",
  "submittedAt": "2026-09-20",
  "approvedAt": "2026-09-20",
  "expiresAt": "2029-09-19",
  "updatedAt": "2026-09-20",
  "registrant": {
    "name": "PARAMA GLOBAL INSPIRA, PT",
    "address": "Jl. Ciledug Raya No. 10 RT. 018 RW. 003, Kel. Ulujami, Kec. Pesanggrahan",
    "district": "Kota Adm Jakarta Selatan",
    "province": "Dki Jakarta",
    "country": "INDONESIA"
  },
  "importer": null,
  "ingredients": null,
  "atcCode": null,
  "halalCertificate": null,
  "eLabel": null,
  "riskLevel": null,
  "documentCategory": null,
  "replacesProductId": null,
  "registrationSystem": "Notifikasi Kosmetika",
  "issuingDirectorate": "Direktorat Registrasi Obat Tradisional, Suplemen Kesehatan, dan Kosmetik",
  "detailUrl": "https://cekbpom.pom.go.id/produk/MAZA-3583675460/01/detail",
  "sourceUrl": "https://cekbpom.pom.go.id/produk-kosmetika",
  "fetchedAt": "2026-09-26T10:04:43.140Z"
}
```

What varies by class:

- **Medicines** carry `ingredients` (active substances) and `atcCode`, and many carry
  `eLabel` links to the patient and professional leaflets (PDFs on ttac.pom.go.id).
- **Traditional medicines, supplements, quasi-drugs** carry `ingredients` (the site
  separates them; the actor returns a list) and often an `importer`.
- **Processed food** carries `importer` for imports (`ML…` numbers), `riskLevel` and
  `documentCategory`; the list publishes no ingredients.
- **Cosmetics** lists publish no ingredients.
- **Sub-lists** (`jamu`, `phytopharmaceuticals`, `standardised-herbal-medicines`,
  `other-natural-medicines`): records say class 10; `productSubclass` records which sub-list
  the run queried.

#### Manufacturer

The site's list data has "manufacturer" columns, but in every record sampled (over 8,000
across the classes) they repeat the registrant. The actual manufacturer appears only on the
product's detail card (`detailUrl`), which costs one more request per product at the 5 s
crawl delay, so this version does not fetch it and does not output a `manufacturer` field
rather than output a misleading one. `registrant` is the company holding the registration.

### Data and privacy

The actor publishes company and product data that BPOM publishes for consumer protection.
It **drops two fields the site sends**:

- `USER_NAME`: the name of the BPOM officer who processed the registration. Not needed to
  identify the product, and a private person's name.
- `REGISTRAR_NPWP`: the registrant's tax id. Since 2024 an individual's NPWP is their
  16-digit national identity number (NIK), and micro-business registrants are often
  individuals, so the actor does not output it in any form, not even hashed.

Both are dropped when the response is parsed: the parser keeps an explicit list of fields,
and the tests check that no output item contains either field or any part of its value.
Indonesia's personal data protection law (UU 27/2022) applies to personal data in what you
store: some registrants are sole proprietors whose company name is their own name and
whose address may be a home address. Use the data for the purpose it is published for
(checking products and their registrations) and do not profile individuals.

### Pricing

Pay per event:

| Event            | Charged for                              |
| ---------------- | ---------------------------------------- |
| `product-record` | Each registration stored in the dataset. |

Apify's standard run-start event covers starting the actor. Records are stored first and
charged after, so you never pay for data you did not receive. When your spending limit runs
out, the run stops cleanly and never stores an unpaid record.

### Data source and politeness

- **Source:** the same JSON endpoint the site's own list pages use: a jQuery DataTables
  `POST https://cekbpom.pom.go.id/produk-dt/<class>` (the traditional-medicine sub-lists use
  `/produk-dt-oba/<id>`), after one `GET https://cekbpom.pom.go.id/` that sets the session
  cookies and the CSRF token (`<meta name="csrf-token">`, sent as `X-CSRF-TOKEN`). No login,
  no captcha.
- **robots.txt** (checked 2026-09-26) allows every user agent (`Disallow:` empty) with
  `Crawl-delay: 5`, disallows `/login`, `/api/`, `/storage/`, `/produk-public-warning/`,
  `/import/`, `/export/` and a few more, and blocks named AI crawlers. The actor
  - waits **at least 5 seconds** after each request to the site ends before the next one
    starts (the session page, retries and renewals included; a constant, not an input),
  - uses **one connection**, one request at a time,
  - identifies itself honestly as
    `id-bpom-actor/0.1 (+https://apify.com/a-g-d/id-bpom)`,
  - never requests a disallowed path (the client refuses them),
  - asks for **5,000 records per request** (the site answered 5,000-row pages in about 1 s),
    so the delay costs little: 100,000 records take 20 data requests.
- **Session:** if the site refuses a request with HTTP 419/401/403 (for example an expired
  session), the actor starts a fresh session once and retries; a second refusal stops the
  run with a clear message.
- **Resilience:** the host sometimes fails DNS resolution from datacenter networks. The
  actor asks public resolvers (1.1.1.1, 8.8.8.8) when the system resolver fails or takes
  more than 5 s, reuses the last good address within a run, and retries DNS errors,
  connection resets, timeouts and 5xx with exponential backoff and jitter inside a run-wide
  budget (90 s of backoff), well inside the default 300 s timeout. An HTTP 200 without the
  expected `data` array is treated as an error.
- **Terms:** no terms of use are linked from cekbpom.pom.go.id; pom.go.id publishes a
  privacy policy only.

### Limitations

- **No manufacturer** (see [Manufacturer](#manufacturer)).
- **No halal certificate data in practice.** The list has halal-certificate columns; they
  were empty in every record sampled. The actor outputs `halalCertificate` when they are
  filled, `null` otherwise.
- **A run is a snapshot.** New approvals that arrive while a long run pages through the list
  shift it; the actor skips rows it has already stored (`duplicates` in the run log), and
  approvals that arrive mid-run belong to the next run.
- **Slow filters.** Some filter combinations make the site slow (a lone "expiry before"
  date took 40 s, which is why `expiringBefore` is bounded at today). Requests time out after
  60 s and are retried.
- **Indonesian text.** Names, forms and statuses are published in Indonesian only; the
  actor translates keys and class names, not text.

# Changelog

This Actor's version history is a separate document: https://apify.com/a-g-d/id-bpom/changelog.md

# Actor input Schema

## `productClass` (type: `string`):

Which of BPOM's product lists to search. Results come newest approval first. "All classes" searches every list at once.

## `productName` (type: `string`):

Only products whose name contains this text (case-insensitive), e.g. "serum" or "paracetamol". Leave empty for all.

## `brand` (type: `string`):

Only products whose brand (merk) contains this text (case-insensitive), e.g. "wardah". Leave empty for all.

## `registrationNumber` (type: `string`):

Only registrations whose number contains this text, e.g. a full NIE such as "NA18261206575" or a prefix such as "NA1826". Leave empty for all.

## `registrant` (type: `string`):

Only registrations held by a company whose name contains this text, e.g. "paragon". The site calls this filter "manufacturer" but matches the registrant (pendaftar). Leave empty for all.

## `status` (type: `string`):

Only registrations with exactly this status. The public list showed only valid (Berlaku) registrations when this actor was built. Leave on "Any status" for all.

## `approvedSince` (type: `string`):

Only registrations issued on or after this date (YYYY-MM-DD). Combine with "Approved until" for a window, or use alone to monitor new approvals.

## `approvedUntil` (type: `string`):

Only registrations issued on or before this date (YYYY-MM-DD).

## `expiringBefore` (type: `string`):

Only registrations whose validity ends between today (Jakarta time) and this date (YYYY-MM-DD), for monitoring registrations that are about to expire.

## `maxItems` (type: `integer`):

Stop after storing this many registrations (newest approval first). The site is read politely, one request every 5 seconds, 5,000 records per request.

## `proxyConfiguration` (type: `object`):

Optional. cekbpom.pom.go.id is sometimes unreachable from datacenter networks; if runs fail with connection or DNS errors, try Apify Proxy.

## Actor input object example

```json
{
  "productClass": "cosmetics",
  "productName": "",
  "brand": "",
  "registrationNumber": "",
  "registrant": "",
  "status": "",
  "approvedSince": "",
  "approvedUntil": "",
  "expiringBefore": "",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `products` (type: `string`):

One item per product registration matching the input, newest approval first: registration number (NIE), product name, brand, packaging, form, class, status, submission/approval/expiry dates (ISO dates, WIB), registrant, importer, ingredients where published, e-label links for medicines, and a link to the detail card.

# API

You can run this Actor programmatically using our API. Below are code examples in JavaScript, Python, and CLI, as well as the OpenAPI specification and MCP server setup.

## JavaScript example

```javascript
import { ApifyClient } from 'apify-client';

// Initialize the ApifyClient with your Apify API token
// Replace the '<YOUR_API_TOKEN>' with your token
const client = new ApifyClient({
    token: '<YOUR_API_TOKEN>',
});

// Prepare Actor input
const input = {
    "productClass": "cosmetics",
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("a-g-d/id-bpom").call(input);

// Fetch and print Actor results from the run's dataset (if any)
console.log('Results from dataset');
console.log(`💾 Check your data here: https://console.apify.com/storage/datasets/${run.defaultDatasetId}`);
const { items } = await client.dataset(run.defaultDatasetId).listItems();
items.forEach((item) => {
    console.dir(item);
});

// 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/js/docs

```

## Python example

```python
from apify_client import ApifyClient

# Initialize the ApifyClient with your Apify API token
# Replace '<YOUR_API_TOKEN>' with your token.
client = ApifyClient("<YOUR_API_TOKEN>")

# Prepare the Actor input
run_input = {
    "productClass": "cosmetics",
    "maxItems": 20,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("a-g-d/id-bpom").call(run_input=run_input)

# Fetch and print Actor results from the run's dataset (if there are any)
print(f"💾 Check your data here: https://console.apify.com/storage/datasets/{run.default_dataset_id}")
for item in client.dataset(run.default_dataset_id).iterate_items():
    print(item)

# 📚 Want to learn more 📖? Go to → https://docs.apify.com/api/client/python/docs/quick-start

```

## CLI example

```bash
echo '{
  "productClass": "cosmetics",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call a-g-d/id-bpom --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,a-g-d/id-bpom"
        }
    }
}
```

The hosted server signs you in with OAuth on first connect, so no API token belongs in this config. Clients without OAuth support can send an `Authorization: Bearer <APIFY_API_TOKEN>` header instead, using a token from API & Integrations in Apify Console (https://console.apify.com/settings/integrations).

## OpenAPI specification

Download the OpenAPI definition: https://api.apify.com/v2/actors/aOFgJd7mc5ghehj0C/builds/UHyBLvDeXmGOD2QSD/openapi.json
