--- Inventory API — Great Space Docs
greatspace

docs / inventory api (draft)

Inventory API

Keep your buildings, units, availability and prices in Great Space up to date straight from your own systems: your property management platform, CRM or data warehouse. Changes land when they happen, not when tomorrow's spreadsheet sync runs.
Status
Draft
For
Workspace providers
Replaces
Daily spreadsheet import

Status

This API isn't available yet. This page describes what we're planning, so you can see where it's heading and tell us what you need. Paths, fields and behaviour may change before release. If you'd like to help shape it or be an early user, get in touch.

Why an API

Most providers keep Great Space current with a scheduled spreadsheet import: a sheet exported from their own system that we re-read on a schedule, usually daily. It works, but a unit let at 10am stays on brokers' screens until the next sync, and someone has to keep the export running.

The Inventory API replaces that sheet with direct calls from your system:

  • Availability and prices, in bulkOne call updates status, available-from dates and prices across hundreds of units: the everyday job the spreadsheet does now.
  • Buildings and unitsCreate and update the structure itself, keyed by your own references, when you add a building or reconfigure a floor.
  • Live, not dailySend changes as they happen, and they reach your Portal, brokers' searches and syndicated feeds within minutes.
  • A heartbeatOn quiet days, one call confirms everything is still current, so your spaces stay in brokers' Auto Matching.

Endpoints

Every request and response is JSON. Successful responses are { "success": true, "data": … }; errors are described under Errors.

Tokens and references

Access tokens

The Inventory API will use the same access tokens as the Syndication API, with an Inventory API (read and write) scope that an admin chooses to grant. Send it as Authorization: Bearer …. Use a separate token for each system that writes, so you can see which one made a change and revoke one without touching the others. Every change shows in your audit log, attributed to the token, like an edit made in the app.

Your references

Buildings and units are addressed by your own references (buildingRef, unitRef), so you never have to store our ids. A reference is up to 100 characters of letters, numbers, -, _ and ., and must be unique in your account: a unit reference is unique across all your buildings, so a bulk update can name units without their building. Responses also give our ids (buildingId, spaceId), which the Syndication API uses.

We plan to carry over the references from your spreadsheet import, so switching from the sheet to the API shouldn't mean starting again. Units created in the app have no reference until you set one with Create or update a unit, using its spaceId in place of the unitRef in the path.

Update availability

PATCH/v1/inventory/availabilityDraft

The call most integrations will make most often: a list of units and only the fields that changed. Anything you leave out stays as it is, so a price change doesn't need the status, and a status change doesn't need the price.

  • Up to 1,000 units a call. Send just what changed since your last run, or your whole availability list every time. Either way works.
  • Idempotent. Sending the same update twice has the same effect as sending it once: the repeat changes nothing and reports unchanged. Retries after a timeout and full-list runs are safe.
  • Row by row. Valid rows are applied even when others fail. The response lists each unit's result and what changed, and only a malformed request as a whole answers 400.
curl -X PATCH "https://api.greatspace.ai/v1/inventory/availability" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "updates": [
      {
        "unitRef": "F4-402",
        "status": "published",
        "availableFrom": "2026-11-01",
        "price": { "amount": 14400, "currency": "GBP", "period": "month" }
      },
      { "unitRef": "F5-501", "status": "under_offer" },
      {
        "unitRef": "F2-210",
        "price": { "amount": 9200, "currency": "GBP", "period": "month" },
        "desks": 16
      },
      { "unitRef": "F9-999", "status": "published" }
    ]
  }'

Fields in each update

FieldTypeWhat it does
unitRef required string Your reference for the unit. Our spaceId works too, for units created in the app.
status publishedunder_offerunpublished Unpublishing takes a unit out of your Portal and every feed it's syndicated to.
availableFrom date | null YYYY-MM-DD. Send null to clear it, meaning available now.
price object { amount, currency, period }: see the next three rows.
price.amount number The price for one period, in major units (14400 is £14,400).
price.currency string A currency code, such as GBP.
price.period monthyear What the amount covers.
desks integer Desk count, when a reconfiguration changes it.

Confirm it's current

POST/v1/inventory/heartbeatDraft

A heartbeat: "we're still here, and everything you hold for us is still right", even when nothing has changed. It marks every unit as up to date without touching any of its details, exactly like Mark all as up to date in the app, or a scheduled spreadsheet import that runs and finds no changes.

It matters because brokers' Auto Matching only puts forward spaces confirmed in the last 7 days, and the app starts warning you after 5. A quiet week with no lettings would otherwise take your whole portfolio out of matching. Units you send in an availability update or a unit update count as confirmed even when they come back unchanged, so a run that sends your full list already keeps them current; the heartbeat covers the rest.

  • Call it at the end of each sync run, once a day, whether or not anything changed. Only call it once your system has actually checked its data: it tells brokers your prices and availability are right.
  • Idempotent. Calling it twice simply confirms twice. The response says how many units were confirmed and how long they stay current.
curl -X POST "https://api.greatspace.ai/v1/inventory/heartbeat" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "source": "Yardi sync"
  }'

Body

FieldTypeWhat it does
buildingRefs string[] Confirm only these buildings, for a system that manages part of your portfolio. Leave it out to confirm everything.
source string A short label, such as the name of your sync job. It shows in your audit log next to the confirmation.

Create or update a building

PUT/v1/inventory/buildings/{buildingRef}Draft

Creates the building if the reference is new and answers 201 Created; otherwise updates it and answers 200 OK. It's an idempotent upsert, so a retry never creates a duplicate. Fields you send replace what we hold; fields you leave out stay as they are.

Include units to set up a whole building in one call, which is handy when onboarding a portfolio. Photos are fetched in the background: the response says how many are queued, and they appear within a few minutes.

curl -X PUT "https://api.greatspace.ai/v1/inventory/buildings/LDN-FOUNDRY" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "name": "The Foundry",
    "address": { "line1": "17 Oval Way", "city": "London", "postcode": "SE11 5RR", "country": "GB" },
    "description": "A converted Victorian print works, five minutes from Vauxhall station.",
    "amenities": ["Bike storage", "Showers", "Roof terrace", "24/7 access"],
    "photos": ["https://cdn.example-provider.com/foundry/front.jpg", "https://cdn.example-provider.com/foundry/reception.jpg"],
    "units": [
      {
        "unitRef": "F4-402",
        "floor": "4",
        "name": "Suite 4.02",
        "desks": 24,
        "sizeSqft": 1450
      },
      {
        "unitRef": "F5-501",
        "floor": "5",
        "name": "Suite 5.01",
        "desks": 40,
        "sizeSqft": 2300
      }
    ]
  }'

Fields

FieldTypeWhat it does
name required string The building's name as brokers should see it.
address required object { line1, line2, city, region, postcode, country }, all strings, with country as an ISO code. We geocode it.
latitude, longitude number Set the map pin yourself instead of geocoding the address.
description string Plain text. Shown on your Portal and in syndicated feeds.
amenities string[] Free text, one amenity per entry.
photos string[] Image URLs, primary first. We fetch and store copies, so they keep working if your links change. A new list replaces the old one.
units object[] Units to create or update in the same call, each shaped like the unit body below plus its unitRef. Units you leave out are left as they are, not removed.

Create or update a unit

PUT/v1/inventory/buildings/{buildingRef}/units/{unitRef}Draft

One floor or unit, with everything about it: size, desks, pricing, term and photos. Like the building call it's an idempotent upsert that answers 201 when it creates and 200 when it updates, and leaves out fields you don't send. For day-to-day price and status changes across many units, Update availability is lighter.

A new unit is created unpublished unless you send a status, so nothing reaches brokers before you're ready.

curl -X PUT "https://api.greatspace.ai/v1/inventory/buildings/LDN-FOUNDRY/units/F4-402" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "floor": "4",
    "name": "Suite 4.02",
    "type": "unit",
    "sizeSqft": 1450,
    "desks": 24,
    "meetingRooms": 1,
    "status": "published",
    "availableFrom": "2026-11-01",
    "price": { "amount": 14400, "currency": "GBP", "period": "month" },
    "term": { "minimumMonths": 12 },
    "description": "Corner suite with dual-aspect windows and a private meeting room.",
    "photos": ["https://cdn.example-provider.com/foundry/402-1.jpg"]
  }'

Fields

FieldTypeWhat it does
name required string The unit's name, such as Suite 4.02.
floor string The floor's name, such as 4 or Ground. A floor we haven't seen in this building is created.
type unitfloor unit by default; floor for a whole floor let as one space.
sizeSqft, sizeSqm number Send one. We work out the other.
desks, meetingRooms integer Counts.
status publishedunder_offerunpublished unpublished by default for a new unit, so nothing reaches brokers before you're ready.
availableFrom date | null As in Update availability.
price object { amount, currency, period }, as in Update availability.
term object { minimumMonths, maximumMonths }, both integers.
description string Plain text, for this unit.
photos string[] As for buildings, for this unit.

Withdraw a unit

DELETE/v1/inventory/buildings/{buildingRef}/units/{unitRef}Draft

For a unit that's gone for good: merged into another, converted, or no longer yours to let. It comes off your Portal, brokers' searches and every syndicated feed at once, so partners delete it on their next pull. Its history stays in Great Space, including deals and viewings that mention it.

For a unit that's let but will come back, set its status to unpublished instead. Withdrawing is idempotent too: withdrawing it again answers 200 with already_withdrawn, and a later create or update with the same reference brings it back.

curl -X DELETE "https://api.greatspace.ai/v1/inventory/buildings/LDN-FOUNDRY/units/F5-501" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN"

List your inventory

GET/v1/inventory/buildingsDraft

Every building we hold for you, with its units, your references and our ids. Use it to reconcile your system with ours: to check a sync worked, or to find units your team added in the app, which have no unitRef until you give them one.

curl "https://api.greatspace.ai/v1/inventory/buildings?limit=50" \
  -H "Authorization: Bearer $GREATSPACE_TOKEN"

Query parameters

ParameterTypeWhat it does
limit integer Buildings per page, 1 to 200. Defaults to 50.
cursor string The nextCursor from the previous page. Absent on the last page.
updatedSince datetime ISO 8601. Only buildings with a change since then, from you, your team in the app or another import.

Status codes and errors

Every response has a standard HTTP status code. 200 and 201 mean the call worked. Anything in the 4xx range is a problem with the request, which retrying unchanged won't fix, except 429; 5xx is a problem on our side, so retry with backoff.

An error's body is JSON, with an errorCode to branch on and an errorMessage for people, which may change. Every response has an x-correlation-id header, repeated in error bodies as correlationId; quote it if you ask us about a request. In a bulk update, a row that fails is reported in that row's result, and the response is still 200.

PUT /v1/inventory/buildings/LDN-FOUNDRY

400 Bad Request
{
  "success": false,
  "errorCode": "validation",
  "errorMessage": "address.postcode: required for a building in GB",
  "correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
}
Status codeDescriptionerrorCodeMeaning
400Bad RequestvalidationA field is missing or malformed. The message names it.
401Unauthorizedinvalid_tokenNo token, or one that is invalid, revoked or missing the Inventory API scope.
403ForbiddenforbiddenThe token is valid, but its organisation isn't a provider, or Inventory API access isn't switched on.
404Not Foundunknown_buildingNo building with that reference in your account.
404Not Foundunknown_unitNo unit with that reference. In a bulk update this is a row result in a 200 response instead.
409Conflictref_in_useThat reference already belongs to another building or unit.
429Too Many Requestsrate_limitedToo many requests. Wait and retry, ideally with backoff.

Until then

Providers can already keep inventory current without re-keying it:

  • Scheduled updates: link a spreadsheet from Google Sheets, Excel, Airtable or your property management tool, and Great Space syncs it daily or weekly.
  • Bulk import: upload a spreadsheet, email an attachment, or type availability changes into an email, and SpaceAI applies them.
  • Talk to us about a custom integration if neither fits your systems.