# Furas Radar — Saudi Municipal Investment Opportunities (فرص) (`dottti/furas-opportunities`) Actor

Saudi municipal investment and rental opportunities from Furas (فرص), the Ministry of Municipalities and Housing platform. Clean JSON: type, activity, amana, baladya, city, area, booklet price in SAR, sell and envelope-opening dates, contract duration, coordinates and a link to the opportunity page.

- **URL**: https://apify.com/dottti/furas-opportunities.md
- **Developed by:** [Dottti](https://apify.com/dottti) (community)
- **Security:** Limited permissions
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $10.00 / 1,000 opportunity 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?

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

## Furas Radar — Saudi Municipal Investment Opportunities (فرص)

Get Saudi municipal investment and rental opportunities from [Furas (فرص)](https://furas.momah.gov.sa) as clean JSON, CSV or Excel. Furas is the platform of the Ministry of Municipalities and Housing.

Each record gives you the site, the activity, the amana and municipality, the area, the price in SAR, the key dates, the contract length and the map coordinates. It also links to the opportunity page on Furas.

### Who uses this

- **Retail and F\&B chains** find kiosks, cafés, restaurant plots and mobile-cart sites in the cities they target.
- **Real-estate investors** track land and building opportunities by area, region and contract length.
- **Site-selection firms** load opportunities with coordinates straight into a map or GIS tool.

Looking for government tenders as well? See [Etimad Tender Radar](https://apify.com/dottti/etimad-tender-radar).

### What you get

One record per opportunity. This record came from a live run on 20 October 2026. It was checked against its page on Furas: area 1000 m², contract 24 months, booklet price 500, envelopes open 2026-12-23.

```json
{
  "opportunityId": "01-26-008203-7005",
  "url": "https://furas.momah.gov.sa/opportunity/01-26-008203-7005?type=Investment",
  "description": "انشاء و تشغيل و صيانة كشك تقديم اطعمه ومشروبات - الأنشطة التجارية - كشك - كشك",
  "status": "Announced",
  "isOpenForPurchase": true,
  "daysUntilLastRfpSellDate": 64,
  "opportunityTypeId": "01",
  "opportunityType": "Investment",
  "opportunityTypeAr": "الفرص الاستثمارية",
  "activityType": "الأنشطة التجارية",
  "subActivityType": "أخرى",
  "amanaId": "008",
  "amana": "أمانة منطقة القصيم",
  "baladya": "بلدية محافظة المذنب",
  "region": "منطقة القصيم",
  "district": "منتزهات خرطم",
  "latitude": 25.878197957999987,
  "longitude": 44.231771611,
  "areaSqm": 1000,
  "currency": "SAR",
  "rfpPriceSar": 500,
  "pricePerSqmSar": null,
  "durationValue": 24,
  "durationUnit": "months",
  "durationYears": 2,
  "announcedDate": "2026-10-20",
  "lastRfpSellAt": "2026-12-23T04:00:00.000Z",
  "lastRfpSellDate": "2026-12-23",
  "envelopesOpenDate": "2026-12-23",
  "isInvitation": false,
  "isInvestmentCenter": false,
  "raw": {
    "OPPORTUNITYID": "01-26-008203-7005",
    "RFPPRICE": "500",
    "TOTALSPACEINMETERS": "1000.0",
    "LASTRFPSELLDATE": 1797998400000,
    "DURATION": 24
  }
}
```

The example is shortened. A full record has about 60 keys, and `raw` holds every original field from the source.

#### Notes on the fields

- **Dates** come twice. `...At` is ISO-8601 in UTC. `...Date` is the calendar date in Saudi Arabia (UTC+3). Use the `...Date` keys. The source stores dates at different times of day, so the time in the `...At` keys carries no meaning.
- **Money** is in Saudi riyals (`currency: "SAR"`). For investment opportunities, `rfpPriceSar` is the booklet price (سعر الكراسة). For temporary rentals (type 02), the same source field is the price per square metre (سعر المتر), so it goes in `pricePerSqmSar`. A value of 0 is kept as 0. The Furas map shows 0 as "متغير" (variable).
- **Contract length** is in months for investment opportunities and in days for temporary rentals. `durationUnit` says which.
- **Coordinates** are WGS84 latitude and longitude.
- **`url`** follows the pattern the Furas map uses. It was checked on four live opportunities.
- **`raw`** keeps the original fields exactly as published, so you can trace any value back to the source. Two fields hold staff usernames (`CREATED_USER`, `LAST_EDITED_USER`). Those two are removed.
- Empty source fields come back as `null`. Nothing is guessed.

### Filters

| Field | What it does |
| --- | --- |
| `status` | `open` (default): announced opportunities whose booklet can still be bought. This is the Furas map's own default. `announced`: every announced opportunity. `all`: every record, including cancelled and old ones. |
| `amanaIds` | One or more amanas, for example `004` Riyadh, `005` Jeddah, `006` Eastern Province. The input form lists all 17. |
| `opportunityTypeIds` | `01` Investment, `02` Temporary Rental, `05` Qualifying, `07` Direct Rental. |
| `cityContains` | Text in the city, municipality, district or region name. Example: `الرياض`. |
| `activityContains` | Text in the activity, sub-activity or location type. Examples: `التجارية`, `العربات المتنقلة`. |
| `keywords` | Words in the description, activity, location type, municipality, city, district or street. Example: `مطعم`, `مقهى`. |
| `matchMode` | `any` (default) or `all` keywords. |
| `minArea` / `maxArea` | Site area in square metres. |
| `publishedAfter` | `YYYY-MM-DD`. Keeps opportunities announced on or after this date. |
| `sortBy` | `newest` (default) or `closingSoon`. |
| `maxItems` | Hard cap on delivered opportunities. Default 100. This is also your cost cap. |
| `includeGeometry` | Latitude and longitude. On by default. |

Arabic text filters ignore common spelling differences. `مقهي` matches `مقهى`. `الانشطه` matches `الأنشطة`.

The status, amana, type and date filters run on the Furas server. The text and area filters run after download. A narrow text search over a large status can therefore read many records before it finds matches.

#### Example: cafés and restaurants in Riyadh, closing soonest first

```json
{
  "status": "open",
  "amanaIds": ["004"],
  "keywords": ["مطعم", "مقهى", "كوفي"],
  "sortBy": "closingSoon",
  "maxItems": 50
}
```

#### Example: land of 5,000 m² or more announced since 1 October 2026

```json
{
  "status": "open",
  "activityContains": "الاراضي",
  "minArea": 5000,
  "publishedAfter": "2026-10-01",
  "maxItems": 200
}
```

### Daily alert in 3 steps

1. Set your filters. Set `publishedAfter` to yesterday's date, or set `sortBy` to `newest` with a small `maxItems`.
2. Click **Save as a new task**. Then add a daily schedule to the task.
3. Open the **Integrations** tab and connect the task to email, Slack or Google Sheets. New opportunities then arrive every morning.

### Pricing

$0.01 per opportunity delivered to your dataset ($10 per 1,000). You pay only for opportunities that pass your filters and are stored. `maxItems` caps the cost of every run. A run that delivers 100 opportunities costs $1.00.

### Size of the source

Counts from 20 October 2026:

- About 202,000 records in total, across all statuses.
- About 80,000 marked Announced.
- About 6,500 open for purchase (the default filter).

### Limits

- **Access from outside Saudi Arabia.** The data comes from the public map service that the Furas website uses. The ministry's firewall may refuse requests from some countries or from data-centre IP addresses. When that happens, the run stops with a clear error. Nothing is delivered and nothing is charged.
- **Pacing.** The Actor waits 1 second between requests by default. It retries network errors and server errors with exponential backoff.
- **Amana codes.** A small share of records carry no amana code, and a few carry a code that does not match their amana name. The `amanaIds` filter uses the code, so those records are left out of an amana-filtered run.
- **Source data quality.** Values are passed through as published. Some records have a blank city, an area of 0, or a price that looks wrong (for example 0.0004 SAR per m²). The `raw` object shows exactly what the source holds.
- **Documents.** Booklets (كراسة الشروط) are not downloaded. Use the `url` to open the opportunity page, where they are listed.
- **No login.** The Actor uses only the public map query service. It uses no account, token or credential.

### Development

```bash
npm install
npm test          # unit tests on saved live responses
apify run --purge # local run, reads storage/key_value_stores/default/INPUT.json
```

# Actor input Schema

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

open: announced opportunities whose booklet (RFP) can still be bought, the same filter the Furas map uses by default. announced: every announced opportunity, including ones whose sell date has passed. all: every record in the layer, including cancelled and old ones.

## `amanaIds` (type: `array`):

Keep only opportunities from these amanas. Filtered on the server by amana code. Leave empty for all of Saudi Arabia.

## `opportunityTypeIds` (type: `array`):

Filtered on the server. Leave empty for all types.

## `cityContains` (type: `string`):

Keep only opportunities whose city, baladya (municipality), district or region contains this text. Arabic spelling variants match (أ/إ/ا, ة/ه, ى/ي). Example: الرياض

## `activityContains` (type: `string`):

Keep only opportunities whose activity, sub-activity or location type contains this text. Examples: التجارية, السياحية, العربات المتنقلة, الحدائق

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

Words matched against the description, activity, location type, municipality, city, district and street, after Arabic normalisation. Examples: مطعم, مقهى, محطة وقود.

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

any: keep an opportunity when at least one keyword matches. all: keep it only when every keyword matches.

## `minArea` (type: `integer`):

Keep only opportunities with a published site area of at least this many square metres. Records without an area are dropped when this is set.

## `maxArea` (type: `integer`):

Keep only opportunities with a published site area of at most this many square metres.

## `publishedAfter` (type: `string`):

YYYY-MM-DD. Keep only opportunities whose announcement date (STARTDATE) is on or after this date. This is the field the Furas site uses for its 'last week' and 'last month' filters. Records without an announcement date are excluded when this is set.

## `sortBy` (type: `string`):

newest: most recently added records first. closingSoon: earliest booklet sell date first.

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

Hard cap on delivered opportunities. You pay per opportunity delivered, so this is also your cost cap.

## `includeGeometry` (type: `boolean`):

Return latitude and longitude (WGS84) for each opportunity.

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

Pause before every request to the Furas service. Keep it at 1000 or more.

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

Retries with exponential backoff on network errors, HTTP 429 and 5xx.

## Actor input object example

```json
{
  "status": "open",
  "keywords": [],
  "matchMode": "any",
  "sortBy": "newest",
  "maxItems": 20,
  "includeGeometry": true,
  "requestDelayMs": 1000,
  "maxRetries": 4
}
```

# Actor output Schema

## `opportunities` (type: `string`):

Matching opportunities: type, activity, amana, municipality, city, area, booklet price in SAR, sell and envelope-opening dates, contract duration, coordinates and a link to the opportunity on furas.momah.gov.sa.

# 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": [],
    "maxItems": 20
};

// Run the Actor and wait for it to finish
const run = await client.actor("dottti/furas-opportunities").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": [],
    "maxItems": 20,
}

# Run the Actor and wait for it to finish
run = client.actor("dottti/furas-opportunities").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": [],
  "maxItems": 20
}' |
apify call dottti/furas-opportunities --silent --output-dataset

```

## MCP server setup

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

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/yufqhcb2bY6G93Qgd/builds/TeegcKcRhjJOtPy8j/openapi.json
