# PPRA Pakistan (`blessed_jouster-owner/epad`) Actor

Tender scraper

- **URL**: https://apify.com/blessed\_jouster-owner/epad.md
- **Developed by:** [I-Ahmad And Sons](https://apify.com/blessed_jouster-owner) (community)
- **Categories:** AI, E-commerce, Lead generation
- **Stats:** 2 total users, 1 monthly users, 0.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $0.00005 / actor start

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/platform/actors/running/actors-in-store#pay-per-event

## What's an Apify Actor?

Actors are web data automations that power AI and operations. They run on the Apify platform to scrape websites, process data, connect APIs, and automate workflows.
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.
Actors are written with capital "A".

## 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.
The best way to integrate Actors is as follows.

- **AI agents and MCP clients** — the [Apify MCP server](https://docs.apify.com/integrations/mcp.md) at `https://mcp.apify.com` (remote, streamable HTTP, OAuth on first use).
- **Agentic workflows and local Actor development** — [Agent Skills](https://apify.com/.well-known/agent-skills/index.json) with the [Apify CLI](https://docs.apify.com/cli/docs.md): `npm install -g apify-cli`, then `apify login`.
- **JavaScript/TypeScript projects** — the official [JS/TS client](https://docs.apify.com/api/client/js/docs.md): `npm install apify-client`.
- **Python projects** — the official [Python client](https://docs.apify.com/api/client/python/docs.md): `pip install apify-client`.
- **Any other language** — the [REST API](https://docs.apify.com/api/v2.md).

For usage examples, see the [API](#api) section below.

For more details, see Apify documentation as [Markdown index](https://docs.apify.com/llms.txt) and [Markdown full-text](https://docs.apify.com/llms-full.txt).

# README

## Pakistan EPADS & PPRA Tender Notices Scraper

An Apify Actor for collecting public procurement opportunities across Pakistan into one normalized dataset. It covers Federal EPADS/PPRA, all four provinces, Gilgit-Baltistan, and Azad Jammu and Kashmir, while isolating portal failures so one unavailable authority does not stop the run.

This project is independent and is not affiliated with PPRA, EPADS, or any government authority. It reads public pages only; it does not log in, save notices, express interest, or submit bids.

### Coverage

| Jurisdiction | Public source | Adapter |
|---|---|---|
| Federal | [EPADS 2.0 open opportunities](https://epads.gov.pk/) | Dedicated EPADS table and embedded SBD-document extraction |
| Federal | [PPRA EPMS active tenders](https://epms.ppra.gov.pk/public/tenders/active-tenders) | Dedicated paginated table extractor |
| Punjab | [Punjab active tenders](https://eproc.punjab.gov.pk/ActiveTenders.aspx) | Header-mapped legacy portal extractor |
| Sindh | [Sindh PPMS notices](https://ppms.pprasindh.gov.pk/PPMS/public/portal/notice-inviting-tender) | Header-mapped PPMS extractor |
| Khyber Pakhtunkhwa | [KP PPRA active tenders](https://www.kppra.gov.pk/kppra/activetenders) | Header-mapped active-tender extractor |
| Balochistan | [BPPRA tender search linked by the provincial gateway](https://bppthree.vdc.services/tenderssearch/), [legacy BPPRA](http://www.bppra.gob.pk/), and [Government tender fallback](https://balochistan.gov.pk/tenders/) | Primary/fallback generic adapters |
| Gilgit-Baltistan | [GB PPRA procurements](https://www.gbppra.gov.pk/viewall) | Dedicated card and document extractor |
| Azad Jammu and Kashmir | [AJK PPRA advertisements](https://www.ajkppra.gov.pk/advertisements.php) | Header-mapped advertisement extractor |

Punjab, Sindh, and KP also have newer EPADS sign-in frontends. The Actor deliberately uses their public tender listings because authenticated pages are outside its scope. Provincial sites are sometimes slow or return 5xx/406 responses to cloud traffic; retries, optional Apify proxy configuration, and per-source status reporting are built in.

### What it extracts

- Tender ID and buyer reference
- Title, description, buyer, jurisdiction, source, and city
- Notice type, procurement category, sector, procedure, and status
- Publication date, deadline, normalized ISO timestamps, and remaining hours/days
- Bid security and bid validity when shown
- Public contact name, email, and phone
- Public advertisements, bidding documents, corrigenda, PDFs, and embedded EPADS SBD links
- Detail and supplier/submission portal links
- Matched keywords, relevance score, and `new`/`updated` state for monitoring runs

### Example input

```json
{
  "sources": ["federal", "punjab", "sindh", "khyber_pakhtunkhwa", "balochistan", "gilgit_baltistan", "ajk"],
  "keywords": ["tool kit", "livelihood kit", "NFI", "furniture", "printing", "stationery", "Pakistan Army", "Ministry of Defence"],
  "reference": "",
  "description": "",
  "cities": ["Lahore", "Rawalpindi", "Islamabad", "Peshawar", "Karachi", "Quetta"],
  "activeOnly": true,
  "minimumRemainingHours": 48,
  "includeDetails": true,
  "includeDocumentLinks": true,
  "onlyNew": true,
  "stateStoreName": "pakistan-daily-bid-monitor",
  "maxItems": 200,
  "maxPagesPerSource": 50,
  "maxDetailPages": 300,
  "sortField": "Relevance",
  "sortAscending": false
}
```

`minimumRemainingHours: 48` implements an automatic no-bid screen for opportunities with less than two days left, when a deadline is available. Unknown deadlines are retained rather than silently discarded.

### Example output

```json
{
  "noticeId": "TS0000010847E",
  "reference": "4615/IT-7860/26-27/MTI/FOR-B/SCM",
  "title": "Procurement of Tools and Store Items for MTI",
  "buyer": "Ministry of Defence Production",
  "jurisdiction": "Federal",
  "source": "federal_ppra_epms",
  "sources": ["federal_ppra_epms"],
  "noticeType": "Tender Notice",
  "category": "Equipments",
  "status": "Published",
  "publishedDateIso": "2026-08-04T00:00:00.000+05:00",
  "deadlineIso": "2026-09-01T10:30:00.000+05:00",
  "remainingHours": 645.5,
  "remainingDays": 26.9,
  "documentLinks": [],
  "detailUrl": "https://epms.ppra.gov.pk/public/tenders/tender-details/TS0000010847E",
  "matchedKeywords": ["tool kit"],
  "matchScore": 31,
  "changeType": "new",
  "scrapedAt": "2026-08-05T07:00:00.000Z"
}
```

### How the crawl works

1. Listing pages are read first across every selected jurisdiction.
2. Dates are normalized in `Asia/Karachi`; obviously inactive or out-of-window notices are removed.
3. Records are deduplicated within a portal and across overlapping Federal sources.
4. The best candidates are opened for public documents and detail fields, within `maxDetailPages`.
5. Full text/buyer/type/category filters are applied, records are scored, and `maxItems` is enforced.
6. With `onlyNew: true`, a named key-value store emits a record again only if it is new or its title, deadline, status, publication date, or document set changed.

The two-stage design avoids opening hundreds of detail pages that cannot pass date or status rules. When a keyword exists only in a long detail description, set a sufficiently high `maxDetailPages` to search it.

### Input reference

| Option | Purpose |
|---|---|
| `sources` | Any of `federal`, `punjab`, `sindh`, `khyber_pakhtunkhwa`, `balochistan`, `gilgit_baltistan`, `ajk` |
| `keywords` / `matchAllKeywords` | Phrase matching across normalized tender text |
| `reference` | Partial or exact portal ID, tender number, inquiry number, or buyer reference |
| `description` | Required phrase in the long description; use with `includeDetails: true` |
| `excludeKeywords` | Reject notices containing any excluded phrase |
| `organizations` | Partial buyer/organization names |
| `cities` | Partial city or location names |
| `noticeTypes` / `categories` | Partial normalized type/category filters |
| `activeOnly` | Remove explicit closed/cancelled/archived/withdrawn notices and past deadlines |
| `publishedWithinHours` | Relative new-notice window; `24` is useful for daily runs |
| `publishedFrom`, `publishedTo` | Inclusive `YYYY-MM-DD` publication window |
| `deadlineFrom`, `deadlineTo` | Inclusive `YYYY-MM-DD` deadline window |
| `minimumRemainingHours` | Deadline feasibility guardrail; use `48` for a two-day cutoff |
| `includeDetails` | Extract long descriptions, contacts, security, validity, and procedure |
| `includeDocumentLinks` | Collect public documents and embedded SBD links |
| `maxItems` | Maximum records saved after filtering |
| `maxPagesPerSource` | Listing pagination safety limit per portal |
| `maxDetailPages` | Cross-portal detail request/cost guardrail |
| `sortField` / `sortAscending` | Sort by relevance, deadline, publication date, title, or reference |
| `onlyNew` / `stateStoreName` | Stateful scheduled monitoring and independent monitor identity |
| `proxyConfiguration` | Optional Apify proxy configuration for difficult provincial portals |

### Deploy to Apify

From the project directory:

```bash
npm install
npm test
apify login
apify push
```

The Actor uses the official Playwright/Chrome base image declared in `.actor/Dockerfile`. The input, dataset views, and output summary are defined under `.actor/` and appear automatically in the Apify Console.

For local fixture verification:

```bash
npm test
```

For a non-browser check against the stable, publicly reachable Federal, GB, and Balochistan-fallback layouts:

```bash
npm run smoke
```

### API usage

#### Node.js

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

const client = new ApifyClient({ token: process.env.APIFY_TOKEN });
const run = await client.actor('YOUR_USERNAME/pakistan-epad-tender-notices-scraper').call({
    keywords: ['tool kit', 'furniture', 'printing'],
    activeOnly: true,
    minimumRemainingHours: 48,
    includeDetails: true,
    includeDocumentLinks: true,
    maxItems: 100,
});

const { items } = await client.dataset(run.defaultDatasetId).listItems();
console.log(items);
```

#### Python

```python
from apify_client import ApifyClient

client = ApifyClient('YOUR_APIFY_TOKEN')
run = client.actor('YOUR_USERNAME/pakistan-epad-tender-notices-scraper').call(run_input={
    'keywords': ['tool kit', 'furniture', 'printing'],
    'activeOnly': True,
    'minimumRemainingHours': 48,
    'includeDetails': True,
    'maxItems': 100,
})
items = client.dataset(run['defaultDatasetId']).list_items().items
print(items)
```

#### cURL

```bash
curl -X POST 'https://api.apify.com/v2/acts/YOUR_USERNAME~pakistan-epad-tender-notices-scraper/runs?token=YOUR_APIFY_TOKEN' \
  -H 'Content-Type: application/json' \
  -d '{"keywords":["tool kit","furniture"],"activeOnly":true,"minimumRemainingHours":48,"maxItems":100}'
```

### Scheduling and integrations

Create a daily Apify schedule with `onlyNew: true`, a stable `stateStoreName`, and a focused keyword list. Dataset items can then feed Google Sheets, Airtable, Notion, email/Slack alerts, a CRM, a bid-triage dashboard, or a webhook. Keep separate state-store names for independent teams or search profiles.

### Data quality and maintenance

- Dates are retained in their original portal text and also normalized. Relative EPADS deadlines are resolved at run time in Pakistan Standard Time.
- A missing public field is returned as `null`; the Actor does not invent bid requirements.
- Some authorities publish only a scanned PDF. The Actor returns the document link but does not OCR the document in version 0.1.0.
- Legacy portals occasionally reject data-center traffic or go offline. Review the `OUTPUT` summary: each source is marked `ok`, `empty`, `partial`, or `failed`, with warnings and counts.
- Government portals can change without notice. Run `npm run smoke` after deployment updates and keep the fixture suite when adjusting selectors.

### Responsible use

Use a modest schedule, narrow searches where possible, and respect portal terms, robots instructions, copyright, and applicable procurement rules. Download public document batches only when needed. This Actor does not bypass authentication, CAPTCHA, or access controls.

# Actor input Schema

## `sources` (type: `array`):

Federal includes both the EPADS 2.0 open-opportunities portal and the Federal PPRA public tender listing.

## `keywords` (type: `array`):

Match against title, description, reference, buyer, category, sector, and location. Leave empty to include every notice.

## `reference` (type: `string`):

Optional partial or exact portal ID, tender number, inquiry number, or buyer reference.

## `description` (type: `string`):

Optional phrase required in the long tender description. Keep Include details enabled so detail-only text can be checked.

## `matchAllKeywords` (type: `boolean`):

When false, any include keyword can match. When true, every include keyword must match.

## `excludeKeywords` (type: `array`):

Discard a notice when any of these phrases appears in its searchable text.

## `organizations` (type: `array`):

Optional partial-name filters such as Pakistan Army, Health Department, or WAPDA.

## `cities` (type: `array`):

Optional partial location filters such as Lahore, Karachi, Rawalpindi, Peshawar, Quetta, or Islamabad.

## `noticeTypes` (type: `array`):

Optional values such as Tender Notice, RFQ, RFP, EOI, Prequalification, Corrigendum, or Auction.

## `categories` (type: `array`):

Optional values such as Goods, Works, Non-Consultancy Services, Consultancy Services, Furniture, or Electrical Items.

## `activeOnly` (type: `boolean`):

Exclude notices explicitly marked closed, cancelled, archived, or with a past deadline.

## `publishedWithinHours` (type: `integer`):

Use 24 for a daily new-tender scan. Use 0 to disable this relative-date filter.

## `publishedFrom` (type: `string`):

Inclusive YYYY-MM-DD date. Leave blank for no lower limit.

## `publishedTo` (type: `string`):

Inclusive YYYY-MM-DD date. Leave blank for no upper limit.

## `deadlineFrom` (type: `string`):

Inclusive YYYY-MM-DD date. Leave blank for no lower limit.

## `deadlineTo` (type: `string`):

Inclusive YYYY-MM-DD date. Leave blank for no upper limit.

## `minimumRemainingHours` (type: `integer`):

Use 48 to exclude opportunities with less than 48 hours remaining. Use 0 to disable.

## `onlyNew` (type: `boolean`):

Remember notices between scheduled runs. A changed deadline, status, title, or document set is emitted again as an update.

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

Use a different name for each independent monitoring task.

## `seenRetentionDays` (type: `integer`):

Forget state records older than this many days.

## `includeDetails` (type: `boolean`):

Extract descriptions, contacts, bid security, validity, procedure, and other fields when public detail pages exist.

## `includeDocumentLinks` (type: `boolean`):

Collect advertisements, bidding documents, corrigenda, and embedded EPADS bidding-document links.

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

Maximum normalized tender records saved to the dataset after filtering and deduplication.

## `maxPagesPerSource` (type: `integer`):

Safety limit for each public portal. EPADS 2.0 currently serves many opportunities on one page.

## `maxDetailPages` (type: `integer`):

Cost guardrail across all portals. Text filters that only appear inside a notice description can only be evaluated for detail pages visited within this limit.

## `maxConcurrency` (type: `integer`):

Maximum number of portal requests processed concurrently. Reduce this value if a government portal throttles or times out.

## `maxRequestRetries` (type: `integer`):

Number of automatic retries after a temporary portal, network, or timeout failure.

## `requestTimeoutSecs` (type: `integer`):

Maximum time allowed for one portal request before it is treated as timed out.

## `sortField` (type: `string`):

Relevance prioritizes keyword matches; other options mirror common tender-search ordering.

## `sortAscending` (type: `boolean`):

Enable for earliest deadline/oldest publication/A-Z. Disable for highest relevance/latest publication/Z-A.

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

Optional. A Pakistan or residential proxy can help when a provincial portal rejects cloud traffic.

## Actor input object example

```json
{
  "sources": [
    "federal",
    "punjab",
    "sindh",
    "khyber_pakhtunkhwa",
    "balochistan",
    "gilgit_baltistan",
    "ajk"
  ],
  "keywords": [],
  "reference": "",
  "description": "",
  "matchAllKeywords": false,
  "excludeKeywords": [],
  "organizations": [],
  "cities": [],
  "noticeTypes": [],
  "categories": [],
  "activeOnly": true,
  "publishedWithinHours": 0,
  "publishedFrom": "",
  "publishedTo": "",
  "deadlineFrom": "",
  "deadlineTo": "",
  "minimumRemainingHours": 0,
  "onlyNew": false,
  "stateStoreName": "pakistan-epad-tender-monitor-state",
  "seenRetentionDays": 120,
  "includeDetails": true,
  "includeDocumentLinks": true,
  "maxItems": 100,
  "maxPagesPerSource": 50,
  "maxDetailPages": 250,
  "maxConcurrency": 5,
  "maxRequestRetries": 3,
  "requestTimeoutSecs": 75,
  "sortField": "Relevance",
  "sortAscending": false,
  "proxyConfiguration": {
    "useApifyProxy": false
  }
}
```

# Actor output Schema

## `results` (type: `string`):

No description

## `summary` (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("blessed_jouster-owner/epad").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("blessed_jouster-owner/epad").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 blessed_jouster-owner/epad --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,blessed_jouster-owner/epad"
        }
    }
}

```

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/ijd1GYwfZEyn0RBUh/builds/c4TW1v6MgsV5unxHB/openapi.json
