# UAE Fragrance Catalog — Ajmal & Swiss Arabian (`vixendata/uae-fragrance-catalog`) Actor

Compare public AED fragrance prices, sizes, variants and availability across Ajmal UAE and Swiss Arabian UAE.

- **URL**: https://apify.com/vixendata/uae-fragrance-catalog.md
- **Developed by:** [Vixendata](https://apify.com/vixendata) (community)
- **Stats:** 2 total users, 1 monthly users, 100.0% runs succeeded, 0 bookmarks
- **User rating**: No ratings yet

## Pricing

from $3.00 / 1,000 ajmal product or swiss arabian variant rows

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

## UAE Fragrance Catalog: Ajmal and Swiss Arabian

Build a comparable AED catalog across two UAE perfume retailers, with SKU, size, price, original listed price, public availability and rating scale preserved. Useful for assortment comparisons and checking public price changes by rerunning saved inputs and comparing exports.

This is one cross-brand dataset: Ajmal's Next.js catalog and Swiss Arabian's Shopify variants are normalized without treating them as interchangeable source formats.

### Quick start

The default input requests two known product pages, one per source, and returns three rows with the sampled catalog: one Ajmal product and two Swiss Arabian variants. For a small hosted smoke run:

```json
{
  "productUrls": [
    "https://en-ae.ajmal.com/alf-laila-o-lail-spray",
    "https://uae.swissarabian.com/products/shaghaf-oud-azraq"
  ],
  "maxProducts": 2,
  "maxPages": 1,
  "maxRequests": 4
}
```

Results go into the default dataset (CSV/JSON exports in Apify). The OUTPUT key-value record contains request statuses, errors, stopped sources, attempted product pages and record count. No records causes a failed run; partial results carry explicit OUTPUT errors. Always inspect OUTPUT when checking completeness.

### Inputs and discovery

- productUrls: up to 50 canonical product URL strings on the two supported HTTPS UAE hosts. No queries, tracking, variant parameters, encoded paths, credentials or alternate hosts. All public variants are returned from a supplied Swiss Arabian product.
- maxProducts: total product-page cap, default 2, hard maximum 50. Variant rows can exceed this number.
- maxPages: Swiss Arabian collection page cap, default 1, maximum 5.
- maxRequests: all HTTP requests, including robots, discovery and redirects; default 8, maximum 100. No retries.
- discoverAjmal: default false. Read the fixed public Ajmal product sitemap and take remaining product capacity. Image sitemap URLs are excluded.
- swissCollectionUrl: optional plain UAE collection URL, such as https://uae.swissarabian.com/collections/shaghaf. Follows only page links actually exposed as “Next” or rel=next, with loop detection. Current robots rules reject prohibited collections.

Explicit URLs get capacity first. Ajmal discovery fills remaining capacity before Swiss discovery. Omit productUrls or set it to \[] for discovery-only; set maxProducts high enough to include both brands if combining discovery methods. This ordering makes output deterministic rather than claiming equal sampling of the brands. When either discovery option is selected, an omitted or empty productUrls field starts with no sample seeds. Without discovery, an omitted field retains the two-product sample behavior. The UI prefill is only a suggestion; clear it for discovery-only.

### What the sources mean

**Ajmal UAE:** Parses expanded product objects embedded in the public page's React Flight scripts using JSON decoding, never JavaScript evaluation. Matches the requested URL key so recommendations are not returned. Tested for simple products; configurable/bundle products and unequal price ranges are rejected rather than flattened into an invented variant price. Missing or reference-only Flight data produces an explicit error.

Ajmal's rating_summary is a 100-point value: a raw 100 means 100/100, not 100 stars. regular_price is retained as originalPrice even when identical to the current price; equality does not imply a discount. Size and concentration come from explicit display attributes.

**Swiss Arabian UAE:** Reads the public embedded product/variant object, with an independent Shopify variant SKU/price cross-check when available. Emits a row per variant. A 75 ML variant and 25 ML variant keep their distinct prices and IDs. compareAtPrice is originalPrice only when explicitly supplied; absent comparison prices stay null. Ratings are shown on a five-point scale only when the page explicitly contains the widget's “out of 5” label. No automatic conversion to a cross-source star score is applied.

### Output fields

- source: ajmal or swiss_arabian; brand: display brand
- productId, variantId, sku: source identifiers as strings; null if absent
- title, productUrl, variantUrl: product title and source URLs; variantUrl is informational and never fetched
- price, originalPrice, currency: numeric AED amounts; missing values null, no inferred discounts
- size, concentration: explicit source fields only; Swiss concentration currently null
- availability, inStock: merchant's public offer status, not verified warehouse quantity, store-level inventory or delivery promise
- imageUrls: source images; not downloaded
- ratingRaw, ratingScale, ratingScaleDescription, reviewCount: preserve original scale and aggregate counts
- fragranceNotes: Ajmal's explicitly named top/heart/base ingredients; Swiss currently null rather than untested prose extraction
- scrapedAt: UTC extraction timestamp

No reviewer names, review text, hidden APIs, account information, inventory counts or checkout data are collected into output.

### Respectful limits

Requests are serial. Ajmal spacing is at least 10 seconds, Swiss Arabian at least 2 seconds; longer published crawl-delay or Request-rate intervals win. Every used host's robots.txt is checked first. Empty/malformed/unavailable policies fail closed. Wildcards and Allow/Disallow precedence are supported.

HTTP 401, 403, 429 and recognized challenge pages stop that source. No custom/paid proxy, login, CAPTCHA solving, retry, API access or access-control workaround. Redirects are manually validated, same-host only, at most two; robots redirects must remain the canonical robots path. Each response is capped at 8 MB and has a 25-second network timeout. Discovery/search/filter/API paths are restricted. Website rules can change; the Actor fails conservatively and reports errors.

Defaults are intended for a small sub-120-second smoke run, not guaranteed latency. Large Ajmal runs necessarily take much longer because of its pacing. Set a matching hosted timeout and keep caps low during testing.

### Verified scope and testing

On 2026-10-06, one live product per brand plus both robots files returned HTTP 200 in a four-request bounded check. The Ajmal sample yielded AED 115.50 / 60 ml. Swiss Arabian yielded AED 240 / 75 ML and AED 110 / 25 ML. This is sample validation, not whole-site coverage.

The original public HTML/XML fixtures include Ajmal's 1,146-entry product sitemap, Ajmal product HTML, Swiss Arabian product HTML and one collection page. Collection page 2's link is tested against the fixture; page 2 itself has not been fetched. Sitemap entries are not a guarantee of current availability.

Local parser and safety tests use Python's standard library:

```sh
python -m unittest discover -s tests -v
```

Runtime needs the Apify Python SDK (requirements.txt). Local fixture tests do not need SDK installation. Source HTML fixtures stay outside the container build. No hosted build/run or marketplace publication is claimed by local tests.

# Actor input Schema

## `productUrls` (type: `array`):

Canonical HTTPS UAE product URLs, at most 50. Omit or clear this field for discovery-only when discoverAjmal or swissCollectionUrl is selected. With no URLs and no discovery selection, omitted input uses two sample products. No queries, APIs or search.

## `maxProducts` (type: `integer`):

Cap across both brands, explicit URLs first. Each Swiss Arabian product may yield multiple variant rows. Default 2.

## `maxPages` (type: `integer`):

Swiss Arabian collection discovery page cap; follows observed next links only. Ajmal discovery reads one fixed product sitemap.

## `maxRequests` (type: `integer`):

Hard global cap including robots, sitemap, collections, redirects and products. No retries. Default 8; small smoke inputs usually use 4.

## `discoverAjmal` (type: `boolean`):

Read Ajmal's public product sitemap, filling remaining maxProducts capacity after explicit URLs. Crawl spacing is at least 10 seconds.

## `swissCollectionUrl` (type: `string`):

Optional UAE collection URL, for example https://uae.swissarabian.com/collections/shaghaf. Fill remaining maxProducts capacity. Disallowed sale, search and filter URLs are rejected.

## Actor input object example

```json
{
  "productUrls": [
    "https://en-ae.ajmal.com/alf-laila-o-lail-spray",
    "https://uae.swissarabian.com/products/shaghaf-oud-azraq"
  ],
  "maxProducts": 2,
  "maxPages": 1,
  "maxRequests": 8,
  "discoverAjmal": false
}
```

# Actor output Schema

## `results` (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 = {
    "productUrls": [
        "https://en-ae.ajmal.com/alf-laila-o-lail-spray",
        "https://uae.swissarabian.com/products/shaghaf-oud-azraq"
    ]
};

// Run the Actor and wait for it to finish
const run = await client.actor("vixendata/uae-fragrance-catalog").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 = { "productUrls": [
        "https://en-ae.ajmal.com/alf-laila-o-lail-spray",
        "https://uae.swissarabian.com/products/shaghaf-oud-azraq",
    ] }

# Run the Actor and wait for it to finish
run = client.actor("vixendata/uae-fragrance-catalog").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 '{
  "productUrls": [
    "https://en-ae.ajmal.com/alf-laila-o-lail-spray",
    "https://uae.swissarabian.com/products/shaghaf-oud-azraq"
  ]
}' |
apify call vixendata/uae-fragrance-catalog --silent --output-dataset

```

## MCP server setup

```json
{
    "mcpServers": {
        "apify": {
            "type": "http",
            "url": "https://mcp.apify.com/?tools=fetch-actor-details,vixendata/uae-fragrance-catalog"
        }
    }
}
```

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/yIubwwexikJip615k/builds/Kmndm025blc4ivMfe/openapi.json
