Prime IndexInventory API v1.1.0openapi.json

Prime Index Inventory API

A read-only REST API that gives authorized banks normalized, published residential inventory from Saudi developers — projects, buildings, units, prices, availability and media — so they can build their own property financing marketplaces. The platform does not originate financing, approve loans or process payments.

Base URL   https://primeindex.app/api/v1
Format     JSON (UTF-8)
Version    v1 (additive changes only; breaking changes ship as /api/v2)

Authentication

Every request (except media downloads) must carry an API key issued to your institution by a Prime Index administrator. Keys are shown once at creation, stored only as hashes, and can be revoked instantly. Each bank receives its own credentials, independent from developer user accounts.

curl https://primeindex.app/api/v1/units \
  -H "Authorization: Bearer $PRIME_INDEX_API_KEY"

Call the API only from your servers. Never ship keys in web pages or mobile apps — expose inventory to customers through your own backend, as the demo marketplace does.

Access & visibility

You see inventory only from developers (or individual projects) your consumer has been explicitly granted, and only items that pass the public visibility policy: the developer is active, the project is published, the building (if any) is active and the unit is published. Drafts, archived records, internal notes, private documents and audit data are never returned. Out-of-scope IDs respond with 404, indistinguishable from non-existent ones.

Availability (AVAILABLE, RESERVED, SOLD, UNAVAILABLE) is a commercial attribute, not a visibility rule. GET /units defaults to AVAILABLE; the change feed always includes every status.

Money & units

Monetary values are objects with an exact decimal string and an ISO 4217 currency. Parse amounts with a decimal type (e.g. BigDecimal, decimal.Decimal, or integer halalas) — never binary floating point.

"price": { "amount": "1185000.00", "currency": "SAR" }

Areas are square metres (areaUnit: "sqm"). Dates are YYYY-MM-DD; timestamps are ISO-8601 UTC.

Languages

Content is available in English (en, default) and Arabic (ar). Pass lang=ar on any read endpoint, or send Accept-Language: ar; an explicit lang wins, and an unsupported value is a 400. Responses carry Content-Language.

Developer-written text (names, titles, descriptions, locations, features and amenities) is returned in the requested language and falls back to English field by field when no translation exists. availableLanguages on each unit, project and building tells you which languages the developer actually provided. Unit titles without an Arabic version are generated in Arabic from the unit's attributes. Enum values (propertyType, availability, …), IDs, numbers and money never change with the language; GET /metadata returns display labels for them in the requested language.

The city filter accepts the canonical English name or the Arabic name. Use cities[].name from metadata as the filter value and cities[].label for display.

curl "https://primeindex.app/api/v1/units/<id>?lang=ar" -H "Authorization: Bearer $KEY"

Pagination

List endpoints use opaque cursors. Pass pagination.nextCursor as cursor to fetch the next page; it is null on the last page. Ordering is deterministic (sort field, then ID), so pages never overlap. A cursor is only valid with the sort it was issued for. Page size: limit up to 100.

curl "https://primeindex.app/api/v1/units?city=Riyadh&propertyType=APARTMENT,PENTHOUSE&minBedrooms=3&maxPrice=2500000&sort=price&limit=50" \
  -H "Authorization: Bearer $KEY"

Synchronization

GET /units/changes is a change feed for keeping a local copy fresh. Every entry is either unit.upserted (store the embedded unit) or unit.removed (delete or hide your copy). The feed is ordered by a monotonic sequence; within a page each unit appears once with its latest state.

  1. Start without a cursor to receive every unit currently in your scope (a full sync).
  2. Keep requesting with nextCursor until hasMore is false.
  3. Persist nextCursor — it is returned even when data is empty.
  4. Poll with the stored cursor (every few minutes is typical).

Tombstones explain why a unit left your feed: unpublished, archived, project_unpublished, building_archived, developer_suspended or access_revoked. That is how you distinguish a removed unit from one that simply was not on a page. Price, availability, media, project and developer changes all produce new unit.upserted events. Page size up to 500. Webhooks are not available in v1.

Errors & rate limits

Errors share one envelope. Include requestId when contacting support.

HTTP/1.1 400 Bad Request
{
  "error": {
    "code": "invalid_request",
    "message": "Invalid query parameters",
    "details": { "limit": ["Too big: expected number to be <=100"] },
    "requestId": "8a4c…"
  }
}
400invalid_requestMalformed parameters or cursor
401unauthorizedMissing, invalid, expired or revoked key; suspended consumer
404not_foundUnknown or not accessible with this key
429rate_limitedPer-key limit exceeded; honour Retry-After
500internal_errorUnexpected error; safe to retry with backoff

Each key has a per-minute budget (default 120). Every response carries X-RateLimit-Limit, X-RateLimit-Remaining and X-RateLimit-Reset (epoch seconds).

Example: keeping a local copy in sync

A complete Node.js sync worker. The same loop performs the initial full sync and every incremental poll.

const API = "https://primeindex.app/api/v1";
const headers = { Authorization: `Bearer ${process.env.PRIME_INDEX_API_KEY}` };

export async function syncInventory(db) {
  let cursor = await db.getCursor();          // null on first run → full sync
  for (;;) {
    const url = new URL(`${API}/units/changes`);
    url.searchParams.set("limit", "500");
    if (cursor) url.searchParams.set("cursor", cursor);

    const res = await fetch(url, { headers });
    if (res.status === 429) { await sleep(Number(res.headers.get("retry-after") ?? 5) * 1000); continue; }
    if (!res.ok) throw new Error(`Sync failed: ${res.status}`);
    const { data, pagination } = await res.json();

    await db.transaction(async (tx) => {
      for (const event of data) {
        if (event.type === "unit.upserted") await tx.upsertUnit(event.unit);   // price.amount is a decimal string
        else await tx.removeUnit(event.id, event.reason);                     // tombstone
      }
      await tx.saveCursor(pagination.nextCursor);   // persist atomically with the data
    });
    cursor = pagination.nextCursor;
    if (!pagination.hasMore) return;
  }
}

Endpoint reference

get/api/v1/units

List published units

Paginated list of published units you are authorized to see. Defaults to `availability=AVAILABLE`; pass `availability=all` or a comma-separated list for other statuses.

ParameterTypeDescription
lang(query)stringContent language (`en` default, `ar`). Missing Arabic fields fall back to English. `Accept-Language` is honoured when omitted.
projectId(query)string (uuid)
developerId(query)string (uuid)Developer (organization) ID
buildingId(query)string (uuid)
city(query)stringCity name in English or Arabic
district(query)string
propertyType(query)stringComma-separated, e.g. APARTMENT,PENTHOUSE
availability(query)"all" | stringComma-separated statuses or `all`. Defaults to AVAILABLE.
minPrice(query)stringMinimum asking price (inclusive), decimal string
maxPrice(query)stringMaximum asking price (inclusive), decimal string
minBedrooms(query)integer
maxBedrooms(query)integer
updatedSince(query)string (date-time)Only units whose public representation changed at or after this ISO-8601 timestamp
sort(query)-updated_at | updated_at | price | -price | -published_at`-updated_at` (default), `updated_at`, `price`, `-price`, `-published_at`
limit(query)integerPage size (1–100, default 20)
cursor(query)string`pagination.nextCursor` from the previous page (same `sort` required)
  • 200 A page of units
  • 400 Invalid request parameters
  • 401 Missing, invalid, expired or revoked API key
  • 429 Rate limit exceeded. See `Retry-After` and `X-RateLimit-*` headers.
{
  "data": [
    {
      "id": "0199f3a2-7c1e-7b55-9a1f-4f2d8c6a1e01",
      "object": "unit",
      "developerReference": "GVR-A-0302",
      "unitNumber": "A302",
      "propertyType": "APARTMENT",
      "title": "3-Bedroom Apartment in Green Valley Residences",
      "description": "Corner apartment with a private balcony overlooking the central gardens.",
      "availability": "AVAILABLE",
      "availableFrom": "2026-12-01",
      "price": {
        "amount": "1185000.00",
        "currency": "SAR"
      },
      "appraisal": {
        "value": {
          "amount": "1150000.00",
          "currency": "SAR"
        },
        "validUntil": "2027-03-31"
      },
      "specifications": {
        "bedrooms": 3,
        "bathrooms": 3,
        "livingRooms": 1,
        "kitchens": 1,
        "parkingSpaces": 1,
        "floorNumber": 3,
        "levels": 1,
        "hasMaidRoom": true,
        "hasDriverRoom": false,
        "internalArea": 168.5,
        "externalArea": 12,
        "totalArea": 180.5,
        "landArea": null,
        "areaUnit": "sqm",
        "furnishing": "UNFURNISHED",
        "finishing": "FULLY_FINISHED",
        "orientation": "SOUTH_EAST",
        "view": "Garden view"
      },
      "features": [
        "Central AC",
        "Private balcony"
      ],
      "amenities": [
        "Swimming pool",
        "Gym"
      ],
      "developer": {
        "id": "0199f3a2-0000-7000-8000-000000000001",
        "name": "Nakhla Urban Developments",
        "slug": "nakhla",
        "logoUrl": null
      },
      "project": {
        "id": "0199f3a2-0000-7000-8000-000000000010",
        "name": "Green Valley Residences",
        "city": "Riyadh",
        "district": "Al Malqa"
      },
      "building": {
        "id": "0199f3a2-0000-7000-8000-000000000020",
        "name": "Building A"
      },
      "location": {
        "country": "SA",
        "region": "Riyadh Region",
        "city": "Riyadh",
        "district": "Al Malqa",
        "address": "Anas Ibn Malik Rd",
        "latitude": 24.8093,
        "longitude": 46.6156
      },
      "media": [
        {
          "id": "6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d",
          "kind": "IMAGE",
          "category": "COVER_IMAGE",
          "title": null,
          "altText": "Living room",
          "mimeType": "image/webp",
          "width": 2400,
          "height": 1600,
          "url": "https://inventory.example.com/api/v1/media/6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d?size=md",
          "renditions": {
            "sm": {
              "url": "https://inventory.example.com/api/v1/media/6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d?size=sm",
              "width": 480,
              "height": 320
            }
          }
        }
      ],
      "publishedAt": "2026-10-01T09:30:00.000Z",
      "updatedAt": "2026-10-20T14:05:11.000Z"
    }
  ],
  "pagination": {
    "limit": 20,
    "hasMore": true,
    "nextCursor": "eyJzIjoiLXVwZGF0ZWRfYXQiLC4uLn0"
  }
}
get/api/v1/units/{id}

Get a unit

ParameterTypeDescription
id*(path)string (uuid)Unit ID
lang(query)stringContent language (`en` default, `ar`). `Accept-Language` is honoured when omitted.
  • 200 The unit
  • 400 Invalid request parameters
  • 401 Missing, invalid, expired or revoked API key
  • 404 Not found or not accessible with this key
  • 429 Rate limit exceeded. See `Retry-After` and `X-RateLimit-*` headers.
{
  "data": {
    "id": "0199f3a2-7c1e-7b55-9a1f-4f2d8c6a1e01",
    "object": "unit",
    "developerReference": "GVR-A-0302",
    "unitNumber": "A302",
    "propertyType": "APARTMENT",
    "title": "3-Bedroom Apartment in Green Valley Residences",
    "description": "Corner apartment with a private balcony overlooking the central gardens.",
    "availability": "AVAILABLE",
    "availableFrom": "2026-12-01",
    "price": {
      "amount": "1185000.00",
      "currency": "SAR"
    },
    "appraisal": {
      "value": {
        "amount": "1150000.00",
        "currency": "SAR"
      },
      "validUntil": "2027-03-31"
    },
    "specifications": {
      "bedrooms": 3,
      "bathrooms": 3,
      "livingRooms": 1,
      "kitchens": 1,
      "parkingSpaces": 1,
      "floorNumber": 3,
      "levels": 1,
      "hasMaidRoom": true,
      "hasDriverRoom": false,
      "internalArea": 168.5,
      "externalArea": 12,
      "totalArea": 180.5,
      "landArea": null,
      "areaUnit": "sqm",
      "furnishing": "UNFURNISHED",
      "finishing": "FULLY_FINISHED",
      "orientation": "SOUTH_EAST",
      "view": "Garden view"
    },
    "features": [
      "Central AC",
      "Private balcony"
    ],
    "amenities": [
      "Swimming pool",
      "Gym"
    ],
    "developer": {
      "id": "0199f3a2-0000-7000-8000-000000000001",
      "name": "Nakhla Urban Developments",
      "slug": "nakhla",
      "logoUrl": null
    },
    "project": {
      "id": "0199f3a2-0000-7000-8000-000000000010",
      "name": "Green Valley Residences",
      "city": "Riyadh",
      "district": "Al Malqa"
    },
    "building": {
      "id": "0199f3a2-0000-7000-8000-000000000020",
      "name": "Building A"
    },
    "location": {
      "country": "SA",
      "region": "Riyadh Region",
      "city": "Riyadh",
      "district": "Al Malqa",
      "address": "Anas Ibn Malik Rd",
      "latitude": 24.8093,
      "longitude": 46.6156
    },
    "media": [
      {
        "id": "6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d",
        "kind": "IMAGE",
        "category": "COVER_IMAGE",
        "title": null,
        "altText": "Living room",
        "mimeType": "image/webp",
        "width": 2400,
        "height": 1600,
        "url": "https://inventory.example.com/api/v1/media/6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d?size=md",
        "renditions": {
          "sm": {
            "url": "https://inventory.example.com/api/v1/media/6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d?size=sm",
            "width": 480,
            "height": 320
          }
        }
      }
    ],
    "publishedAt": "2026-10-01T09:30:00.000Z",
    "updatedAt": "2026-10-20T14:05:11.000Z"
  }
}
get/api/v1/units/changes

Incremental unit changes

Ordered change feed. Each entry is either `unit.upserted` (store the embedded unit) or `unit.removed` (delete your copy). A unit appears at most once per page with its latest state. `nextCursor` is always returned — persist it even when `data` is empty.

ParameterTypeDescription
lang(query)stringContent language (`en` default, `ar`). Missing Arabic fields fall back to English. `Accept-Language` is honoured when omitted.
cursor(query)stringOmit (or use `0`) to start a full sync from the beginning.
limit(query)integerPage size (1–500, default 100)
  • 200 A page of change events
  • 400 Invalid request parameters
  • 401 Missing, invalid, expired or revoked API key
  • 429 Rate limit exceeded. See `Retry-After` and `X-RateLimit-*` headers.
{
  "data": [
    {
      "type": "unit.upserted",
      "id": "0199f3a2-7c1e-7b55-9a1f-4f2d8c6a1e01",
      "sequence": "1042",
      "occurredAt": "2026-10-20T14:05:11.000Z",
      "unit": {
        "id": "0199f3a2-7c1e-7b55-9a1f-4f2d8c6a1e01",
        "object": "unit",
        "developerReference": "GVR-A-0302",
        "unitNumber": "A302",
        "propertyType": "APARTMENT",
        "title": "3-Bedroom Apartment in Green Valley Residences",
        "description": "Corner apartment with a private balcony overlooking the central gardens.",
        "availability": "AVAILABLE",
        "availableFrom": "2026-12-01",
        "price": {
          "amount": "1185000.00",
          "currency": "SAR"
        },
        "appraisal": {
          "value": {
            "amount": "1150000.00",
            "currency": "SAR"
          },
          "validUntil": "2027-03-31"
        },
        "specifications": {
          "bedrooms": 3,
          "bathrooms": 3,
          "livingRooms": 1,
          "kitchens": 1,
          "parkingSpaces": 1,
          "floorNumber": 3,
          "levels": 1,
          "hasMaidRoom": true,
          "hasDriverRoom": false,
          "internalArea": 168.5,
          "externalArea": 12,
          "totalArea": 180.5,
          "landArea": null,
          "areaUnit": "sqm",
          "furnishing": "UNFURNISHED",
          "finishing": "FULLY_FINISHED",
          "orientation": "SOUTH_EAST",
          "view": "Garden view"
        },
        "features": [
          "Central AC",
          "Private balcony"
        ],
        "amenities": [
          "Swimming pool",
          "Gym"
        ],
        "developer": {
          "id": "0199f3a2-0000-7000-8000-000000000001",
          "name": "Nakhla Urban Developments",
          "slug": "nakhla",
          "logoUrl": null
        },
        "project": {
          "id": "0199f3a2-0000-7000-8000-000000000010",
          "name": "Green Valley Residences",
          "city": "Riyadh",
          "district": "Al Malqa"
        },
        "building": {
          "id": "0199f3a2-0000-7000-8000-000000000020",
          "name": "Building A"
        },
        "location": {
          "country": "SA",
          "region": "Riyadh Region",
          "city": "Riyadh",
          "district": "Al Malqa",
          "address": "Anas Ibn Malik Rd",
          "latitude": 24.8093,
          "longitude": 46.6156
        },
        "media": [
          {
            "id": "6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d",
            "kind": "IMAGE",
            "category": "COVER_IMAGE",
            "title": null,
            "altText": "Living room",
            "mimeType": "image/webp",
            "width": 2400,
            "height": 1600,
            "url": "https://inventory.example.com/api/v1/media/6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d?size=md",
            "renditions": {
              "sm": {
                "url": "https://inventory.example.com/api/v1/media/6f1c1f0e-3d7b-4b8e-9d6a-2c1b0a9f8e7d?size=sm",
                "width": 480,
                "height": 320
              }
            }
          }
        ],
        "publishedAt": "2026-10-01T09:30:00.000Z",
        "updatedAt": "2026-10-20T14:05:11.000Z"
      }
    },
    {
      "type": "unit.removed",
      "id": "0199f3a2-7c1e-7b55-9a1f-4f2d8c6a1e02",
      "sequence": "1043",
      "occurredAt": "2026-10-20T14:07:40.000Z",
      "reason": "unpublished"
    }
  ],
  "pagination": {
    "limit": 100,
    "hasMore": false,
    "nextCursor": "c1_1043"
  }
}
get/api/v1/projects

List published projects

ParameterTypeDescription
lang(query)stringContent language (`en` default, `ar`). Missing Arabic fields fall back to English. `Accept-Language` is honoured when omitted.
developerId(query)string (uuid)Developer (organization) ID
city(query)string
limit(query)integer
cursor(query)string
  • 200 A page of projects
  • 400 Invalid request parameters
  • 401 Missing, invalid, expired or revoked API key
  • 429 Rate limit exceeded. See `Retry-After` and `X-RateLimit-*` headers.
get/api/v1/projects/{id}

Get a project

ParameterTypeDescription
id*(path)string (uuid)Project ID
lang(query)stringContent language (`en` default, `ar`). `Accept-Language` is honoured when omitted.
  • 200 The project
  • 400 Invalid request parameters
  • 401 Missing, invalid, expired or revoked API key
  • 404 Not found or not accessible with this key
  • 429 Rate limit exceeded. See `Retry-After` and `X-RateLimit-*` headers.
get/api/v1/projects/{id}/buildings

List a project's buildings

Active buildings of a published project with published-unit counts. Projects without buildings return an empty list.

ParameterTypeDescription
id*(path)string (uuid)Project ID
lang(query)stringContent language (`en` default, `ar`). `Accept-Language` is honoured when omitted.
  • 200 Buildings
  • 400 Invalid request parameters
  • 401 Missing, invalid, expired or revoked API key
  • 404 Not found or not accessible with this key
  • 429 Rate limit exceeded. See `Retry-After` and `X-RateLimit-*` headers.
get/api/v1/metadata

Filter metadata

Property types, currencies, cities/districts, developers and price/bedroom ranges across your visible inventory. Labels follow `lang`.

ParameterTypeDescription
lang(query)stringContent language (`en` default, `ar`). `Accept-Language` is honoured when omitted.
  • 200 Metadata
  • 400 Invalid request parameters
  • 401 Missing, invalid, expired or revoked API key
  • 429 Rate limit exceeded. See `Retry-After` and `X-RateLimit-*` headers.
get/api/v1/media/{id}no API key

Download published media

Binary download of a published photo, floor plan or shared document. URLs are returned by other endpoints; no API key is required so they can be used directly in `<img>` tags. Media is only served while its owner is published.

ParameterTypeDescription
id*(path)string (uuid)Media ID
size(query)sm | md | lg | xl
  • 200 The file
  • 404 Not found or not published