Overview
One normalised feed that carries agencies, agents and property listings in a single consistent shape — imported by AdminBuilder and published to the HAVHO app.
What is the HAVHO feed?
HAVHO is a property feed format for any agency website, property platform, CRM, or aggregator that needs to publish real-estate data for import. A feed is a single properties array: each property listing carries its own agency and agent, and listings that disappear from the array are removed automatically.
Field names follow the HAVHO Property Feed contract exactly. The pipeline is:
Agency website / property system / source feed → normalisation → HAVHO feed document (this spec) → AdminBuilder import → the HAVHO app.
The three record types
| Record | Key field | Purpose |
|---|---|---|
| Agency | id (in agency) | The organisation a listing belongs to. |
| Agent | id (in agent) | The person responsible for a listing; always linked to one agency. |
| Property listing | id | An active listing, linked to exactly one agent and one agency. |
| Removed listing | — (automatic) | A listing that is no longer in properties is removed on the next sync. An explicit deletions array with a reason is optional. |
When AdminBuilder imports the feed it stores each property together with its agent and agency (one record per listing — there are no separate agent or agency records to keep in step). See the Field Reference for every field.
Identifier rules
- The listing, agent and agency
ids must permanently identify the same source record and must be unique within their type. - They must not change when ordinary information changes — e.g. an agent changing name, email, phone, office, agency or location keeps the same
id; an agency renaming keeps the sameid. - Every listing carries its own
agencyandagentobjects. - You never send a
providerIdon a property, agent or agency: it is the feed's own identifier, set once — on the feed's configuration, or captured automatically from a top-levelproviderIdyour feed document declares the first time it imports — and returned on every record from it. It must be agreed with the consumer up front and kept stable — it is not derived from anything that can shift on its own (see the Field Reference for the full policy). LikewisesourceUpdatedAtis set automatically when a record is stored or changes, andstatusis implied (everything inpropertiesis active).
Transport
- The feed is served from a single HTTPS URL, reachable with a plain
GET. - It returns either a JSON document (
Content-Type: application/jsonor a URL ending in.json) or an XML document (application/xml/text/xml, or a URL ending in.xml). Both carry identical data. - The importer sends
Accept: application/json, application/xml, aUser-AgentofAdminBuilder-HAVHOFeed/2.0, follows redirects, and uses a 120 second timeout.
Paging (large feeds)
A feed may be split into pages. Give us the feed URL and we request it one page per run: the first run reads page=1, the next run page=2, and so on. We add (or replace) the page query parameter and keep everything else you put in the URL:
https://api.example.com/properties?site=acme&page=1
https://api.example.com/properties?site=acme&page=2
https://api.example.com/properties?site=acme&page=3 → { "properties": [] } (end)
- Every page has the same shape:
{ "properties": [ … ] }, each property with its ownagencyandagent. We start atpage=1and keep going, one page per run, until a page returns an emptypropertiesarray. You choose the page size. - An unpaginated feed needs no changes: after its single page we ask once more, and stop when the answer repeats the same listings or is rejected with HTTP
400,404or422. - Listings are only removed once the whole feed has been read to its last page in one clean pass. If a page fails (network error, HTTP error, unreadable document) we retry that same page on the next run and remove nothing; a safety limit of 500 pages also prevents removal.
- Keep pages stable while we read them: a listing must not be skipped because others moved between pages mid-pull (order by a fixed key such as the listing
id).
Protecting your feed (optional)
- Your feed URL can be public, or protected. If it should not be public, require an
X-API-Keyrequest header and give us the key when you submit the feed. - The importer then sends
X-API-Key: <your key>on every pull. Return200with the document when the key matches,401when it is missing or wrong. - Serve the feed over HTTPS only. Use a long, random key, and a different one for staging and production.
Sync model
The feed is pulled on a schedule and treated as the current state of the source:
- A listing
idnot seen before → created. - A listing
idseen before with changed values → updated. - A listing
idthat leaves thepropertiesarray → removed automatically. Nothing else to send. - Once a listing is gone from
properties(or is listed indeletions), it is automatically deleted from our database on the next sync and recorded as a removal (source_removedif you did not report it). Removal only happens after a complete, successfully read snapshot — a failed request, an unreadable document or a document without apropertiescollection never removes anything. An intentionally emptypropertiesarray does remove all of the feed's listings.
Because absence means removal, each pull must contain all active listings. A failed request, an unreadable document or a document without a properties collection never removes anything; an intentionally empty properties array removes all of the feed's listings.
Every record gets a sourceUpdatedAt automatically: the time it was first stored, moved forward whenever any returned value changes — including price, description, address, images, or agent/agency contact details. Consumers use it to detect changes incrementally. You may still send your own if your source has a reliable timestamp.
Incremental retrieval
A feed may support returning only records changed since a given time by
honouring an updated_since query parameter (ISO-8601). When
it does, it echoes the value back in the envelope's updated_since
field. When it does not, it returns the full snapshot and
updated_since is null — the importer copes
with both.
Reading the catalogue back out
Everything above is about sending us a feed. A separate read-only REST API (/api/havho/v1) serves the imported catalogue back out to a consuming application — different audience, different field names (it uses externalListingId where the feed uses id). That API, its pagination, incremental sync and every endpoint's exact response shape are documented on its own page:
Pricing
priceis a plain number in ZAR — no currency symbol, no thousands separators, up to 2 decimals.- For
type: "sale"it is the asking price; fortype: "rent"it is the monthly rental amount.
Images
photoUrlsis an ordered list of 1 to 21 unique HTTPS URLs.- The first URL is the primary / thumbnail image.
- URLs must point directly at the image file, be publicly reachable, and stay reachable while the listing is active.
Controlled values
| Field | Allowed values |
|---|---|
status | Optional. Leave it out: everything in properties is active |
type | sale, rent |
propertyType | house, apartment, townhouse, vacant land, farm/ smallholding, commercial property, industrial property, development |
features | Free text (max 100 labels, 100 characters each). Known labels Pet friendly, Garden, Flatlet, Pool, Security Estate power the site filters; any other label (e.g. Borehole, Solar Inverter) is kept as sent. |
reason (deletions) | sold, rented, withdrawn, expired, deleted, source_removed |
Quickstart
- Normalise your source data into the JSON or XML document — a
propertiesarray, each property with its agency and agent. Every field is in the Field Reference. - Serve it at a stable HTTPS URL. Protect it with an
X-API-Keyheader if it should not be public. - Check it against sample-feed.json / sample-feed.xml.
- In your provider dashboard: Submit a feed — paste the URL (and the key, if protected). It is fetched and dry-run through the importer on submit, showing the record counts and any problems before anything goes live.