docs / inventory api (draft)
Inventory API
- 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.
- PATCH
/v1/inventory/availabilityUpdate availability and prices for many units in one callDraft - POST
/v1/inventory/heartbeatConfirm your inventory is still current, even when nothing has changedDraft - PUT
/v1/inventory/buildings/{buildingRef}Create or update a building, keyed by your own referenceDraft - PUT
/v1/inventory/buildings/{buildingRef}/units/{unitRef}Create or update a floor or unit in a buildingDraft - DELETE
/v1/inventory/buildings/{buildingRef}/units/{unitRef}Withdraw a unitDraft - GET
/v1/inventory/buildingsList what we hold for you, with your references and oursDraft
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" }
]
}' const res = await fetch('https://api.greatspace.ai/v1/inventory/availability', {
method: 'PATCH',
headers: {
Authorization: `Bearer ${process.env.GREATSPACE_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
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" }
]
}),
})
const body = await res.json()
if (!body.success) throw new Error(`${res.status} ${body.errorCode}: ${body.errorMessage}`)
console.log(body.data) 200 OK
{
"success": true,
"data": {
"updated": 2,
"unchanged": 1,
"errors": 1,
"results": [
{
"unitRef": "F4-402",
"spaceId": "sp_8Qm2xR4kLp",
"result": "updated",
"changed": ["availableFrom", "price"]
},
{
"unitRef": "F5-501",
"spaceId": "sp_1Zt7Nc0vWd",
"result": "updated",
"changed": ["status"]
},
{ "unitRef": "F2-210", "spaceId": "sp_4Hc2Lm8qPx", "result": "unchanged" },
{
"unitRef": "F9-999",
"result": "error",
"errorCode": "unknown_unit",
"errorMessage": "No unit with reference F9-999"
}
]
}
} 400 Bad Request
{
"success": false,
"errorCode": "validation",
"errorMessage": "updates: send between 1 and 1,000 rows",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} Fields in each update
| Field | Type | What 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"
}' const res = await fetch('https://api.greatspace.ai/v1/inventory/heartbeat', {
method: 'POST',
headers: {
Authorization: `Bearer ${process.env.GREATSPACE_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
source: "Yardi sync"
}),
})
const body = await res.json()
if (!body.success) throw new Error(`${res.status} ${body.errorCode}: ${body.errorMessage}`)
console.log(body.data) 200 OK
{
"success": true,
"data": {
"confirmed": 128,
"confirmedAt": "2026-10-15T06:00:02.000Z",
"currentUntil": "2026-10-22T06:00:02.000Z"
}
} 403 Forbidden
{
"success": false,
"errorCode": "forbidden",
"errorMessage": "This token does not carry the Inventory API scope.",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} Body
| Field | Type | What 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
}
]
}' const res = await fetch('https://api.greatspace.ai/v1/inventory/buildings/LDN-FOUNDRY', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.GREATSPACE_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
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
}
]
}),
})
const body = await res.json()
if (!body.success) throw new Error(`${res.status} ${body.errorCode}: ${body.errorMessage}`)
console.log(body.data) 201 Created
{
"success": true,
"data": {
"buildingRef": "LDN-FOUNDRY",
"buildingId": "sp_7Rw3Kd9mQa",
"result": "created",
"units": [
{ "unitRef": "F4-402", "spaceId": "sp_8Qm2xR4kLp", "result": "created" },
{ "unitRef": "F5-501", "spaceId": "sp_1Zt7Nc0vWd", "result": "created" }
],
"photos": { "queued": 2 }
}
} 400 Bad Request
{
"success": false,
"errorCode": "validation",
"errorMessage": "address.postcode: required for a building in GB",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} Fields
| Field | Type | What 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"]
}' const res = await fetch('https://api.greatspace.ai/v1/inventory/buildings/LDN-FOUNDRY/units/F4-402', {
method: 'PUT',
headers: {
Authorization: `Bearer ${process.env.GREATSPACE_TOKEN}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({
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"]
}),
})
const body = await res.json()
if (!body.success) throw new Error(`${res.status} ${body.errorCode}: ${body.errorMessage}`)
console.log(body.data) 200 OK
{
"success": true,
"data": {
"buildingRef": "LDN-FOUNDRY",
"unitRef": "F4-402",
"spaceId": "sp_8Qm2xR4kLp",
"result": "updated",
"changed": ["meetingRooms", "description", "photos"]
}
} 404 Not Found
{
"success": false,
"errorCode": "unknown_building",
"errorMessage": "No building with reference LDN-FOUNDRY",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} Fields
| Field | Type | What 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" const res = await fetch('https://api.greatspace.ai/v1/inventory/buildings/LDN-FOUNDRY/units/F5-501', {
method: 'DELETE',
headers: {
Authorization: `Bearer ${process.env.GREATSPACE_TOKEN}`,
},
})
const body = await res.json()
if (!body.success) throw new Error(`${res.status} ${body.errorCode}: ${body.errorMessage}`)
console.log(body.data) 200 OK
{
"success": true,
"data": {
"buildingRef": "LDN-FOUNDRY",
"unitRef": "F5-501",
"spaceId": "sp_1Zt7Nc0vWd",
"result": "withdrawn",
"withdrawnAt": "2026-10-15T10:42:07.000Z"
}
} 404 Not Found
{
"success": false,
"errorCode": "unknown_unit",
"errorMessage": "No unit with reference F5-501 in LDN-FOUNDRY",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} 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" const res = await fetch('https://api.greatspace.ai/v1/inventory/buildings?limit=50', {
headers: {
Authorization: `Bearer ${process.env.GREATSPACE_TOKEN}`,
},
})
const body = await res.json()
if (!body.success) throw new Error(`${res.status} ${body.errorCode}: ${body.errorMessage}`)
console.log(body.data) 200 OK
{
"success": true,
"data": [
{
"buildingRef": "LDN-FOUNDRY",
"buildingId": "sp_7Rw3Kd9mQa",
"name": "The Foundry",
"updatedAt": "2026-10-15T09:12:44.000Z",
"units": [
{
"unitRef": "F4-402",
"spaceId": "sp_8Qm2xR4kLp",
"name": "Suite 4.02",
"status": "published",
"availableFrom": "2026-11-01"
},
{
"unitRef": null,
"spaceId": "sp_6Yp1Vb5nTe",
"name": "Suite 6.01",
"status": "unpublished",
"availableFrom": null
}
]
}
],
"pagination": { "nextCursor": "eyJhZnRlciI6IkxETi1GT1VORFJZIn0", "count": 1 }
} 401 Unauthorized
{
"success": false,
"errorCode": "invalid_token",
"errorMessage": "This access token is not valid.",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} Query parameters
| Parameter | Type | What 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"
} PATCH /v1/inventory/availability
401 Unauthorized
{
"success": false,
"errorCode": "invalid_token",
"errorMessage": "This access token is not valid.",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} DELETE /v1/inventory/buildings/LDN-FOUNDRY/units/F5-501
404 Not Found
{
"success": false,
"errorCode": "unknown_unit",
"errorMessage": "No unit with reference F5-501 in LDN-FOUNDRY",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} PUT /v1/inventory/buildings/LDN-FOUNDRY/units/F4-402
409 Conflict
{
"success": false,
"errorCode": "ref_in_use",
"errorMessage": "Reference F4-402 already belongs to a unit in LDN-SHOREDITCH",
"correlationId": "8f0c2e1a-5d7b-4c39-9a51-2b6e0f4d7c18"
} | Status code | Description | errorCode | Meaning |
|---|---|---|---|
400 | Bad Request | validation | A field is missing or malformed. The message names it. |
401 | Unauthorized | invalid_token | No token, or one that is invalid, revoked or missing the Inventory API scope. |
403 | Forbidden | forbidden | The token is valid, but its organisation isn't a provider, or Inventory API access isn't switched on. |
404 | Not Found | unknown_building | No building with that reference in your account. |
404 | Not Found | unknown_unit | No unit with that reference. In a bulk update this is a row result in a 200 response instead. |
409 | Conflict | ref_in_use | That reference already belongs to another building or unit. |
429 | Too Many Requests | rate_limited | Too 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.