openapi: 3.2.0 info: title: RAIA Portal Feed Listings API version: 0.1.0 summary: Vendor-neutral HTTP contract for syndicating property listings, reconciling branch inventory, polling enquiries, and activating portal products. description: 'The RAIA Portal Feed API gives implementers a single, vendor-neutral contract that covers the same jobs-to-be-done as historical UK portal feed integrations (Rightmove Real-Time Data Feed, Rightmove Commercial Listings, Zoopla Real-Time Listings, and Zoopla Products).' contact: name: RAIA Protocol Working Group email: protocol@estateaigents.org url: https://estateaigents.org license: name: MIT identifier: MIT servers: - url: https://feed.example.com/api/raia/portal/v1 description: Production (implementer-hosted) - url: https://staging.feed.example.com/api/raia/portal/v1 description: Staging / sandbox (implementer-hosted) - url: http://localhost:8787/api/raia/portal/v1 description: Local development security: - OAuth2ClientCredentials: - feed.read - feed.write - products.write tags: - name: Listings description: Upload, update, retrieve and remove residential or commercial listings. paths: /listings/{reference}: parameters: - $ref: '#/components/parameters/ListingReference' put: tags: - Listings summary: Upsert a listing description: 'Upload a new listing or update an existing one. Identity is driven by the path `reference`; if the reference is new the listing is created (HTTP `201`), otherwise it is updated (HTTP `200`). The payload covers both residential and commercial property. Use the `residential` body when marketing a single dwelling and the `commercial` body when marketing a building (optionally with up to 50 spaces). Buyers and portals can see the same listing projected through the public RAIA property card.' operationId: upsertListing security: - OAuth2ClientCredentials: - feed.write requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ListingUpsert' examples: residentialLetting: $ref: '#/components/examples/ResidentialLettingUpsert' commercialBuilding: $ref: '#/components/examples/CommercialBuildingUpsert' responses: '200': description: Listing updated. content: application/json: schema: $ref: '#/components/schemas/ListingSaveAction' '201': description: Listing created. content: application/json: schema: $ref: '#/components/schemas/ListingSaveAction' '202': description: 'Accepted for asynchronous processing. Returned when the feed implementation queues uploads (mirrors the Rightmove RTDF and Zoopla ZPG behaviour where ingestion is asynchronous). ' content: application/json: schema: $ref: '#/components/schemas/AsyncAccepted' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '409': description: The reference conflicts with another existing listing (for example, attempting to re-use a building reference for a space). content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetail' '429': $ref: '#/components/responses/TooManyRequests' 5XX: $ref: '#/components/responses/ServerError' get: tags: - Listings summary: Retrieve a listing operationId: getListing security: - OAuth2ClientCredentials: - feed.read parameters: - $ref: '#/components/parameters/BranchIdHeader' responses: '200': description: Listing found. content: application/json: schema: $ref: '#/components/schemas/Listing' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/TooManyRequests' 5XX: $ref: '#/components/responses/ServerError' delete: tags: - Listings summary: Remove a listing description: 'Permanently removes a listing from the feed. The caller must supply a `removal_reason` so the receiving portal/system can audit why the listing was withdrawn. Removing a building also removes any associated spaces.' operationId: deleteListing security: - OAuth2ClientCredentials: - feed.write requestBody: required: true content: application/json: schema: $ref: '#/components/schemas/ListingRemovalRequest' example: branch_id: 56726 removal_reason: SOLD_BY_US removed_at: '2026-05-28T06:00:00Z' responses: '200': description: Listing removed. content: application/json: schema: $ref: '#/components/schemas/ListingRemovalResult' '202': description: Removal accepted for asynchronous processing. content: application/json: schema: $ref: '#/components/schemas/AsyncAccepted' '400': $ref: '#/components/responses/BadRequest' '401': $ref: '#/components/responses/Unauthorized' '403': $ref: '#/components/responses/Forbidden' '404': $ref: '#/components/responses/NotFound' '429': $ref: '#/components/responses/TooManyRequests' 5XX: $ref: '#/components/responses/ServerError' components: schemas: CommercialBuilding: type: object required: - reference - address - status - primary_classification description: 'Commercial building. A building may be marketed on its own (BUILDING model) or alongside one or more `spaces` (SPACE model). Maximum of 50 spaces per building, matching the upstream Rightmove Commercial limit. ' properties: reference: type: string pattern: ^[A-Za-z0-9_-]{1,100}$ address: $ref: '#/components/schemas/Address' status: $ref: '#/components/schemas/ListingStatus' published: type: boolean default: true primary_classification: $ref: '#/components/schemas/PropertyClassification' secondary_classifications: type: array items: $ref: '#/components/schemas/PropertyClassification' available_date: type: string format: date let_type: type: string enum: - STANDARD - SHORT_TERM - LONG_TERM - FLEXIBLE let_contract_length: type: integer service_charge: type: number business_rates: type: number rent_all_inclusive: type: boolean condition: type: string enum: - FULL_FIT_OUT - PARTIAL_FIT_OUT - SHELL_SPACE sizing: $ref: '#/components/schemas/Sizing' pricing: $ref: '#/components/schemas/Pricing' amenities: type: array items: type: string description: Free-form amenity tags such as `WIFI`, `CONCIERGE`, `PARKING`, `AIR_CONDITIONING`. auction: type: boolean default: false description: True if the building is being offered via auction (sales only). media: $ref: '#/components/schemas/Media' spaces: type: array items: $ref: '#/components/schemas/CommercialSpace' maxItems: 50 ListingRemovalRequest: type: object required: - removal_reason properties: branch_id: oneOf: - type: integer format: int64 - type: string removal_reason: $ref: '#/components/schemas/RemovalReason' removed_at: type: string format: date-time description: When the removal was committed in the source system. note: type: string maxLength: 500 AsyncAccepted: type: object required: - status - request_id properties: status: type: string enum: - QUEUED request_id: type: string description: Identifier the caller can use to correlate logs. polling_url: type: string format: uri description: Optional URL the caller may poll for the final result. Furnishing: type: string enum: - FURNISHED - PART_FURNISHED - UNFURNISHED - FURNISHED_OR_UNFURNISHED TenureType: type: string enum: - FREEHOLD - LEASEHOLD - SHARE_OF_FREEHOLD - COMMONHOLD Address: type: object required: - display_address - postcode - country description: 'Full address for the listing as held by the feed implementer. Note the public RAIA property card masks the address to district level until an enquiry reaches the `COMMITTED` state — see ADR-211. ' properties: display_address: type: string maxLength: 120 building_identifier: type: string maxLength: 100 description: Number or name of the building. address_line_1: type: string maxLength: 200 address_line_2: type: string maxLength: 200 district: type: string maxLength: 100 town: type: string maxLength: 100 county: type: string maxLength: 100 postcode: type: string maxLength: 12 examples: - W1D 3QU country: type: string description: ISO 3166-1 alpha-2 country code. pattern: ^[A-Z]{2}$ latitude: type: number format: float minimum: -90 maximum: 90 longitude: type: number format: float minimum: -180 maximum: 180 uprn: type: integer format: int64 description: UK Unique Property Reference Number, where applicable. show_map: type: boolean default: true MediaAsset: type: object required: - url properties: url: type: string format: uri maxLength: 1024 description: Publicly accessible URL. Brochures must end in `.pdf`. description: type: string maxLength: 200 order: type: integer minimum: 0 etag: type: string description: 'Optional ETag of the asset at the time it was published. Feed consumers SHOULD honour this when revalidating downloads. ' MeasurementType: type: string enum: - GEA - GIA - NIA - IPMS1 - IPMS2 - IPMS3_1 - IPMS3_2 RemovalReason: type: string description: Why a listing is being removed. Aligned with both Rightmove Commercial removal reasons and the RTDF action set. enum: - SOLD_BY_US - SOLD_BY_ANOTHER_AGENT - LET_BY_US - LET_BY_ANOTHER_AGENT - WITHDRAWN_FROM_MARKET - LOST_INSTRUCTION - REMOVED CommercialListingInput: type: object required: - transaction_type - building description: Commercial listing payload. properties: transaction_type: $ref: '#/components/schemas/TransactionType' building: $ref: '#/components/schemas/CommercialBuilding' public_card: $ref: '#/components/schemas/PublicCardProjection' TransactionType: type: string enum: - SALES - LETTINGS PropertyType: type: string description: Residential property sub-type. Use the `commercial` payload for commercial classifications. enum: - FLAT - APARTMENT - STUDIO - MAISONETTE - TERRACED - END_TERRACE - SEMI_DETACHED - DETACHED - BUNGALOW - COTTAGE - TOWNHOUSE - LAND - OTHER ListingUpsert: oneOf: - $ref: '#/components/schemas/ResidentialListingInput' - $ref: '#/components/schemas/CommercialListingInput' discriminator: propertyName: kind mapping: residential: '#/components/schemas/ResidentialListingInput' commercial: '#/components/schemas/CommercialListingInput' ListingStatus: type: string description: Marketing lifecycle status. Mirrors the public RAIA property card statuses plus the commercial-only `UNDER_OFFER`. enum: - AVAILABLE - UNDER_OFFER - SOLD_STC - SOLD_STCM - RESERVED - LET_AGREED - OFF_MARKET - WITHDRAWN CommercialClassification: type: string description: 'Commercial classification family. The selected sub-type determines which optional extension object (e.g. `office`, `industrial`) is meaningful. ' enum: - OFFICE - INDUSTRIAL_AND_LOGISTICS - RETAIL - LEISURE_AND_HOSPITALITY - LAND_AND_DEVELOPMENT - OTHER Media: type: object description: Bundles of media for a listing or commercial space. properties: photos: type: array items: $ref: '#/components/schemas/MediaAsset' floor_plans: type: array items: $ref: '#/components/schemas/MediaAsset' epcs: type: array items: $ref: '#/components/schemas/MediaAsset' epc_graphs: type: array items: $ref: '#/components/schemas/MediaAsset' brochures: type: array items: $ref: '#/components/schemas/MediaAsset' virtual_tours: type: array items: $ref: '#/components/schemas/MediaAsset' Listing: type: object required: - reference - branch_id - transaction_type - status - updated_at properties: reference: type: string branch_id: type: string description: Stringified branch identifier for stable JSON consumers. transaction_type: $ref: '#/components/schemas/TransactionType' status: $ref: '#/components/schemas/ListingStatus' kind: type: string enum: - residential - commercial residential: $ref: '#/components/schemas/ResidentialListingInput' commercial: $ref: '#/components/schemas/CommercialListingInput' public_card_url: type: string format: uri description: URL of the public RAIA property card derived from this listing. created_at: type: string format: date-time updated_at: type: string format: date-time version: type: integer description: Monotonically increasing version. Useful for optimistic concurrency. ListingSaveAction: type: object required: - reference - action - updated_at description: Returned by upsert operations. Mirrors the upstream `PropertySaveAction` envelope. properties: reference: type: string action: type: string enum: - CREATED - UPDATED - NO_CHANGE updated_at: type: string format: date-time version: type: integer public_card_url: type: string format: uri AreaSizeUnit: type: string enum: - SQFT - SQM - ACRES - HECTARES RentFrequency: type: string enum: - MONTHLY - YEARLY - WEEKLY ProblemDetail: description: RFC 7807 problem detail. Implementations may add vendor-specific extension members. type: object properties: type: type: string format: uri default: about:blank title: type: string status: type: integer minimum: 100 maximum: 599 detail: type: string instance: type: string format: uri trace_id: type: string timestamp: type: string format: date-time validation_errors: type: array items: type: object required: - field - message properties: field: type: string examples: - building.location.postcode message: type: string code: type: string examples: - MISSING PropertyClassification: type: object required: - classification - sub_type properties: classification: $ref: '#/components/schemas/CommercialClassification' sub_type: $ref: '#/components/schemas/CommercialSubType' CommercialSubType: type: string enum: - OFFICE - SERVICED_OFFICE - WAREHOUSE - DISTRIBUTION_WAREHOUSE - FACTORY_MANUFACTURING - SELF_STORAGE - TRADE_COUNTER - INDUSTRIAL_PARK - LIGHT_INDUSTRIAL - HEAVY_INDUSTRIAL - LAND - WOODLAND - FARM - COMMERCIAL_DEVELOPMENT - RESIDENTIAL_DEVELOPMENT - SCIENCE_PARK - RETAIL_HIGH_STREET - RETAIL_OUT_OF_TOWN - RETAIL_PROPERTY_SHOPPING_CENTRE - SHOP - CONVENIENCE_STORE - POST_OFFICE - HOTEL - PUB - RESTAURANT - BAR - CAFE - LEISURE_FACILITY - CAMPSITE_HOLIDAY_VILLAGE - COMM_GUEST_HOUSE - HEALTHCARE_FACILITY - DENTAL_CARE - PHARMACY - CARE_HOME_FACILITY - CHILDCARE_FACILITY - PLACE_OF_WORSHIP - GARAGE - PETROL_STATION - DATA_CENTRE - LIFE_SCIENCES_LABS - AUTOMOTIVE - MIXED_USE - STUDENT_HOUSING - OTHER ResidentialListingInput: type: object required: - transaction_type - status - property_type - address - headline description: Residential single-dwelling listing payload. properties: transaction_type: $ref: '#/components/schemas/TransactionType' status: $ref: '#/components/schemas/ListingStatus' property_type: $ref: '#/components/schemas/PropertyType' headline: type: string maxLength: 200 description: type: string maxLength: 10000 bedrooms: type: integer minimum: 0 bathrooms: type: integer minimum: 0 reception_rooms: type: integer minimum: 0 floor_area_sqm: type: number minimum: 0 available_from: type: string format: date asking_price: type: number minimum: 0 description: Sales price. Use when transaction_type is SALES. asking_rent_pcm: type: number minimum: 0 description: Lettings rent per calendar month. Use when transaction_type is LETTINGS. rent_frequency: $ref: '#/components/schemas/RentFrequency' deposit: type: number minimum: 0 currency: $ref: '#/components/schemas/Currency' tenure: $ref: '#/components/schemas/TenureType' furnishing: $ref: '#/components/schemas/Furnishing' epc_rating: type: string pattern: ^[A-G]$ features: type: array items: type: string maxLength: 200 maxItems: 20 parking: type: array items: type: string enum: - OFF_STREET - GARAGE - ALLOCATED - RESIDENTS_PERMIT - NONE outside_space: type: array items: type: string enum: - PRIVATE_GARDEN - BALCONY - TERRACE - ROOF_TERRACE - NONE address: $ref: '#/components/schemas/Address' media: $ref: '#/components/schemas/Media' public_card: $ref: '#/components/schemas/PublicCardProjection' ListingRemovalResult: type: object required: - reference - removed_at - removal_reason properties: reference: type: string removed_at: type: string format: date-time removal_reason: $ref: '#/components/schemas/RemovalReason' Currency: type: string description: ISO 4217 currency code. pattern: ^[A-Z]{3}$ examples: - GBP - EUR - USD - THB - SGD Sizing: type: object properties: size: type: number minimum: 0 min_size: type: number minimum: 0 max_size: type: number minimum: 0 unit: $ref: '#/components/schemas/AreaSizeUnit' measurement_type: $ref: '#/components/schemas/MeasurementType' PublicCardProjection: type: object description: 'Optional projection that controls how the listing surfaces on the public RAIA property card (`schemas/property.json`). When omitted the feed implementation derives sensible defaults from the listing. ' properties: raia_id: type: string pattern: ^prop-[a-z]{2}-[a-z0-9]+-[0-9]+$ description: External stable RAIA property identifier. publish: type: boolean default: true max_data_level: type: integer minimum: 0 maximum: 3 default: 0 suppress_address: type: boolean default: true description: When true, the public card only exposes district-level location until COMMITTED. Pricing: type: object required: - price properties: price: type: number minimum: 0 currency: $ref: '#/components/schemas/Currency' display_qualifier: type: string enum: - NONE - PRICE_ON_APPLICATION - GUIDE_PRICE - OFFERS_IN_EXCESS_OF - OFFERS_IN_REGION_OF - FROM frequency: $ref: '#/components/schemas/RentFrequency' rent_obligation: type: string enum: - FULLY_REPAIRING_AND_INSURING - INTERNAL_REPAIRING_AND_INSURING - INTERNAL_REPAIRING_ONLY - NEGOTIABLE CommercialSpace: type: object required: - reference - name - floor_identifier - sizing - status - primary_classification properties: reference: type: string pattern: ^[A-Za-z0-9_-]{1,100}$ description: Unique within the building; must differ from the building reference. name: type: string maxLength: 200 floor_identifier: type: string maxLength: 50 description: type: string maxLength: 100000 sizing: $ref: '#/components/schemas/Sizing' status: $ref: '#/components/schemas/ListingStatus' pricing: $ref: '#/components/schemas/Pricing' let_type: type: string enum: - STANDARD - SHORT_TERM - LONG_TERM - FLEXIBLE available_date: type: string format: date service_charge: type: number business_rates: type: number let_contract_length: type: integer description: Length of rental contract in months. Lettings only. rent_all_inclusive: type: boolean published: type: boolean condition: type: string enum: - FULL_FIT_OUT - PARTIAL_FIT_OUT - SHELL_SPACE primary_classification: $ref: '#/components/schemas/PropertyClassification' secondary_classifications: type: array items: $ref: '#/components/schemas/PropertyClassification' media: $ref: '#/components/schemas/Media' order: type: integer minimum: 0 key_features: type: array items: type: string maxLength: 300 maxItems: 10 responses: BadRequest: description: Validation error. The body is a `ProblemDetail`. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetail' NotFound: description: Resource not found. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetail' Unauthorized: description: Missing or invalid bearer token. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetail' Forbidden: description: Token is valid but lacks the required scope. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetail' TooManyRequests: description: Rate limit exceeded. Quotas reset every 60 seconds. headers: Retry-After: schema: type: integer description: Seconds to wait before retrying. X-RateLimit-Limit: schema: type: integer X-RateLimit-Remaining: schema: type: integer X-RateLimit-Reset: schema: type: integer description: Epoch seconds when the quota resets. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetail' ServerError: description: Unexpected error. content: application/problem+json: schema: $ref: '#/components/schemas/ProblemDetail' parameters: BranchIdHeader: name: X-RAIA-Branch-Id in: header required: false description: 'Optional branch context for the read. When omitted the response is scoped to all branches the caller''s credentials grant access to. ' schema: type: string ListingReference: name: reference in: path required: true description: 'Customer-controlled listing reference. Must be unique per branch. For commercial properties with spaces, this is the building reference; each space carries its own reference inside the payload. ' schema: type: string pattern: ^[A-Za-z0-9_-]{1,100}$ examples: - REF_001 - BLD_LON_001 examples: CommercialBuildingUpsert: summary: Commercial building with one space value: kind: commercial transaction_type: LETTINGS building: reference: BLD_LON_001 status: AVAILABLE published: true primary_classification: classification: OFFICE sub_type: SERVICED_OFFICE address: display_address: 33 Soho Square, London building_identifier: 33 Soho Square postcode: W1D 3QU country: GB latitude: 51.5139 longitude: -0.1317 pricing: price: 750000 currency: GBP display_qualifier: GUIDE_PRICE amenities: - WIFI - CONCIERGE - PARKING - AIR_CONDITIONING spaces: - reference: SPACE_LON_001_FL2 name: 2nd Floor floor_identifier: Floor 2 sizing: size: 5941 unit: SQFT measurement_type: GIA status: AVAILABLE pricing: price: 65 currency: GBP frequency: YEARLY primary_classification: classification: OFFICE sub_type: SERVICED_OFFICE key_features: - Open plan with private meeting rooms - Bike storage and shower facilities ResidentialLettingUpsert: summary: Residential lettings listing value: kind: residential transaction_type: LETTINGS status: AVAILABLE property_type: FLAT headline: 2-bed flat, Hammersmith W6 description: Bright two-bedroom flat close to the station. bedrooms: 2 bathrooms: 1 reception_rooms: 1 floor_area_sqm: 62 available_from: '2026-07-01' asking_rent_pcm: 2450 rent_frequency: MONTHLY deposit: 2826 currency: GBP furnishing: FURNISHED epc_rating: C features: - Balcony - Close to tube - Furnished - Managed property parking: - RESIDENTS_PERMIT outside_space: - BALCONY address: display_address: 42 King Street, London W6 9TA address_line_1: 42 King Street town: London postcode: W6 9TA country: GB latitude: 51.4927 longitude: -0.2228 media: photos: - url: https://example-estates.test/media/REF_001/photo-1.jpg description: Living room order: 0 public_card: raia_id: prop-gb-example-000001 publish: true max_data_level: 3 suppress_address: true securitySchemes: OAuth2ClientCredentials: type: oauth2 description: 'Server-to-server OAuth2 client credentials flow. The token endpoint is published by the implementer; credentials are issued out-of-band during onboarding. Tokens are short-lived Bearer JWTs. ' flows: clientCredentials: tokenUrl: https://feed.example.com/oauth/token scopes: feed.read: Read listings, branches, performance and enquiries. feed.write: Upsert and remove listings. products.write: Request portal product activations.