{
  "openapi": "3.1.0",
  "info": {
    "title": "Vatra Homes Listings API",
    "version": "1.1.0",
    "summary": "Published property listings, incremental sync, and enquiry capture.",
    "description": "The Vatra Homes Listings API serves a managed portfolio's published listings to portals, partner sites and internal tools.\n\nThe API serves exactly the properties an owner has published for listing — the ones carrying is_published in the portfolio, surfaced through the Share Config screen. Unpublished, sold and internal-only units are never returned, and no tenant, contract, cheque or financial record is reachable through any endpoint on this API.\n\n## Authentication\n\nEvery endpoint except GET /v1/health requires an API key, sent in the x-api-key header. Keys are issued from the Vatra Homes super admin console and are shown once at creation — only a SHA-256 hash is stored, so a lost key must be replaced rather than recovered.\n\n- Keys are scoped. A request for an endpoint outside the key's scopes is rejected with 403 and never touches the database.\n- A key can carry an expiry date. Requests after that moment are rejected with 401.\n- A revoked key stops working immediately — revocation is a status change, not a delete, so its request history stays intact.\n- No Supabase anon key, bearer token or cookie is needed. The API key is the whole credential.\n\n## Scopes\n\n- `listings:read` — Read published listings and the incremental sync feed.\n- `inquiries:write` — Submit an enquiry against a published listing.\n\n## Rate limits\n\nEach key carries its own quota, counted over a rolling 24-hour window. Every authenticated response — including errors — carries the three headers below, so a client can back off before it is refused. Exceeding the quota returns 429 with a Retry-After header; the window is rolling, so capacity returns gradually rather than all at once on a fixed reset.\n\n## Envelope\n\nEvery response is a JSON object carrying `success` and `meta`. Successful responses carry `data`; failures carry `error` as an object with a stable `code` and a human `message`. Collection endpoints repeat `total`, `limit` and `offset` at the top level for the first release's consumers; `meta` is canonical.",
    "contact": {
      "name": "Vatra Homes",
      "email": "contact@vatra.homes"
    }
  },
  "servers": [
    {
      "url": "https://scxbhdciuguloaplqzqm.supabase.co/functions/v1/listings-api",
      "description": "Production"
    }
  ],
  "tags": [
    {
      "name": "Listings",
      "description": "Published listings, sync feed and enquiries."
    },
    {
      "name": "Service",
      "description": "Unauthenticated service endpoints."
    }
  ],
  "security": [
    {
      "ApiKeyAuth": []
    }
  ],
  "paths": {
    "/v1/health": {
      "get": {
        "operationId": "health",
        "summary": "Health check",
        "description": "Liveness probe. The only endpoint that needs no API key.\n\nReturns the running API version and the server clock. Use it to confirm connectivity and to verify you are pointed at the right base URL before you start debugging credentials.",
        "tags": [
          "Service"
        ],
        "security": [],
        "parameters": [],
        "responses": {
          "200": {
            "description": "Liveness probe. The only endpoint that needs no API key.",
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/Health"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "status": "ok",
                    "version": "1.1.0",
                    "time": "2026-09-18T17:05:36.581Z"
                  },
                  "meta": {
                    "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                    "timestamp": "2026-09-18T17:05:36.581Z"
                  }
                }
              }
            }
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        }
      }
    },
    "/v1/me": {
      "get": {
        "operationId": "me",
        "summary": "Inspect the calling key",
        "description": "What this key is allowed to do, and how much of its quota is left.\n\nReturns the key's name, prefix, scopes, quota and current usage in the rolling window. Requires a valid key but no particular scope, which makes it the right first call when wiring up a new integration: it proves the credential works without depending on any data being published.",
        "tags": [
          "Listings"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [],
        "responses": {
          "200": {
            "description": "What this key is allowed to do, and how much of its quota is left.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed for this key per 24-hour window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds at which the oldest counted request leaves the window.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/KeyInfo"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "name": "Portal Sync",
                    "key_prefix": "pk_live_ffba...",
                    "scopes": [
                      "listings:read",
                      "inquiries:write"
                    ],
                    "status": "active",
                    "rate_limit": 1000,
                    "window_seconds": 86400,
                    "usage": {
                      "used": 12,
                      "remaining": 988,
                      "reset_at": "2026-09-19T17:05:36.581Z"
                    },
                    "expires_at": null,
                    "created_at": "2026-03-10T11:26:06.838Z"
                  },
                  "meta": {
                    "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                    "timestamp": "2026-09-18T17:05:36.581Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-required-scope": "any valid key"
      }
    },
    "/v1/listings": {
      "get": {
        "operationId": "listings",
        "summary": "List published listings",
        "description": "The full published catalogue, filtered and paged.\n\nReturns published listings newest first. meta.total is the number of listings matching your filters across all pages — not the size of the page you are holding — so it is safe to drive a pager from it. Every filter is applied in the database before paging, so page 2 of a filtered query contains what you expect.",
        "tags": [
          "Listings"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "type",
            "in": "query",
            "required": false,
            "description": "rent or sale.",
            "schema": {
              "type": "string"
            },
            "example": "rent"
          },
          {
            "name": "bedrooms",
            "in": "query",
            "required": false,
            "description": "Minimum bedrooms. A studio is 0.",
            "schema": {
              "type": "integer"
            },
            "example": "2"
          },
          {
            "name": "area",
            "in": "query",
            "required": false,
            "description": "Case-insensitive partial match on the community area, e.g. Marina matches Dubai Marina.",
            "schema": {
              "type": "string"
            },
            "example": "Dubai Marina"
          },
          {
            "name": "min_price",
            "in": "query",
            "required": false,
            "description": "Minimum listing price in AED.",
            "schema": {
              "type": "number"
            },
            "example": "50000"
          },
          {
            "name": "max_price",
            "in": "query",
            "required": false,
            "description": "Maximum listing price in AED.",
            "schema": {
              "type": "number"
            },
            "example": "250000"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–100. Default 20. Non-numeric values are rejected with 400.",
            "schema": {
              "type": "integer"
            },
            "example": "20"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip. Default 0. Must not be negative.",
            "schema": {
              "type": "integer"
            },
            "example": "0"
          }
        ],
        "responses": {
          "200": {
            "description": "The full published catalogue, filtered and paged.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed for this key per 24-hour window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds at which the oldest counted request leaves the window.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ListingSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    },
                    "total": {
                      "type": "integer",
                      "deprecated": true,
                      "description": "Mirrors meta.total. Kept for the first release's consumers."
                    },
                    "limit": {
                      "type": "integer",
                      "deprecated": true,
                      "description": "Mirrors meta.limit."
                    },
                    "offset": {
                      "type": "integer",
                      "deprecated": true,
                      "description": "Mirrors meta.offset."
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "sunset-2a",
                      "unit": "Apt 2A",
                      "building": "Sunset Apartments",
                      "area": "Dubai Marina",
                      "developer": "Ellington Properties",
                      "type": "rent",
                      "bedrooms": 0,
                      "bathrooms": 1,
                      "sqft": 520,
                      "price": 52000,
                      "description": "Bright studio on the second floor, fully fitted kitchen, chiller-free, one minute from the tram. Available now.",
                      "parking": [],
                      "published_at": "2026-09-06T09:00:00+00:00"
                    }
                  ],
                  "meta": {
                    "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                    "timestamp": "2026-09-18T17:05:36.581Z",
                    "total": 3,
                    "returned": 1,
                    "limit": 1,
                    "offset": 0,
                    "has_more": true
                  },
                  "total": 3,
                  "limit": 1,
                  "offset": 0
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-required-scope": "listings:read"
      }
    },
    "/v1/listings/{id}": {
      "get": {
        "operationId": "listing",
        "summary": "Get one listing",
        "description": "One listing in full, including its gallery.\n\nEverything the summary carries, plus address, view, ownership, building amenities and the media gallery. Gallery URLs are signed server-side and expire one hour after the response is generated — download or re-fetch rather than storing them. A listing that exists but is not published returns 404, exactly like an unknown id.",
        "tags": [
          "Listings"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "id",
            "in": "path",
            "required": true,
            "description": "The listing id returned by GET /v1/listings.",
            "schema": {
              "type": "string"
            },
            "example": "sunset-2a"
          }
        ],
        "responses": {
          "200": {
            "description": "One listing in full, including its gallery.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed for this key per 24-hour window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds at which the oldest counted request leaves the window.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/ListingDetail"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "sunset-2a",
                    "unit": "Apt 2A",
                    "building": "Sunset Apartments",
                    "developer": "Ellington Properties",
                    "area": "Dubai Marina",
                    "address": "Sunset Apartments, Unit 2A, Dubai Marina",
                    "type": "rent",
                    "bedrooms": 0,
                    "bathrooms": 1,
                    "sqft": 520,
                    "view": "Community View",
                    "ownership": "Freehold",
                    "price": 52000,
                    "description": "Bright studio on the second floor, fully fitted kitchen, chiller-free, one minute from the tram.",
                    "amenities": [
                      "infinity_pool",
                      "gym",
                      "concierge"
                    ],
                    "parking": [],
                    "gallery": [
                      {
                        "url": "https://…/storage/v1/object/sign/property-media/…",
                        "type": "photo",
                        "caption": "Living area",
                        "is_cover": true
                      }
                    ],
                    "published_at": "2026-09-06T09:00:00+00:00"
                  },
                  "meta": {
                    "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                    "timestamp": "2026-09-18T17:05:36.581Z"
                  }
                }
              }
            }
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-required-scope": "listings:read"
      }
    },
    "/v1/inquiries": {
      "post": {
        "operationId": "inquiries",
        "summary": "Submit an enquiry",
        "description": "Hand a prospective tenant or buyer back to the managing owner.\n\nRecords an enquiry against a published listing and surfaces it in the owner's activity feed. property_id must name a currently published listing; anything else is refused with 404 rather than silently stored. The response carries the created enquiry id so you can reconcile duplicates on your side.",
        "tags": [
          "Listings"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [],
        "responses": {
          "201": {
            "description": "Hand a prospective tenant or buyer back to the managing owner.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed for this key per 24-hour window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds at which the oldest counted request leaves the window.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "$ref": "#/components/schemas/InquiryResult"
                    },
                    "meta": {
                      "$ref": "#/components/schemas/Meta"
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": {
                    "id": "a9e501bb-43a9-4741-9d92-29d88675ec32",
                    "property_id": "sunset-2a",
                    "received_at": "2026-09-18T17:06:37.027Z"
                  },
                  "meta": {
                    "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                    "timestamp": "2026-09-18T17:06:37.027Z"
                  }
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "404": {
            "$ref": "#/components/responses/NotFound"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "requestBody": {
          "required": true,
          "content": {
            "application/json": {
              "schema": {
                "$ref": "#/components/schemas/InquiryRequest"
              },
              "example": {
                "property_id": "sunset-2a",
                "name": "Layla Haddad",
                "email": "layla@example.com",
                "phone": "+971 50 123 4567",
                "message": "Is the studio still available for a September move-in?"
              }
            }
          }
        },
        "x-required-scope": "inquiries:write"
      }
    },
    "/v1/sync/updated": {
      "get": {
        "operationId": "sync",
        "summary": "Incremental sync",
        "description": "Listings published since a timestamp you supply.\n\nReturns listings whose published_at is at or after since, oldest change last. Store meta.sync_timestamp from each response and pass it back as since on the next run. This feed reports additions and re-publications only: a listing withdrawn from the market stops appearing in GET /v1/listings but does not appear here as a deletion, so reconcile against the full catalogue periodically — daily is enough for most portals.",
        "tags": [
          "Listings"
        ],
        "security": [
          {
            "ApiKeyAuth": []
          }
        ],
        "parameters": [
          {
            "name": "since",
            "in": "query",
            "required": true,
            "description": "ISO 8601 timestamp, e.g. 2026-09-01T00:00:00Z. An unparseable value is rejected with 400.",
            "schema": {
              "type": "string"
            },
            "example": "2026-09-01T00:00:00Z"
          },
          {
            "name": "limit",
            "in": "query",
            "required": false,
            "description": "Page size, 1–500. Default 100.",
            "schema": {
              "type": "integer"
            },
            "example": "100"
          },
          {
            "name": "offset",
            "in": "query",
            "required": false,
            "description": "Rows to skip. Default 0.",
            "schema": {
              "type": "integer"
            },
            "example": "0"
          }
        ],
        "responses": {
          "200": {
            "description": "Listings published since a timestamp you supply.",
            "headers": {
              "X-RateLimit-Limit": {
                "description": "Requests allowed for this key per 24-hour window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Remaining": {
                "description": "Requests still available in the current window.",
                "schema": {
                  "type": "integer"
                }
              },
              "X-RateLimit-Reset": {
                "description": "Unix seconds at which the oldest counted request leaves the window.",
                "schema": {
                  "type": "integer"
                }
              }
            },
            "content": {
              "application/json": {
                "schema": {
                  "type": "object",
                  "required": [
                    "success",
                    "data",
                    "meta"
                  ],
                  "properties": {
                    "success": {
                      "type": "boolean",
                      "enum": [
                        true
                      ]
                    },
                    "data": {
                      "type": "array",
                      "items": {
                        "$ref": "#/components/schemas/ListingSummary"
                      }
                    },
                    "meta": {
                      "$ref": "#/components/schemas/PaginationMeta"
                    },
                    "sync_timestamp": {
                      "type": "string",
                      "format": "date-time",
                      "deprecated": true,
                      "description": "Mirrors meta.sync_timestamp."
                    }
                  }
                },
                "example": {
                  "success": true,
                  "data": [
                    {
                      "id": "sunset-2a",
                      "unit": "Apt 2A",
                      "building": "Sunset Apartments",
                      "area": "Dubai Marina",
                      "developer": "Ellington Properties",
                      "type": "rent",
                      "bedrooms": 0,
                      "bathrooms": 1,
                      "sqft": 520,
                      "price": 52000,
                      "description": "Bright studio on the second floor, fully fitted kitchen, chiller-free, one minute from the tram. Available now.",
                      "parking": [],
                      "published_at": "2026-09-06T09:00:00+00:00"
                    }
                  ],
                  "meta": {
                    "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                    "timestamp": "2026-09-18T17:05:36.581Z",
                    "sync_timestamp": "2026-09-18T17:05:36.581Z",
                    "total": 3,
                    "returned": 3,
                    "limit": 100,
                    "offset": 0,
                    "has_more": false
                  },
                  "sync_timestamp": "2026-09-18T17:05:36.581Z"
                }
              }
            }
          },
          "400": {
            "$ref": "#/components/responses/BadRequest"
          },
          "401": {
            "$ref": "#/components/responses/Unauthorized"
          },
          "403": {
            "$ref": "#/components/responses/Forbidden"
          },
          "405": {
            "$ref": "#/components/responses/MethodNotAllowed"
          },
          "429": {
            "$ref": "#/components/responses/RateLimited"
          },
          "500": {
            "$ref": "#/components/responses/ServerError"
          }
        },
        "x-required-scope": "listings:read"
      }
    }
  },
  "components": {
    "securitySchemes": {
      "ApiKeyAuth": {
        "type": "apiKey",
        "in": "header",
        "name": "x-api-key",
        "description": "Your API key, issued from the Vatra Homes console. Format: pk_live_…"
      }
    },
    "schemas": {
      "Meta": {
        "type": "object",
        "description": "Present on every response. request_id identifies this exact request in our logs.",
        "required": [
          "request_id",
          "timestamp"
        ],
        "properties": {
          "request_id": {
            "type": "string",
            "format": "uuid"
          },
          "timestamp": {
            "type": "string",
            "format": "date-time"
          }
        },
        "additionalProperties": true
      },
      "PaginationMeta": {
        "allOf": [
          {
            "$ref": "#/components/schemas/Meta"
          },
          {
            "type": "object",
            "properties": {
              "total": {
                "type": "integer",
                "description": "Rows matching the query across all pages."
              },
              "returned": {
                "type": "integer",
                "description": "Rows in this response."
              },
              "limit": {
                "type": "integer"
              },
              "offset": {
                "type": "integer"
              },
              "has_more": {
                "type": "boolean",
                "description": "True when offset + returned is below total."
              }
            }
          }
        ]
      },
      "Error": {
        "type": "object",
        "required": [
          "success",
          "error",
          "meta"
        ],
        "properties": {
          "success": {
            "type": "boolean",
            "enum": [
              false
            ]
          },
          "error": {
            "type": "object",
            "required": [
              "code",
              "message"
            ],
            "properties": {
              "code": {
                "type": "string",
                "enum": [
                  "missing_api_key",
                  "invalid_api_key",
                  "key_revoked",
                  "key_expired",
                  "insufficient_scope",
                  "invalid_request",
                  "invalid_json",
                  "not_found",
                  "method_not_allowed",
                  "rate_limit_exceeded",
                  "internal_error"
                ],
                "description": "Stable. Branch on this."
              },
              "message": {
                "type": "string",
                "description": "Written for a human. Do not parse."
              }
            }
          },
          "meta": {
            "$ref": "#/components/schemas/Meta"
          }
        }
      },
      "ParkingSpace": {
        "type": "object",
        "properties": {
          "number": {
            "type": "string",
            "examples": [
              "P1-031"
            ]
          },
          "type": {
            "type": "string",
            "examples": [
              "Covered"
            ]
          }
        },
        "additionalProperties": true
      },
      "GalleryItem": {
        "type": "object",
        "properties": {
          "url": {
            "type": "string",
            "format": "uri",
            "description": "Signed URL, valid for one hour from the response."
          },
          "type": {
            "type": "string",
            "examples": [
              "photo"
            ]
          },
          "caption": {
            "type": [
              "string",
              "null"
            ]
          },
          "is_cover": {
            "type": [
              "boolean",
              "null"
            ]
          }
        }
      },
      "ListingSummary": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "examples": [
              "sunset-2a"
            ]
          },
          "unit": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "Apt 2A"
            ]
          },
          "building": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "Sunset Apartments"
            ]
          },
          "area": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "Dubai Marina"
            ]
          },
          "developer": {
            "type": [
              "string",
              "null"
            ],
            "examples": [
              "Ellington Properties"
            ]
          },
          "type": {
            "type": "string",
            "enum": [
              "rent",
              "sale"
            ]
          },
          "bedrooms": {
            "type": [
              "integer",
              "null"
            ],
            "description": "0 for a studio."
          },
          "bathrooms": {
            "type": [
              "integer",
              "null"
            ]
          },
          "sqft": {
            "type": [
              "integer",
              "null"
            ]
          },
          "price": {
            "type": "number",
            "description": "AED. Falls back to the property's current value when no listing price is set."
          },
          "description": {
            "type": [
              "string",
              "null"
            ]
          },
          "parking": {
            "type": "array",
            "items": {
              "$ref": "#/components/schemas/ParkingSpace"
            }
          },
          "published_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "ListingDetail": {
        "allOf": [
          {
            "$ref": "#/components/schemas/ListingSummary"
          },
          {
            "type": "object",
            "properties": {
              "address": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "view": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "ownership": {
                "type": [
                  "string",
                  "null"
                ]
              },
              "amenities": {
                "type": "array",
                "items": {
                  "type": "string"
                }
              },
              "gallery": {
                "type": "array",
                "items": {
                  "$ref": "#/components/schemas/GalleryItem"
                }
              }
            }
          }
        ]
      },
      "Health": {
        "type": "object",
        "properties": {
          "status": {
            "type": "string",
            "enum": [
              "ok"
            ]
          },
          "version": {
            "type": "string",
            "examples": [
              "1.1.0"
            ]
          },
          "time": {
            "type": "string",
            "format": "date-time"
          }
        }
      },
      "KeyInfo": {
        "type": "object",
        "properties": {
          "name": {
            "type": "string"
          },
          "key_prefix": {
            "type": "string",
            "description": "The first characters of the key. The key itself is never returned."
          },
          "scopes": {
            "type": "array",
            "items": {
              "type": "string",
              "enum": [
                "listings:read",
                "inquiries:write"
              ]
            }
          },
          "status": {
            "type": "string",
            "enum": [
              "active",
              "revoked"
            ]
          },
          "rate_limit": {
            "type": "integer"
          },
          "window_seconds": {
            "type": "integer",
            "examples": [
              86400
            ]
          },
          "usage": {
            "type": "object",
            "properties": {
              "used": {
                "type": "integer",
                "description": "Requests in the current window, including this one."
              },
              "remaining": {
                "type": "integer"
              },
              "reset_at": {
                "type": "string",
                "format": "date-time"
              }
            }
          },
          "expires_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          },
          "created_at": {
            "type": [
              "string",
              "null"
            ],
            "format": "date-time"
          }
        }
      },
      "InquiryRequest": {
        "type": "object",
        "required": [
          "property_id",
          "name",
          "email"
        ],
        "properties": {
          "property_id": {
            "type": "string",
            "description": "Id of a currently published listing."
          },
          "name": {
            "type": "string",
            "minLength": 2,
            "maxLength": 120
          },
          "email": {
            "type": "string",
            "format": "email",
            "maxLength": 200
          },
          "phone": {
            "type": "string",
            "maxLength": 40
          },
          "message": {
            "type": "string",
            "maxLength": 2000
          }
        },
        "additionalProperties": false
      },
      "InquiryResult": {
        "type": "object",
        "properties": {
          "id": {
            "type": "string",
            "format": "uuid",
            "description": "Use it to reconcile duplicates on your side."
          },
          "property_id": {
            "type": "string"
          },
          "received_at": {
            "type": "string",
            "format": "date-time"
          }
        }
      }
    },
    "responses": {
      "BadRequest": {
        "description": "`invalid_request` — A parameter or body field is missing or malformed. See error.message.\n\n`invalid_json` — The request body was not valid JSON.",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Requests allowed for this key per 24-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests still available in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the oldest counted request leaves the window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": {
                "code": "invalid_request",
                "message": "A parameter or body field is missing or malformed. See error.message."
              },
              "meta": {
                "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                "timestamp": "2026-09-18T17:05:36.581Z"
              }
            }
          }
        }
      },
      "Unauthorized": {
        "description": "`missing_api_key` — No x-api-key header was sent.\n\n`invalid_api_key` — The key is not recognised.\n\n`key_revoked` — The key has been revoked in the console.\n\n`key_expired` — The key passed its expiry date.",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Requests allowed for this key per 24-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests still available in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the oldest counted request leaves the window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": {
                "code": "missing_api_key",
                "message": "No x-api-key header was sent."
              },
              "meta": {
                "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                "timestamp": "2026-09-18T17:05:36.581Z"
              }
            }
          }
        }
      },
      "Forbidden": {
        "description": "`insufficient_scope` — The key is valid but lacks the scope this endpoint requires.",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Requests allowed for this key per 24-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests still available in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the oldest counted request leaves the window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": {
                "code": "insufficient_scope",
                "message": "The key is valid but lacks the scope this endpoint requires."
              },
              "meta": {
                "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                "timestamp": "2026-09-18T17:05:36.581Z"
              }
            }
          }
        }
      },
      "NotFound": {
        "description": "`not_found` — No published listing with that id, or no such route.",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Requests allowed for this key per 24-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests still available in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the oldest counted request leaves the window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": {
                "code": "not_found",
                "message": "No published listing with that id, or no such route."
              },
              "meta": {
                "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                "timestamp": "2026-09-18T17:05:36.581Z"
              }
            }
          }
        }
      },
      "MethodNotAllowed": {
        "description": "`method_not_allowed` — The route exists but not for this HTTP method.",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Requests allowed for this key per 24-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests still available in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the oldest counted request leaves the window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": {
                "code": "method_not_allowed",
                "message": "The route exists but not for this HTTP method."
              },
              "meta": {
                "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                "timestamp": "2026-09-18T17:05:36.581Z"
              }
            }
          }
        }
      },
      "RateLimited": {
        "description": "`rate_limit_exceeded` — The key's 24-hour quota is spent. Retry after the seconds given in Retry-After.",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Requests allowed for this key per 24-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests still available in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the oldest counted request leaves the window.",
            "schema": {
              "type": "integer"
            }
          },
          "Retry-After": {
            "description": "Seconds to wait before retrying. Sent only with a 429 response.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": {
                "code": "rate_limit_exceeded",
                "message": "The key's 24-hour quota is spent. Retry after the seconds given in Retry-After."
              },
              "meta": {
                "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                "timestamp": "2026-09-18T17:05:36.581Z"
              }
            }
          }
        }
      },
      "ServerError": {
        "description": "`internal_error` — Something failed on our side. The request_id in meta identifies it in our logs.",
        "headers": {
          "X-RateLimit-Limit": {
            "description": "Requests allowed for this key per 24-hour window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Remaining": {
            "description": "Requests still available in the current window.",
            "schema": {
              "type": "integer"
            }
          },
          "X-RateLimit-Reset": {
            "description": "Unix seconds at which the oldest counted request leaves the window.",
            "schema": {
              "type": "integer"
            }
          }
        },
        "content": {
          "application/json": {
            "schema": {
              "$ref": "#/components/schemas/Error"
            },
            "example": {
              "success": false,
              "error": {
                "code": "internal_error",
                "message": "Something failed on our side. The request_id in meta identifies it in our logs."
              },
              "meta": {
                "request_id": "0f1c2a3b-4d5e-6f70-8192-a3b4c5d6e7f8",
                "timestamp": "2026-09-18T17:05:36.581Z"
              }
            }
          }
        }
      }
    }
  }
}
