# Apify Indonesian Cosmetics Inteligence (`endru_e/apify-indonesian-cosmetics-inteligence`) Actor

- **URL**: https://apify.com/endru\_e/apify-indonesian-cosmetics-inteligence.md
- **Developed by:** [Andrew E](https://apify.com/endru_e) (community)
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

Pay per usage

This Actor is paid per platform usage. The Actor is free to use, and you only pay for the Apify platform usage, which gets cheaper the higher subscription plan you have.

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

## 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

## Indonesian Cosmetics Intelligence — Apify Actor

MVP Actor for **targeted cosmetics intelligence from the public BPOM Cek Produk registry**.

Instead of attempting to scrape the entire cosmetics registry on every run, this Actor is designed for recurring watchlists such as:

- monitor selected cosmetics brands;
- monitor selected registrants / companies;
- search selected products or BPOM registration numbers;
- watch composition / ingredient keywords supported by BPOM search;
- compare the result with the previous run and classify records as `NEW`, `CHANGED`, or `UNCHANGED`.

> Source: Badan Pengawas Obat dan Makanan Republik Indonesia (BPOM), public Cek Produk website. This Actor is an independent integration and is not affiliated with or endorsed by BPOM.

### What this MVP does

1. Opens the current **Produk Kosmetika** page with Playwright.
2. Uses the visible BPOM filter UI instead of relying on undocumented private endpoints.
3. Scrapes list results and, optionally, attempts to open the product detail dialog.
4. Normalizes results into a stable JSON structure.
5. Deduplicates products by BPOM registration number (NIE).
6. Optionally compares records with a persistent snapshot stored in a named Apify Key-Value Store.
7. Outputs records to the default Apify Dataset.

### Example input

```json
{
  "brands": ["SOMETHINC", "SKINTIFIC"],
  "registrants": [],
  "productNames": [],
  "registrationNumbers": [],
  "compositions": [],
  "maxItemsPerQuery": 100,
  "maxPagesPerQuery": 20,
  "includeDetails": true,
  "detectChanges": true,
  "emit": "changes",
  "stateStoreName": "indonesian-cosmetics-intelligence-state",
  "stateKey": "competitor-watchlist",
  "requestDelayMs": 500
}
```

### Example output

```json
{
  "eventType": "NEW",
  "registrationNumber": "NA18260123456",
  "productName": "Example Brightening Serum",
  "brand": "EXAMPLE",
  "registrant": "EXAMPLE COSMETICS, PT",
  "packaging": "Botol 20 mL",
  "dosageForm": "Cairan",
  "composition": "...",
  "issuedDate": "2026-08-28",
  "expiryDate": "2029-08-28",
  "status": "Aktif",
  "category": "Kosmetika",
  "source": "BPOM RI - Cek Produk",
  "sourceUrl": "https://cekbpom.pom.go.id/produk-kosmetika",
  "matchedBy": {
    "type": "brand",
    "value": "EXAMPLE"
  },
  "detectedAt": "2026-09-02T05:00:00.000Z",
  "changedFields": []
}
```

Actual detail fields depend on what the BPOM interface exposes for the record at runtime. The Actor keeps list-level fields even when the detail dialog cannot be parsed.

### Change detection

When `detectChanges` is enabled, the Actor stores a snapshot in the named Key-Value Store:

```text
SNAPSHOT_<stateKey>
```

On the next successful run:

- record absent from previous snapshot → `NEW`;
- same NIE, different normalized fingerprint → `CHANGED`;
- same fingerprint → `UNCHANGED`.

The MVP deliberately **does not mark a missing result as REMOVED/REVOKED**. A product may disappear from a query because of filtering, pagination limits, or a temporary source issue; calling that a regulatory removal would be unsafe. A future version should implement removal detection only with a verified complete snapshot strategy.

If any query job fails, the snapshot is not replaced. This prevents a partial failed run from becoming the next baseline.

### Why Playwright?

The current BPOM product table is rendered dynamically. This Actor interacts with the public UI rather than hard-coding an undocumented internal endpoint. The tradeoff is higher compute use, but the MVP is easier to understand and more resilient to backend endpoint changes.

### Deploy to Apify

Install the Apify CLI, log in, and from this folder run:

```bash
apify push
```

You can also create an Actor in Apify Console and upload/push this repository.

### Run locally

Requirements: Docker or a local Node.js environment with a compatible Playwright browser.

With Apify CLI:

```bash
apify run
```

The sample input is included in:

```text
storage/key_value_stores/default/INPUT.json
```

### Production recommendations before publishing to Apify Store

- Start with low `maxItemsPerQuery` and one browser concurrency.
- Validate the BPOM site's applicable access/use rules before commercial scale-up.
- Add a proxy strategy only when legitimately needed; do not use it to defeat access controls.
- Add monitoring/alerts for selector changes.
- Add tests based on saved, legally obtained HTML fixtures.
- Keep `includeDetails=false` for high-volume list discovery and run a second enrichment pass only for new records.
- Consider a two-stage architecture for lower cost: discovery → detail enrichment.

### Suggested roadmap

#### v0.1 — included here

- BPOM cosmetics watchlists
- brand / registrant / product / NIE / composition queries
- structured dataset
- detail-dialog enrichment (best effort)
- persistent change detection

#### v0.2

- BPOM Public Warning / dangerous cosmetics feed
- explicit `WARNING_ADDED` events
- webhook-friendly compact event dataset

#### v0.3

- scheduled competitor watch presets
- richer trend aggregation (registrations per brand / month)
- separate `discover` and `enrich` modes to reduce browser cost

#### v1.0 — Indonesian Cosmetics Intelligence

- BPOM registry + BPOM public warnings
- marketplace product/pricing connectors where permitted
- cross-source product/entity resolution
- competitor launch radar
- market trend aggregates
- API-friendly product intelligence layer

### Important scope note

A BPOM registration record is regulatory registry data; it should not be represented as a guarantee that a marketplace listing is genuine, safe for a specific person, or identical to the registered item. Matching a listing to BPOM data requires additional product-identity checks.

# Changelog

This Actor's version history is a separate document: https://apify.com/endru\_e/apify-indonesian-cosmetics-inteligence/changelog.md

# Actor input Schema

## `brands` (type: `array`):

Optional cosmetics brands to monitor in BPOM. Each brand is queried separately. Leave empty when monitoring by another criterion.

## `registrants` (type: `array`):

Optional BPOM registrant or company names to monitor.

## `productNames` (type: `array`):

Optional product-name searches.

## `registrationNumbers` (type: `array`):

Optional BPOM registration numbers to monitor.

## `compositions` (type: `array`):

Optional composition or ingredient keywords supported by the BPOM filter.

## `maxItemsPerQuery` (type: `integer`):

0 = no item limit. For production monitoring, keep this at 0 so the complete matching result set is checked.

## `maxPagesPerQuery` (type: `integer`):

Safety cap for BPOM pagination. Increase this if a monitored brand has more results than the current cap can cover.

## `detailStrategy` (type: `string`):

Controls when the Actor opens BPOM product detail. changesOnly is recommended for routine monitoring; staleOnly is recommended for periodic detail refresh.

## `detailMaxAgeDays` (type: `integer`):

Used by staleOnly. Product details are refreshed when their last successful detail fetch is at least this many days old. Recommended: 30.

## `detailFetchLimitPerRun` (type: `integer`):

Used by staleOnly to cap successful detail attempts requested in one run. 0 = unlimited. Use this to spread stale-detail refresh cost across multiple runs.

## `baselineWarmupRuns` (type: `integer`):

Number of successful full-coverage runs used to build a trusted union baseline before NEW alerts are enabled. Recommended: 3.

## `newProductWindowDays` (type: `integer`):

After the trusted baseline is ready, a first-seen product is classified as NEW only when its BPOM issued date is within this many days. Older or unknown dates are classified as DISCOVERED.

## `detectChanges` (type: `boolean`):

Compare current records with a persistent snapshot. When disabled, records are emitted as SNAPSHOT events.

## `emit` (type: `string`):

all = every record; changes = trusted NEW or CHANGED records only; new = trusted NEW only. BASELINE and DISCOVERED records are visible with all but do not trigger changes output.

## `stateStoreName` (type: `string`):

Named Apify Key-Value Store used to persist monitoring snapshots.

## `stateKey` (type: `string`):

Unique key for this watchlist. Keep the same key across scheduled runs. Use a new key when starting a new independent trusted baseline.

## `requestDelayMs` (type: `integer`):

Delay between BPOM interactions to reduce request pressure on the public source.

## `allowPartialSnapshotUpdate` (type: `boolean`):

Advanced. If enabled, the Actor may update stored observations even when full result coverage was not achieved. Partial runs never advance trusted-baseline readiness. Keep disabled for production monitoring.

## `debug` (type: `boolean`):

Enable verbose diagnostics.

## Actor input object example

```json
{
  "brands": [],
  "registrants": [],
  "productNames": [],
  "registrationNumbers": [],
  "compositions": [],
  "maxItemsPerQuery": 0,
  "maxPagesPerQuery": 100,
  "detailStrategy": "changesOnly",
  "detailMaxAgeDays": 30,
  "detailFetchLimitPerRun": 0,
  "baselineWarmupRuns": 3,
  "newProductWindowDays": 30,
  "detectChanges": true,
  "emit": "changes",
  "stateStoreName": "indonesian-cosmetics-intelligence-state",
  "stateKey": "default",
  "requestDelayMs": 800,
  "allowPartialSnapshotUpdate": false,
  "debug": false
}
```

# Actor output Schema

## `dataset` (type: `string`):

No description

# 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 = {};

// Run the Actor and wait for it to finish
const run = await client.actor("endru_e/apify-indonesian-cosmetics-inteligence").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 = {}

# Run the Actor and wait for it to finish
run = client.actor("endru_e/apify-indonesian-cosmetics-inteligence").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 '{}' |
apify call endru_e/apify-indonesian-cosmetics-inteligence --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,endru_e/apify-indonesian-cosmetics-inteligence"
        }
    }
}
```

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/njUig85HqArrplsnn/builds/zUBi2MvHX47b7Fy2b/openapi.json
