greatspace

docs / syndication api v1

Syndication API

Live availability from every provider who has chosen to syndicate their spaces to you, in the shape your importer expects. One URL that never changes as providers opt in and out.
Status
Live
For
Listing Platforms and Availability Databases
Replaces
Integrations with individual providers

Quickstart

The Syndication API is for listing platforms and data partners. Data API access is switched on by arrangement: contact us if your account doesn't have it yet.

  1. Create an access tokenIn the app, under Settings → Access tokens, with the Data API (read only) scope. It's shown once.
  2. Shape your exportOptional. Under Integrations → Data API, save an export profile with your columns, headers and format.
  3. Pull the feedCall GET /v1/spaces with your token. You get every space currently syndicated to you.
  4. Poll with an ETagSend back the ETag on each pull. Unchanged snapshots answer 304, so polling costs almost nothing.
curl "https://api.greatspace.ai/v1/spaces?format=json" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN"

Authentication

Every pull authenticates with an access token. The token identifies your organisation, so there's no account id in the URL: what you get back is whatever is syndicated to you at that moment.

Create a token

  1. Sign in to Great Space as an admin of your organisation.
  2. Go to Settings → Access tokens and create a token with the Data API (read only) scope.
  3. Give it a name that says where it's used, such as Production importer.
  4. Copy it. It's shown only once: we keep just a hash, so we can't show it to you again.

Send it with each request

Put it in the Authorization header as Bearer followed by the token. Clients that can't set headers, such as spreadsheet import functions and some platform pollers, can pass it as a token query parameter instead. A URL ends up in logs and shared documents, so prefer the header wherever you can.

curl "https://api.greatspace.ai/v1/spaces" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN"

What a token can do

  • Read only. A Data API token can pull the feed and nothing else. It can't change anything.
  • Your organisation only. It reaches the spaces syndicated to the organisation that created it.
  • Revoke at any time. Revoking a token in Settings → Access tokens stops it on the next request. Use one token per importer, so you can revoke one without touching the others.
  • Checked on every pull. If your organisation's Data API access is switched off, every token stops with 403, without anyone having to revoke them.

Pull the feed

One endpoint, GET /v1/spaces. With no parameters it returns every space syndicated to you, shaped by your default export profile. Every parameter is optional.

ParameterDefaultWhat it does
profileIdyour defaultThe id of a saved export profile to shape the response with. Leave it out for your default profile, or the built-in one if you have none. Can't be combined with columns.
columnsnoneA one-off, comma-separated list of column keys, in the order you want them. Headers are the standard ones from the table below. Must include spaceId and providerId. Can't be combined with profileId.
formatthe profile'scsv, xls or json. Overrides the profile's own format for one pull.
providerall providersNarrows the pull to one provider, by org id or Portal slug. Repeat it for several. An unknown value answers 400, never an empty feed.
limitnoneReturns only the first n rows, for sampling the shape. Never cached and never answers 304, so don't use it for production pulls.
tokennoneYour access token, for clients that can't send an Authorization header.
curl -G "https://api.greatspace.ai/v1/spaces" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN" \
  --data-urlencode "format=csv" \
  --data-urlencode "columns=spaceId,providerId,building,unit,desks,listPrice,city" \
  -o spaces.csv

-G with --data-urlencode puts each parameter on its own line and encodes it, which is safer than pasting one long URL.

Export profiles

An export profile is the shape of your feed, saved in your account so your importer's URL can stay simple. Create and edit them in the app under Integrations → Data API. A profile sets:

  • Columns and their order, from the columns below. spaceId and providerId are always included.
  • Headers: rename any column to the name your system expects.
  • Value maps: rewrite enum values, such as under_offer to Let agreed.
  • Format (csv, xls or json) and date format (ISO, UK or US).
  • CSV details: the delimiter, quoting, and how an empty value is written.

Mark one profile as your default and it applies whenever a pull doesn't name one. With no profiles at all you get the built-in profile: every column, standard headers, ISO dates, CSV. Editing a profile changes its ETag, so an importer polling with If-None-Match picks up the new shape on its next pull.

For a quick look at a particular slice without saving a profile, use the columns parameter. Renaming, reordering beyond the list order, and value maps are what a profile is for.

Response formats

Every format carries the same rows. CSV and XLS have one header row followed by one row per space, and come with a Content-Disposition filename that names the snapshot. JSON wraps the rows in an envelope that says which snapshot and which profile produced them:

{
  "schemaVersion": 2,          // matches X-Feed-Schema-Version
  "generation": "2026-10-15T09-00-04-512Z",  // the snapshot these rows come from
  "generatedAt": "2026-10-15T09:00:04.512Z",
  "profileId": "k2Lw9qXc",          // null for the built-in profile or a columns pull
  "profileName": "Nightly import",
  "count": 1284,
  "data": [ { … }, … ]              // one object per space, keyed by your headers
}

Each object in data is keyed by your profile's headers, not by column key, so the JSON and the CSV from the same profile always agree. The one difference is photos: CSV and XLS have a column per photo slot, so the header row never changes, while JSON gathers them into a single photos array, primary photo first, with empty slots left out ([] when a space has none). Here are the same two spaces in each:

curl -G "https://api.greatspace.ai/v1/spaces" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN" \
  --data-urlencode "format=json" \
  --data-urlencode "columns=spaceId,providerId,building,unit,desks,listPrice,currency,city,photo_1,photo_2,photo_3"

Polling and the contract

The feed is rebuilt from providers' live inventory every hour. Polling more often than that won't find anything new, but with conditional requests it costs nearly nothing, so poll on whatever schedule suits your importer.

Conditional GET

Every full response carries an ETag and a Last-Modified. Send the ETag back as If-None-Match (or the date as If-Modified-Since) and an unchanged snapshot answers 304 Not Modified with no body. The ETag's double quotes are part of its value: send it without them and nothing matches, which looks like a normal 200 rather than an error. Prefer the ETag; it also changes when you edit your profile, which a timestamp can't capture.

curl -i "https://api.greatspace.ai/v1/spaces?format=json" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN" \
  -H 'If-None-Match: "d3a26cdd5f0e41b2"'

The contract

  • Every pull is a full snapshot. It's the complete current availability, not a list of changes. A space that has gone, been let or been withdrawn is simply absent, so delete anything missing from the latest pull.
  • Upsert on spaceId + providerId. Both are always present and can't be removed from a profile, so an import keyed on them is idempotent.
  • Only published spaces. You get spaces a provider has published and syndicated to you, including those under offer. Unpublishing or stopping syndication removes them from the next snapshot.
  • 304 and 503 are never empty feeds. Keep what you have. Only a 200 replaces your data.
  • One permanent URL. As providers opt in and out, the same request returns more or fewer spaces. You never need to change your importer's URL.

Columns

Every column the feed can return: the key to name it by in columns and in a profile, and the header it gets unless your profile renames it.

KeyHeaderTypeNotes
spaceId required GS Space ID string Stable id of the space. Half of the upsert key.
providerId required GS Provider ID string The provider who listed it. The other half of the upsert key.
building Building name string
floor Floor name string
unit Unit name string
status Publication status enum published or under_offer: the feed never includes unpublished spaces. A profile can map these to your own values.
availableFrom Available from date
sizeSqft Size (sq ft) number
sizeSqm Size (sq m) number
desks Desks number
meetingRooms Meeting rooms number
density Desk density (desks/sqft) number
listPrice Price (pcm) number
currency Currency string Currency code, e.g. GBP.
pricePerSqft Price per sq ft per year number
pricePerDesk Price per desk (pcm) number
addressLine1 Address line 1 string
addressLine2 Address line 2 string
city City string
region Region string
postcode Postcode string
country Country string
latitude Latitude number
longitude Longitude number
description Description string
photo_1 Photo 1 string Public image URLs, primary photo first. In JSON, the photo columns you choose come back as one photos array.
photo_2 Photo 2 string
photo_3 Photo 3 string
photo_4 Photo 4 string
photo_5 Photo 5 string
photo_6 Photo 6 string
photo_7 Photo 7 string
photo_8 Photo 8 string
brochureUrl Brochure URL string
listingUrl Listing URL string
providerName Provider name string
providerUrl Provider website string
roomUrl Broker Portal URL string The space on the provider's Broker Portal.
createdAt Created At datetime
updatedAt Updated At datetime

Status codes and errors

Every response has a standard HTTP status code. 200 and 304 mean the pull worked. Anything in the 4xx range is a problem with the request, which retrying unchanged won't fix; 5xx is a problem on our side, so try again later.

An error's body is JSON whatever format you asked for: an errorMessage that says what went wrong, and a correlationId, also sent as the x-correlation-id header. Quote it if you ask us about a request. Branch on the status code, not the message, which may change.

GET /v1/spaces?provider=acme-offices

400 Bad Request
{
  "success": false,
  "errorMessage": "Unknown provider \"acme-offices\". Use a provider's organisation id or Portal slug, and only providers who have syndicated to you.",
  "correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
}
Status codeDescriptionMeaning
200OKThe full current snapshot, in your profile's format, with ETag, Last-Modified and X-Feed-Schema-Version headers.
304Not ModifiedNothing has changed since the ETag or Last-Modified you sent. No body: keep what you already have, and never read it as an empty feed.
400Bad RequestA parameter is wrong: an unknown provider, column or format, a non-numeric limit, or columns and profileId together. The message names it.
401UnauthorizedNo token, or one that is invalid, revoked or missing the Data API scope.
403ForbiddenThe token is valid, but your organisation no longer has Data API access. Access is checked on every pull.
404Not FoundThe profile named by profileId doesn't exist.
503Service UnavailableNo snapshot is available right now. Try again on your normal schedule. This is never an instruction to delete listings.

Versions

Every response carries X-Feed-Schema-Version: 2, and JSON responses repeat it as schemaVersion. Within a schema version we only add things: new columns you can opt into, and new fields in the JSON envelope. The shape of a row your profile already produces won't change without the schema version changing, so check it and ignore fields you don't recognise.