# Kenya PPB Registered Products (Medicines, Devices) (`a-g-d/ke-ppb`) Actor

Kenya Pharmacy and Poisons Board public registers as English-keyed JSON: registered medicines, food supplements, medical devices and herbals with registration number, product, active ingredients, form, country, holder, local representative and ISO registration and expiry dates.

- **URL**: https://apify.com/a-g-d/ke-ppb.md
- **Developed by:** [Aloever Dulay](https://apify.com/a-g-d) (community)
- **Categories:** E-commerce, Other
- **Security:** Limited permissions
- **Stats:** 3 total users, 0 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

## Kenya PPB Registered Products Actor (Medicines, Food, Devices, Herbals)

The public product registers of the **Pharmacy and Poisons Board (PPB) of Kenya**, read
from [products.pharmacyboardkenya.org](https://products.pharmacyboardkenya.org/ppb_admin/pages/public_view_retention_products.php),
as clean, English-keyed JSON: registration number, product name, active ingredients,
dosage form, country of origin, marketing authorisation holder, local technical
representative or manufacturer, and ISO registration and expiry dates.

Four registers, about 17,300 rows on 2026-10-11:

| `list`      | Register                                           | Rows   |
| ----------- | -------------------------------------------------- | ------ |
| `medicines` | Registered medicines (the retention register)      | 3,273  |
| `food`      | Food supplements and nutritional products          | 12,754 |
| `devices`   | Retained medical devices (the 2026 retention list) | 1,246  |
| `herbals`   | Registered herbal products                         | 21     |

### Who needs this

- **Pharmacies, distributors, importers and e-pharmacies** checking that a product is
  registered in Kenya, who holds the registration and when it expires.
- **Pharmaceutical and device companies and their local technical representatives**
  watching new registrations, competitors' portfolios and upcoming expiries.
- **Hospitals, insurers, procurement and tender teams** verifying products and suppliers
  against the regulator's register.
- **Regulatory-affairs consultants, researchers and journalists** studying what is
  registered in Kenya, from which countries and by which companies.
- **AI agents and automations** that need a stable schema instead of a paged admin grid.

### Input

| Field                | Type    | Default       | Description                                                                                                                                                                                |
| -------------------- | ------- | ------------- | ------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------ |
| `list`               | string  | `"medicines"` | `medicines`, `food`, `devices` or `herbals`.                                                                                                                                               |
| `productName`        | string  | `""`          | Trade name contains this text, any case (`panadol`, `oximeter`).                                                                                                                           |
| `activeIngredient`   | string  | `""`          | Active ingredient column contains this text (`cefixime`). Medicines and food.                                                                                                              |
| `registrationNumber` | string  | `""`          | Registration number contains this text (`H2026/CTD13447`, `MD/2026/001`). Medicines and devices.                                                                                           |
| `company`            | string  | `""`          | Part of a company name: holder or local technical representative (medicines), holder or manufacturing site (food), manufacturer or local representative (devices), manufacturer (herbals). |
| `country`            | string  | `""`          | Country of origin contains this text (`India`). Medicines, food and herbals.                                                                                                               |
| `registeredSince`    | string  | `""`          | Registered on or after this date (`YYYY-MM-DD`). Devices and herbals publish only a year, so that whole year matches. Not food.                                                            |
| `expiringBefore`     | string  | `""`          | Medicines whose registration expires before this date (`YYYY-MM-DD`).                                                                                                                      |
| `maxItems`           | integer | `100`         | Stop after this many rows (1-20,000).                                                                                                                                                      |
| `proxyConfiguration` | object  | no proxy      | Optional Apify Proxy, if the site is unreachable from Apify's network.                                                                                                                     |

Filters combine with AND. The site's search takes one text per request, so the actor sends
it the most selective filter (registration number, then product name, ingredient, company,
country) and checks the others on each row it reads. A filter the chosen register does not
publish (for example `activeIngredient` on devices) stops the run with a clear message
instead of returning nothing.

**Order.** Medicines come **newest registration first**. With `expiringBefore` and no
`registeredSince`, they come **soonest expiry first**. Food, devices and herbals come in
the register's own record order, highest record ID (newest record) first.

**Date filters stop early.** The actor reads medicines one registration year at a time,
newest first, so `registeredSince: "2025-07-01"` reads 2026 and 2025 and stops; and one
expiry year at a time for `expiringBefore`. A whole register fits in the default
300-second run timeout.

### Output

One dataset item per register row. Keys are English and shared across registers where they
mean the same thing; a register's items leave out the fields it does not publish. Names
are kept exactly as the register prints them (HTML entities decoded, whitespace tidied),
dates are ISO (`YYYY-MM-DD`), years are numbers. The Output tab on this page documents
every field, with one table view per register.

A real medicines item (2026-10-11):

```json
{
  "list": "medicines",
  "registrationNumber": "H2026/CTD13447/29671",
  "productName": "ACECOR GEL",
  "activeIngredients": ["ACECLOFENAC"],
  "dosageForm": "Gel",
  "countryOfOrigin": "India",
  "localOrForeign": "Foreign",
  "marketingAuthorizationHolder": "SIGNATURE HEALTHCARE LIMITED",
  "localTechnicalRepresentative": "SIGNATURE HEALTHCARE LTD",
  "registeredAt": "2026-09-29",
  "expiresAt": "2031-09-29",
  "sourceUrl": "https://products.pharmacyboardkenya.org/ppb_admin/pages/public_view_retention_products.php",
  "fetchedAt": "2026-10-11T22:35:13.238Z"
}
```

A real food item:

```json
{
  "list": "food",
  "sourceId": "15080",
  "productName": "Fitfizz Immuno D",
  "activeIngredients": ["Vitamin C", "Zinc", "Vitamin D3"],
  "strength": "Vitamin C 1000 mg + Zinc 10 mg 400 IU",
  "dosageForm": "Effervescent Tablet",
  "route": "Oral",
  "countryOfOrigin": "India",
  "localOrForeign": "Foreign",
  "marketingAuthorizationHolder": "MAXIM PHARMACEUTICALS LIMITED",
  "manufacturer": "VITAEGISS WELLNESS PVT. LTD",
  "humanOrVet": "Human",
  "applicationType": "New/Innovator",
  "packaging": "1 Tube of 20 Effervescent Tablets",
  "shelfLife": null,
  "approved": true,
  "sourceUrl": "https://products.pharmacyboardkenya.org/ppb_admin/pages/public_view_food.php",
  "fetchedAt": "2026-10-11T22:35:40.694Z"
}
```

A real medical device item:

```json
{
  "list": "devices",
  "sourceId": "21227",
  "registrationNumber": "MD/2026/001",
  "productName": "PULSE OXIMETER",
  "manufacturer": "SHENZHEN MINDRAY BIO-MEDICAL ELECTRONICS CO., LTD",
  "localTechnicalRepresentative": "MEDIGLOBE SOLUTIONS LIMITED",
  "registrationYear": 2026,
  "retentionYear": 2026,
  "licenceStatus": "VALID",
  "sourceUrl": "https://products.pharmacyboardkenya.org/ppb_admin/pages/public_view_md_retained.php",
  "fetchedAt": "2026-10-11T22:35:27.147Z"
}
```

A real herbal item:

```json
{
  "list": "herbals",
  "sourceId": "153",
  "productName": "EUGICA IVY SYRUP",
  "countryOfOrigin": "THAILAND",
  "manufacturer": "MEGA LIFESCIENCEC PUBLIC COMPANY LIMITED",
  "registrationYear": 2024,
  "sourceUrl": "https://products.pharmacyboardkenya.org/ppb_admin/pages/public_view_herbal.php",
  "fetchedAt": "2026-10-11T22:36:34.485Z"
}
```

Notes on the fields:

- **Active ingredients** are the register's "Inn Of Api" text split at commas, semicolons,
  `+`, `&` and the word "and". Food rows often leave it blank (`[]`).
- **Cut values.** The register itself cuts long text at about 50 characters and marks it
  with `…` (for example `"KETOCONAZOLE 2%, ZINC PYRITHIONE 1% & ALOE VERA…"`). The actor
  keeps the mark, so you can see where text is incomplete.
- **sourceUrl** is the register's public page. Its rows load through a form the page posts,
  so the link opens the register, not the row.

### Pricing

Pay per event:

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

Apify's standard run-start event covers starting the actor. Rows 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 row.

### Data source and politeness

- **Source:** the Pharmacy and Poisons Board's public register pages under
  `https://products.pharmacyboardkenya.org/ppb_admin/pages/`
  (`public_view_retention_products.php`, `public_view_food.php`,
  `public_view_md_retained.php`, `public_view_herbal.php`), linked from the Board's
  website. Each is a data grid; the actor loads the page, then asks the grid for rows the
  way the page's own buttons do (up to 1,000 rows per request), sending back the one-time
  key each answer carries. No login, no captcha. TLS certificates are always verified.
- **robots.txt:** the register host has none; the Board's main site
  (web.pharmacyboardkenya.org) disallows only `/wp-admin/`. The actor still
  - waits **at least 1.5 seconds** after each request ends before the next one starts
    (retries and session renewals included; a constant, not an input),
  - uses **one connection**, one request at a time,
  - identifies itself honestly as `ke-ppb-actor/0.1 (+https://apify.com/a-g-d/ke-ppb)`,
  - asks only for the rows it needs: 20 newest medicines take 4 requests.
- **Measured on 2026-10-11:** 20 newest medicines in 8 s (4 requests); 200 in 7 s;
  the whole medicines register (3,273) in 17 s (9 requests, 165 KB on the wire); 100 food
  rows in 3 s (2 requests); the whole food register (12,754) in 36 s (14 requests, about
  85 KB per 1,000-row page); 300 devices in 4 s.
- **Resilience:** connection errors, timeouts, HTTP 5xx and 429, and answers that are not a
  register grid are retried with exponential backoff and jitter inside a run-wide budget
  (90 s of backoff). If the grid's key or session expires, the actor reloads the page once
  and repeats the same request; a second refusal, an HTTP 403/404, a challenge page or a
  changed grid layout stops the run with a clear message rather than working around it.
- **Terms:** the Board's terms page carries a disclaimer, a cookie and server-log notice
  and "Copyright © 2026 Pharmacy and Poisons Board. All Rights Reserved."; nothing about
  automated access. The registers are published so the public can check products. Check
  the Board's terms for your own use of the data.

### Data and privacy

The registers publish product and company data: registered products, the companies that
hold, make or represent them, and dates. The actor outputs only the fields listed above.

- **Company fields are output as the register publishes them**, but **without addresses
  or contact details**. In the food register (230 of 12,754 rows on 2026-10-11) and a few
  medicines (77) and device (17) rows, a company column carries the company's postal or
  street address after its name ("Four Life Research Africa Limited P.O Box 23433…",
  "LABOREX KENYA LTD-MOMBASA ROAD"). The actor keeps the name and drops the address; a
  value that is only an address becomes `null`. Any e-mail address or phone number is
  dropped the same way (none appeared on 2026-10-11), and free-text fields (`strength`,
  `packaging`) that contain one become `null`.
- **The food register's "Visual Description"** (what the product looks like) is not
  collected.
- **People named as companies.** The register sometimes names a natural person as
  marketing authorisation holder (an individual is listed as holder of a cream made in
  India). The actor outputs the name as the regulator publishes it on its public register.
  Kenya's Data Protection Act, 2019 applies to personal data you store: use the data for
  the purpose it is published for, checking products and their registrations.

### Limitations

- **Cut text.** Long names, ingredients and company names arrive cut at about 50
  characters (see Output). The site's search sees the full value, so a search still finds
  text beyond the cut, but other filters checked by the actor see only the shown part.
- **No detail pages.** The registers have no per-product page the actor could read (the
  food grid's "view" button is one request per row and is not used), so the fields are the
  grid's columns.
- **Food has no dates; devices and herbals have only years.** `registeredSince` and
  `expiringBefore` say which registers they apply to.
- **Which products are listed is the register's choice.** The medicines list is the
  retention register (currently valid registrations, 2021-2026 on 2026-10-11); the device
  list is the 2026 retention list.
- **A run is a snapshot.** Rows added while a long run pages through a register belong to
  the next run; a row seen twice is stored once.
- **Search wildcards.** The site's search treats `%` and `_` as wildcards; when your text
  contains them, the actor checks the rows itself.

# Changelog

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

# Actor input Schema

## `list` (type: `string`):

Which of the Pharmacy and Poisons Board's public registers to read. Medicines come newest registration first; food, devices and herbals come in the register's record order, newest record first.

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

Trade name contains this text, any case, e.g. "panadol" or "oximeter". Leave empty for all.

## `activeIngredient` (type: `string`):

The active ingredient column contains this text, e.g. "cefixime" or "vitamin d". Medicines and food only. Leave empty for all.

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

Registration number contains this text, e.g. "H2026/CTD13447" or "MD/2026/001". Medicines and devices only. Leave empty for all.

## `company` (type: `string`):

Part of a company's name: the marketing authorisation holder or local technical representative (medicines), the holder or manufacturing site (food), the manufacturer or local representative (devices), the manufacturer (herbals). Leave empty for all.

## `country` (type: `string`):

Country of origin contains this text, e.g. "India". Medicines, food and herbals only. Leave empty for all.

## `registeredSince` (type: `string`):

Only products registered on or after this date (YYYY-MM-DD). Medicines compare the registration date; devices and herbals publish only a registration year, so a product registered in that date's year matches. Not available for food.

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

Only medicines whose registration expires before this date (YYYY-MM-DD). Without registeredSince, results come soonest expiry first. Medicines only.

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

Stop after storing this many register rows. The default 300 s run timeout covers any whole register; raise it only for the food register with filters the site cannot apply.

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

Optional. Use Apify Proxy if products.pharmacyboardkenya.org is unreachable from Apify's network.

## Actor input object example

```json
{
  "list": "medicines",
  "productName": "",
  "activeIngredient": "",
  "registrationNumber": "",
  "company": "",
  "country": "",
  "registeredSince": "",
  "expiringBefore": "",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

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

One item per register row matching the input, with the fields that register publishes: registration number or register ID, product name, active ingredients, form, country, holder, local technical representative or manufacturer, registration and expiry dates (ISO) or years, plus the food and device specifics. The dataset has one table view per register.

# 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 = {
    "list": "medicines",
    "maxItems": 20,
    "proxyConfiguration": {
        "useApifyProxy": false
    }
};

// Run the Actor and wait for it to finish
const run = await client.actor("a-g-d/ke-ppb").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 = {
    "list": "medicines",
    "maxItems": 20,
    "proxyConfiguration": { "useApifyProxy": False },
}

# Run the Actor and wait for it to finish
run = client.actor("a-g-d/ke-ppb").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 '{
  "list": "medicines",
  "maxItems": 20,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}' |
apify call a-g-d/ke-ppb --silent --output-dataset

```

## MCP server setup

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

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/TFY8wXZgepCFD8ICQ/builds/rIboPHdtnHDDSq1Qd/openapi.json
