# Etimad Tender Radar — Saudi Government Tenders (Arabic-safe) (`dottti/etimad-tender-radar`) Actor

Search live Saudi government tenders from Etimad with Arabic text repaired, so keyword filters actually match. Clean JSON: agency, activity, deadlines (Gregorian + Hijri), fees in SAR, direct tender URL.

- **URL**: https://apify.com/dottti/etimad-tender-radar.md
- **Developed by:** [Mohanad Alshaka](https://apify.com/dottti) (community)
- **Stats:** 1 total users, 0 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $5.00 / 1,000 tender delivereds

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?

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

## Etimad Tender Radar — Saudi Government Tenders (Arabic-safe)

Search live Saudi government tenders published on [Etimad](https://tenders.etimad.sa), the Ministry of Finance procurement platform, and get them back as clean JSON, CSV or Excel.

Filter by keyword, government agency, tender type, activity and closing deadline. Schedule it to run every morning and you have a tender-alert feed for your sector.

### Why this Actor exists

Etimad publishes a large share of tender names in **Arabic Presentation Forms-B** (Unicode U+FE70–U+FEFF) instead of standard Arabic letters. They look identical on screen and are a different set of code points underneath.

Taken verbatim from the live feed:

```
published : ﺇﺳﺘﻜﻤﺎﻝ ﺃﻋﻤﺎﻝ ﺇﻧﺎﺭﺓ ﻃﺮﻳﻖ ﺣﺎﺋﻞ ﺟﺒﺔ ﺍﻟﻤﺮﺣﻠﺔ ﺍﻟﺜﺎﻧﻴﺔ
searching for "أعمال"  ->  no match
```

Any pipeline that filters the raw feed silently drops these tenders, and nothing in the output tells you it happened. In a sample of 12 live tenders taken on 13 September 2026, **4 needed repair**.

This Actor normalises every text field before matching, so:

- `أعمال` matches tenders published in presentation forms
- `انارة` matches `إنارة`, `اعمال` matches `أعمال` (alef and hamza variants)
- `مستشفي` matches `مستشفى`, `صيانه` matches `صيانة`
- diacritics, tatweel (`ـ`) and Arabic-Indic digits (`٢٠٢٦`) are handled

The original wording is never thrown away. Every record carries a `raw` object with the exact text as Etimad published it, so you can still search the source portal for it.

### Output

One record per tender:

```json
{
  "tenderId": 1103165,
  "referenceNumber": "260939003389",
  "tenderNumber": "4800167238",
  "tenderName": "إستكمال أعمال إنارة طريق حائل جبة المرحلة الثانية",
  "agencyName": "أمانة منطقة حائل",
  "branchName": "منافسات حايل",
  "tenderActivityName": "مقاولات الإنشاءات العامة (التشييد و بناء المرافق العامة) - إنشاء الطرق",
  "tenderTypeName": "منافسة عامة",
  "raw": {
    "tenderName": " ﺇﺳﺘﻜﻤﺎﻝ ﺃﻋﻤﺎﻝ ﺇﻧﺎﺭﺓ ﻃﺮﻳﻖ ﺣﺎﺋﻞ ﺟﺒﺔ ﺍﻟﻤﺮﺣﻠﺔ ﺍﻟﺜﺎﻧﻴﺔ   "
  },
  "submitionDate": "2026-09-13T10:13:10.658Z",
  "lastEnqueriesDate": "2026-09-23T10:13:10.658Z",
  "lastOfferPresentationDate": "2026-09-29T06:59:00.000Z",
  "offersOpeningDate": "2026-09-29T07:00:00.000Z",
  "lastOfferPresentationDateHijri": "1448-04-18",
  "remainingDays": 15,
  "isOpen": true,
  "currency": "SAR",
  "conditionalBookletPriceSar": 500,
  "financialFeesSar": 500,
  "url": "https://tenders.etimad.sa/Tender/DetailsForVisitor?STenderId=..."
}
```

Notes on the fields:

- Dates come back twice: ISO-8601 UTC (`lastOfferPresentationDate`) and the Hijri date exactly as Etimad publishes it (`lastOfferPresentationDateHijri`).
- All amounts are Saudi riyals. The `currency` field states this explicitly and the field names carry the `Sar` suffix.
- `url` links straight to the tender page on Etimad.
- `isOpen` is derived from the remaining time Etimad reports, so a closed tender is easy to drop.

### Input

| Field | What it does |
| --- | --- |
| `keywords` | Words matched against tender name, agency, branch and activity, after Arabic normalisation. Empty returns everything the other filters allow. |
| `matchMode` | `any` (default) or `all`. |
| `agencyContains` | Keep only tenders from agencies or branches whose name contains this text. |
| `tenderTypeIds` | `1` منافسة عامة, `2` شراء مباشر, `5` المزايدة العكسية الالكترونية, `6` المنافسة على مرحلتين (المرحلة الاولى), `8` تأهيل لاحق. |
| `tenderActivityIds` | 19 business activities read from Etimad's own lookup: `9` الاتصالات وتقنية المعلومات, `2` المقاولات, `11` الرعاية الصحية والنقاهة, and so on. Sub-activity IDs work too. |
| `tenderAreaIds` | The 13 Saudi regions from Etimad's منطقة التنفيذ lookup: `1` الرياض, `2` مكة المكرمة, `5` المنطقة الشرقية, and so on. |
| `publishDateId` | Etimad's publish-date window. Narrower IDs return fewer, newer tenders. |
| `onlyOpen` | Drop tenders whose offer deadline has passed. On by default. |
| `minRemainingDays` / `maxRemainingDays` | Deadline window. Set `maxRemainingDays: 7` for a closing-soon digest. |
| `maxItems` | Hard cap on delivered tenders, and therefore on cost. |

#### Example: IT tenders, public competition, Riyadh and Eastern Province, closing within 30 days

```json
{
  "tenderTypeIds": ["1"],
  "tenderActivityIds": ["9"],
  "tenderAreaIds": ["1", "5"],
  "onlyOpen": true,
  "maxRemainingDays": 30,
  "maxItems": 500
}
```

#### Example: everything a specific municipality published this week

```json
{
  "agencyContains": "أمانة منطقة حائل",
  "publishDateId": "4",
  "onlyOpen": true,
  "maxItems": 300
}
```

### Pricing

Pay per tender delivered to your dataset. You are only charged for tenders that pass your filters and are actually stored, and `maxItems` caps the spend of any run.

### Reliability

Etimad answers bursts of requests with HTTP 429. This Actor spaces its requests out and retries with exponential backoff, honouring `Retry-After` when present, so a throttled page is retried instead of being reported as an empty result.

### Notes and limits

- Data comes from Etimad's public visitor endpoint. No login, no credentials, no personal data.
- Tender documents themselves (كراسة الشروط) are not downloaded; the record links to the tender page where they are obtained.
- Etimad reports roughly 288,000 tenders in total across all statuses, so always set filters and `maxItems`.
- Field availability follows the source: fields Etimad leaves empty are returned as `null` rather than guessed.

### Development

```bash
npm install
npm test                 # Arabic normalisation test suite
node src/main.js         # local run, reads storage/key_value_stores/default/INPUT.json
```

# Actor input Schema

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

Words to match against tender name, agency, branch and activity. Arabic is matched after normalisation, so أعمال matches tenders published in presentation forms, and alef/hamza, ة/ه and ى/ي spelling variants all match. Leave empty to return everything the other filters allow.

## `matchMode` (type: `string`):

any: keep a tender when at least one keyword matches. all: keep it only when every keyword matches.

## `agencyContains` (type: `string`):

Keep only tenders whose agency or branch name contains this text. Normalised the same way as keywords. Example: أمانة

## `tenderTypeIds` (type: `array`):

Etimad tender type IDs. Verified against the live feed on 13 September 2026 with the tender counts shown.

## `tenderActivityIds` (type: `array`):

Etimad main activity IDs, read from the portal's own نشاط المنافسة lookup. Sub-activity IDs (for example 1919 خدمات الموانئ) are accepted here too. One query is issued per activity.

## `tenderAreaIds` (type: `array`):

Saudi administrative regions, as published by Etimad's own منطقة التنفيذ lookup. One query is issued per region.

## `publishDateId` (type: `string`):

Etimad's own publish-date filter. Narrower IDs return fewer, newer tenders. Counts observed on 13 September 2026: 2 -> 58, 3 -> 557, 4 -> 2,416, 5 -> 7,423 tenders. Leave empty for no date restriction.

## `onlyOpen` (type: `boolean`):

Drop tenders whose offer-submission deadline has already passed.

## `minRemainingDays` (type: `integer`):

Keep only tenders with at least this many days left before the offer deadline. Useful to skip tenders you cannot realistically bid on.

## `maxRemainingDays` (type: `integer`):

Keep only tenders closing within this many days. Set to 7 for a weekly closing-soon digest.

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

Hard cap on delivered tenders. You are charged per tender delivered, so this is also your cost cap.

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

Etimad returns HTTP 429 on bursts. Lower this only if you see the run is slower than you need and the logs show no 429s.

## `maxRetries` (type: `integer`):

Retries with exponential backoff when Etimad answers 429 or 5xx.

## Actor input object example

```json
{
  "keywords": [
    "تقنية المعلومات"
  ],
  "matchMode": "any",
  "publishDateId": "",
  "onlyOpen": true,
  "maxItems": 200,
  "requestDelayMs": 700,
  "maxRetries": 5
}
```

# Actor output Schema

## `tenders` (type: `string`):

Matching tenders: name, agency, activity, tender type, offer and enquiry deadlines in both Gregorian and Hijri, remaining time, fees in SAR, and a direct link to the tender on Etimad.

# 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 = {
    "keywords": [
        "تقنية المعلومات"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/etimad-tender-radar").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 = { "keywords": ["تقنية المعلومات"] }

# Run the Actor and wait for it to finish
run = client.actor("dottti/etimad-tender-radar").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 '{
  "keywords": [
    "تقنية المعلومات"
  ]
}' |
apify call dottti/etimad-tender-radar --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,dottti/etimad-tender-radar"
        }
    }
}
```

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/cf3c6teDU1JcCTLbd/builds/yurewvNWZSd2FNHJJ/openapi.json
