Exhibitions REST API - User Guide
The Exhibitions API lets an external system create and manage exhibition spaces and their object placements programmatically, using a scoped API key. It is a thin, authenticated layer over the same service the 3D builder UI uses, so anything the builder can do to a space or a placement can also be driven over HTTP.
This is separate from the public, read-only exports an exhibition already publishes (IIIF manifest.json, scene.json, exhibition.jsonld). Those need no key and are unchanged. It is also separate from the federated borrow endpoints (peer-scene / place-remote). Use this API when an external system needs to create, read, update, or delete exhibition spaces and placements.
Authentication and scopes
All endpoints require an API key (see the API Keys guide for issuing one). Scopes follow the same posture as the other resources:
| Scope | Allows |
|---|---|
read |
GET (list, show, list placements) |
write |
POST, PUT (create/update spaces and placements) |
delete |
DELETE (remove spaces and placements) |
Send the key as Authorization: Bearer <key> (or the other supported key headers). Writes honour idempotency, ETag, and CORS via the standard API middleware.
Endpoints (v1)
GET api/v1/exhibitions list spaces (paginated) read
GET api/v1/exhibitions/{slug} show a space + its placements read
POST api/v1/exhibitions create a space write
PUT api/v1/exhibitions/{slug} update a space write
DELETE api/v1/exhibitions/{slug} delete a space delete
GET api/v1/exhibitions/{slug}/placements list placements (local + remote) read
POST api/v1/exhibitions/{slug}/placements add or update a local placement write
POST api/v1/exhibitions/{slug}/placements/remote borrow a remote placement (#1277) write
PUT api/v1/exhibitions/{slug}/placements/{id} update a local placement write
DELETE api/v1/exhibitions/{slug}/placements/{id} remove a placement delete
The same read endpoints are mirrored under the api/v2 read group: GET api/v2/exhibitions and GET api/v2/exhibitions/{slug}.
Examples
Create an exhibition space
POST /api/v1/exhibitions
Authorization: Bearer <write-key>
Content-Type: application/json
{ "title": "Voices of the Coast", "description": "Oral histories, 1900-1960" }
Returns 201 with the created space (including its generated slug).
Add a local object placement
POST /api/v1/exhibitions/voices-of-the-coast/placements
Authorization: Bearer <write-key>
Content-Type: application/json
{ "information_object_id": 10421, "size_units_used": 2.5,
"starts_at": "2027-03-02", "ends_at": "2027-06-30" }
Returns 201 with { "id": <placement_id> }. Posting the same body again with a placement_id updates that placement in place and returns 200.
Update a placement (dates only)
PUT /api/v1/exhibitions/voices-of-the-coast/placements/87
Authorization: Bearer <write-key>
Content-Type: application/json
{ "starts_at": "2027-04-01", "ends_at": "2027-07-31" }
PUT behaves as a merge: any field you omit keeps its current value, so you can adjust dates without resetting size or notes. Returns 200.
Borrow a remote object (federation, #1277)
POST /api/v1/exhibitions/voices-of-the-coast/placements/remote
Authorization: Bearer <write-key>
Content-Type: application/json
{ "remote_peer_id": 4, "remote_ref": "peer-obj-991",
"remote_payload": { ...normalised peer object... } }
Remote (borrowed) placements are read-only mirrors and are managed only through this /placements/remote endpoint, not through PUT .../placements/{id}.
Validation and errors
- All writes are validated. Missing or malformed fields return
422with amessagesmap. - A placement whose date range would exceed the space's capacity returns
422with a clear "would exceed capacity by N units" message. starts_atafterends_atreturns422.- Deleting a space that still has placements returns
409(remove the placements first). - A placement that does not belong to the addressed exhibition returns
404(placements are always scoped to their space).
Notes
- No business logic is duplicated in the API layer; it calls
AhgExhibition\Services\ExhibitionSpaceServicedirectly, so capacity rules, slug generation, and date checks behave exactly as they do in the builder UI. - The public exports and the federated borrow endpoints are unaffected by this API.
Refs: issue #1280; federation #1277; scene export #1151.