Chapter 1
Start here
The API does what the Exhibitions workspace does: your captures, their exhibits and drafts, collections, and the webhooks that tell your own systems when something changes.
Everything is at https://heimdall3d.com/api/v1, answers JSON, and is described in full as OpenAPI 3.1 at /api/v1/openapi.json. The exhibit format has its own JSON Schema at /api/v1/schema/exhibit.json, every field explained.
A change you make is a draft that visitors do not see until it is published. Publishing is a permission of its own, so a program or an assistant can do the work while a person decides when it goes out.
The server stores and serves. A capture reaches it already converted, in the browser of whoever chose the file, and nothing is converted on the server.
Chapter 2
Make a key
A key belongs to your organisation, or to you, and carries what it may do.
In the Exhibitions workspace, open Keys and make one. Choose what it may do: read sees your pieces, exhibits and collections; write changes them, every exhibit change a draft; publish makes a draft what visitors see. The key is shown once; keep it where you keep other secrets.
Send it on every request as Authorization: Bearer h3d_…. A key makes at most 120 requests a minute; past that the answer is 429 with Retry-After.
/api/v1/piecesreadList the pieces
Every capture and picture of the account, in the order of its page.
curl -X GET https://heimdall3d.com/api/v1/pieces \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"pieces": [
{
"bytes": 48210044,
"embed": "https://heimdall3d.com/u/trogir-museum/…",
"exhibit_changed_at": "2026-09-30T09:12:00Z",
"has_draft": true,
"has_exhibit": true,
"id": 128,
"kind": "model",
"note": "The old town, captured in 2026.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"title": "Trogir, Croatia",
"triangles": 0,
"views": 3412
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute
Chapter 3
Your first exhibit
Read what is there, save a zone as a draft, share the draft for checking, then publish.
Send the whole exhibit. If a field does not match the format, the answer names it, as zones[3].title, so you can put it right.
A preview link shows the draft, read only, to whoever has it, for 1 to 90 days. Publishing keeps the version, with who published it, and tells your webhooks.
/api/v1/pieces/{id}/exhibitreadRead a piece's exhibit
The exhibit as its editors see it: the draft when there is one, otherwise what readers see. ?state=published reads what readers see. The X-Exhibit-State header says which it is.
curl -X GET https://heimdall3d.com/api/v1/pieces/128/exhibit \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"version": 1,
"zones": [
{
"center": [
48.77,
-0.43,
-169.34
],
"id": "west-portal",
"radius": 6,
"shape": "cylinder",
"story": {
"sources": [
{
"label": "Trogir Cathedral",
"url": "https://en.wikipedia.org/wiki/Trogir_Cathedral"
}
],
"title": "Radovan"
},
"text": "Master Radovan signed the west portal in 1240.",
"title": "The west portal"
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibitwriteSave a piece's exhibit as a draft
The whole exhibit, as /api/v1/schema/exhibit.json describes it. It is kept as a draft that readers do not see until it is published. A field that does not match the format is named in the answer.
curl -X PUT https://heimdall3d.com/api/v1/pieces/128/exhibit \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"version":1,"zones":[{"center":[48.77,-0.43,-169.34],"id":"west-portal","radius":6,"shape":"cylinder","story":{"sources":[{"label":"Trogir Cathedral","url":"https://en.wikipedia.org/wiki/Trogir_Cathedral"}],"title":"Radovan"},"text":"Master Radovan signed the west portal in 1240.","title":"The west portal"}]}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit", {
method: "PUT",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"version": 1,
"zones": [
{
"center": [
48.77,
-0.43,
-169.34
],
"id": "west-portal",
"radius": 6,
"shape": "cylinder",
"story": {
"sources": [
{
"label": "Trogir Cathedral",
"url": "https://en.wikipedia.org/wiki/Trogir_Cathedral"
}
],
"title": "Radovan"
},
"text": "Master Radovan signed the west portal in 1240.",
"title": "The west portal"
}
]
}),
});
const answer = await response.json();{
"draft": true
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/pieces/{id}/previewswriteMake a preview link
A link that shows the draft, read only, to whoever has it, for 1 to 90 days.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/previews \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"days":7,"label":"the director"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/previews", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"days": 7,
"label": "the director"
}),
});
const answer = await response.json();{
"created": "2026-09-30T10:02:00Z",
"expires": "2026-10-07T10:02:00Z",
"id": 128,
"label": "the director",
"state": "open",
"url": "https://heimdall3d.com/u/trogir-museum/…"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/pieces/{id}/exhibit/publishpublishPublish the draft
Readers see the draft from now on; it is kept as a version, and the webhooks told. 409 when there is nothing new to publish.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/exhibit/publish \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/publish", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the publish scope · 429 past 120 a minute · 404 not the key's
Chapter 4
Drafts and history
Every publish is kept. Throw a draft away, look at an earlier version, or bring one back as the draft.
/api/v1/pieces/{id}/exhibit/draftwriteDiscard the draft
Throws away changes readers have not seen. 409 when there are none.
curl -X DELETE https://heimdall3d.com/api/v1/pieces/128/exhibit/draft \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/draft", {
method: "DELETE",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibit/versionsreadList the published versions
curl -X GET https://heimdall3d.com/api/v1/pieces/128/exhibit/versions \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/versions", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"versions": [
{
"at": "2026-09-29T16:40:00Z",
"by": "ana-kovac",
"bytes": 48210044,
"id": 128,
"removed": false
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibit/versions/{version}/restorewriteRestore a version as the draft
The version becomes the draft, to be published or changed further.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/exhibit/versions/6/restore \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/versions/6/restore", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's
Chapter 5
Captures, and replacing them
Upload a capture already converted, and swap in a better one without losing its exhibit.
A new capture of the same place takes the old one's place: the piece keeps its id, and so its exhibit, links, embeds, collections and views. If the new capture sits in another frame, the exhibit editor moves every zone at once.
/api/v1/pieceswriteUpload a piece
A multipart form with one file field, "file": a glb, a streamed package (.ssog, .smesh, .spoints) or a picture, already converted on your side. The server stores it and converts nothing. Larger files go in parts, through the resumable upload road.
curl -X POST https://heimdall3d.com/api/v1/pieces \ -H "Authorization: Bearer h3d_your_key" \ -F "file=@trogir.ssog.zip"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key" },
body: form, // a FormData holding the file under "file"
});
const answer = await response.json();{
"allowance": 107374182400,
"edit": "https://heimdall3d.com/u/trogir-museum/…",
"id": 128,
"page": "https://heimdall3d.com/u/trogir-museum/…",
"used": 1210044221
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 400 a wrong body, the field named
/api/v1/pieces/{id}/replacewriteReplace a piece's file
Upload the new file as a piece first, then name it here: it takes this piece's place, which keeps its id, exhibit, links, embeds, collections and view counts, and the uploaded piece goes. A model is replaced by a model, a picture by a picture.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/replace \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"from":131}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/replace", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"from": 131
}),
});
const answer = await response.json();{
"bytes": 48210044,
"embed": "https://heimdall3d.com/u/trogir-museum/…",
"exhibit_changed_at": "2026-09-30T09:12:00Z",
"has_draft": true,
"has_exhibit": true,
"id": 128,
"kind": "model",
"note": "The old town, captured in 2026.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"title": "Trogir, Croatia",
"triangles": 0,
"views": 3412
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
Chapter 6
Collections
Gather pieces on a page of their own: private, unlisted with a link, or public.
/api/v1/collectionswriteMake a collection
A new collection, private until its visibility is changed.
curl -X POST https://heimdall3d.com/api/v1/collections \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"title":"Trogir, Croatia"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"title": "Trogir, Croatia"
}),
});
const answer = await response.json();{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 400 a wrong body, the field named
/api/v1/collections/{id}/pieceswriteSet a collection's pieces
The pieces it holds, in order. A piece that is not the account's is left out.
curl -X PUT https://heimdall3d.com/api/v1/collections/14/pieces \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"pieces":[128,131]}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections/14/pieces", {
method: "PUT",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"pieces": [
128,
131
]
}),
});
const answer = await response.json();{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/collections/{id}writeChange a collection
Any of its title, introduction and visibility; what is left out stays.
curl -X PATCH https://heimdall3d.com/api/v1/collections/14 \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"intro":"Radovan's portal and the chapels.","title":"Trogir, Croatia","visibility":"unlisted"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections/14", {
method: "PATCH",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"intro": "Radovan's portal and the chapels.",
"title": "Trogir, Croatia",
"visibility": "unlisted"
}),
});
const answer = await response.json();{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
Chapter 7
Webhooks
Tell your own systems when an exhibit is published or a capture uploaded.
Every delivery is a POST signed with the webhook's secret: X-Heimdall-Signature: t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>">. Check it before you trust the body. A delivery not answered 2xx within ten seconds is tried again, eight times over about two days.
/api/v1/webhookswriteAdd a webhook
An https address told of events by a signed POST: X-Heimdall-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>" under the secret>. The secret is answered once. A delivery that is not answered 2xx within ten seconds is tried again, eight times over about two days.
curl -X POST https://heimdall3d.com/api/v1/webhooks \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"events":["exhibit.published"],"url":"https://museum.example/hooks/heimdall"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/webhooks", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"events": [
"exhibit.published"
],
"url": "https://museum.example/hooks/heimdall"
}),
});
const answer = await response.json();{
"secret": "whsec_…",
"webhook": {
"created": "2026-09-30T10:02:00Z",
"events": [
"exhibit.published"
],
"id": 128,
"last_at": "2026-09-30T10:06:00Z",
"last_status": "200 OK",
"url": "https://heimdall3d.com/u/trogir-museum/…"
}
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 400 a wrong body, the field named
Chapter 8
Assistants
Let an assistant build exhibits through the MCP server, with the same keys.
The MCP server is https://heimdall3d.com/api/v1/mcp, over Streamable HTTP. Connect any MCP client with your key; what it changes is a draft unless the key may publish. The builder's guide, with worked prompts, is /api/v1/guide.md.
claude mcp add --transport http skirnir https://heimdall3d.com/api/v1/mcp --header "Authorization: Bearer h3d_your_key"
Chapter 9
Reference
Every route, with a request and an example answer.
/api/v1/piecesreadList the pieces
Every capture and picture of the account, in the order of its page.
curl -X GET https://heimdall3d.com/api/v1/pieces \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"pieces": [
{
"bytes": 48210044,
"embed": "https://heimdall3d.com/u/trogir-museum/…",
"exhibit_changed_at": "2026-09-30T09:12:00Z",
"has_draft": true,
"has_exhibit": true,
"id": 128,
"kind": "model",
"note": "The old town, captured in 2026.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"title": "Trogir, Croatia",
"triangles": 0,
"views": 3412
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute
/api/v1/pieceswriteUpload a piece
A multipart form with one file field, "file": a glb, a streamed package (.ssog, .smesh, .spoints) or a picture, already converted on your side. The server stores it and converts nothing. Larger files go in parts, through the resumable upload road.
curl -X POST https://heimdall3d.com/api/v1/pieces \ -H "Authorization: Bearer h3d_your_key" \ -F "file=@trogir.ssog.zip"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key" },
body: form, // a FormData holding the file under "file"
});
const answer = await response.json();{
"allowance": 107374182400,
"edit": "https://heimdall3d.com/u/trogir-museum/…",
"id": 128,
"page": "https://heimdall3d.com/u/trogir-museum/…",
"used": 1210044221
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 400 a wrong body, the field named
/api/v1/pieces/{id}readRead a piece
curl -X GET https://heimdall3d.com/api/v1/pieces/128 \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"bytes": 48210044,
"embed": "https://heimdall3d.com/u/trogir-museum/…",
"exhibit_changed_at": "2026-09-30T09:12:00Z",
"has_draft": true,
"has_exhibit": true,
"id": 128,
"kind": "model",
"note": "The old town, captured in 2026.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"title": "Trogir, Croatia",
"triangles": 0,
"views": 3412
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/replacewriteReplace a piece's file
Upload the new file as a piece first, then name it here: it takes this piece's place, which keeps its id, exhibit, links, embeds, collections and view counts, and the uploaded piece goes. A model is replaced by a model, a picture by a picture.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/replace \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"from":131}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/replace", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"from": 131
}),
});
const answer = await response.json();{
"bytes": 48210044,
"embed": "https://heimdall3d.com/u/trogir-museum/…",
"exhibit_changed_at": "2026-09-30T09:12:00Z",
"has_draft": true,
"has_exhibit": true,
"id": 128,
"kind": "model",
"note": "The old town, captured in 2026.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"title": "Trogir, Croatia",
"triangles": 0,
"views": 3412
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/pieces/{id}/exhibitreadRead a piece's exhibit
The exhibit as its editors see it: the draft when there is one, otherwise what readers see. ?state=published reads what readers see. The X-Exhibit-State header says which it is.
curl -X GET https://heimdall3d.com/api/v1/pieces/128/exhibit \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"version": 1,
"zones": [
{
"center": [
48.77,
-0.43,
-169.34
],
"id": "west-portal",
"radius": 6,
"shape": "cylinder",
"story": {
"sources": [
{
"label": "Trogir Cathedral",
"url": "https://en.wikipedia.org/wiki/Trogir_Cathedral"
}
],
"title": "Radovan"
},
"text": "Master Radovan signed the west portal in 1240.",
"title": "The west portal"
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibitwriteSave a piece's exhibit as a draft
The whole exhibit, as /api/v1/schema/exhibit.json describes it. It is kept as a draft that readers do not see until it is published. A field that does not match the format is named in the answer.
curl -X PUT https://heimdall3d.com/api/v1/pieces/128/exhibit \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"version":1,"zones":[{"center":[48.77,-0.43,-169.34],"id":"west-portal","radius":6,"shape":"cylinder","story":{"sources":[{"label":"Trogir Cathedral","url":"https://en.wikipedia.org/wiki/Trogir_Cathedral"}],"title":"Radovan"},"text":"Master Radovan signed the west portal in 1240.","title":"The west portal"}]}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit", {
method: "PUT",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"version": 1,
"zones": [
{
"center": [
48.77,
-0.43,
-169.34
],
"id": "west-portal",
"radius": 6,
"shape": "cylinder",
"story": {
"sources": [
{
"label": "Trogir Cathedral",
"url": "https://en.wikipedia.org/wiki/Trogir_Cathedral"
}
],
"title": "Radovan"
},
"text": "Master Radovan signed the west portal in 1240.",
"title": "The west portal"
}
]
}),
});
const answer = await response.json();{
"draft": true
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/pieces/{id}/exhibit/draftwriteDiscard the draft
Throws away changes readers have not seen. 409 when there are none.
curl -X DELETE https://heimdall3d.com/api/v1/pieces/128/exhibit/draft \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/draft", {
method: "DELETE",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibit/publishpublishPublish the draft
Readers see the draft from now on; it is kept as a version, and the webhooks told. 409 when there is nothing new to publish.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/exhibit/publish \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/publish", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the publish scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibit/versionsreadList the published versions
curl -X GET https://heimdall3d.com/api/v1/pieces/128/exhibit/versions \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/versions", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"versions": [
{
"at": "2026-09-29T16:40:00Z",
"by": "ana-kovac",
"bytes": 48210044,
"id": 128,
"removed": false
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibit/versions/{version}readRead a published version
curl -X GET https://heimdall3d.com/api/v1/pieces/128/exhibit/versions/6 \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/versions/6", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"version": 1,
"zones": [
{
"center": [
48.77,
-0.43,
-169.34
],
"id": "west-portal",
"radius": 6,
"shape": "cylinder",
"story": {
"sources": [
{
"label": "Trogir Cathedral",
"url": "https://en.wikipedia.org/wiki/Trogir_Cathedral"
}
],
"title": "Radovan"
},
"text": "Master Radovan signed the west portal in 1240.",
"title": "The west portal"
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/exhibit/versions/{version}/restorewriteRestore a version as the draft
The version becomes the draft, to be published or changed further.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/exhibit/versions/6/restore \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/exhibit/versions/6/restore", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/previewsreadList the preview links
curl -X GET https://heimdall3d.com/api/v1/pieces/128/previews \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/previews", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"previews": [
{
"created": "2026-09-30T10:02:00Z",
"expires": "2026-10-07T10:02:00Z",
"id": 128,
"label": "the director",
"state": "open",
"url": "https://heimdall3d.com/u/trogir-museum/…"
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/pieces/{id}/previewswriteMake a preview link
A link that shows the draft, read only, to whoever has it, for 1 to 90 days.
curl -X POST https://heimdall3d.com/api/v1/pieces/128/previews \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"days":7,"label":"the director"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/previews", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"days": 7,
"label": "the director"
}),
});
const answer = await response.json();{
"created": "2026-09-30T10:02:00Z",
"expires": "2026-10-07T10:02:00Z",
"id": 128,
"label": "the director",
"state": "open",
"url": "https://heimdall3d.com/u/trogir-museum/…"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/pieces/{id}/previews/{preview}writeWithdraw a preview link
curl -X DELETE https://heimdall3d.com/api/v1/pieces/128/previews/12 \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/pieces/128/previews/12", {
method: "DELETE",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's
/api/v1/collectionsreadList the collections
curl -X GET https://heimdall3d.com/api/v1/collections \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"collections": [
{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute
/api/v1/collectionswriteMake a collection
A new collection, private until its visibility is changed.
curl -X POST https://heimdall3d.com/api/v1/collections \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"title":"Trogir, Croatia"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"title": "Trogir, Croatia"
}),
});
const answer = await response.json();{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 400 a wrong body, the field named
/api/v1/collections/{id}readRead a collection
curl -X GET https://heimdall3d.com/api/v1/collections/14 \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections/14", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
401 no key · 403 a key without the read scope · 429 past 120 a minute · 404 not the key's
/api/v1/collections/{id}writeChange a collection
Any of its title, introduction and visibility; what is left out stays.
curl -X PATCH https://heimdall3d.com/api/v1/collections/14 \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"intro":"Radovan's portal and the chapels.","title":"Trogir, Croatia","visibility":"unlisted"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections/14", {
method: "PATCH",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"intro": "Radovan's portal and the chapels.",
"title": "Trogir, Croatia",
"visibility": "unlisted"
}),
});
const answer = await response.json();{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/collections/{id}/pieceswriteSet a collection's pieces
The pieces it holds, in order. A piece that is not the account's is left out.
curl -X PUT https://heimdall3d.com/api/v1/collections/14/pieces \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"pieces":[128,131]}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections/14/pieces", {
method: "PUT",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"pieces": [
128,
131
]
}),
});
const answer = await response.json();{
"id": 128,
"intro": "Radovan's portal and the chapels.",
"page": "https://heimdall3d.com/u/trogir-museum/…",
"pieces": [
128,
131
],
"slug": "the-cathedral",
"title": "Trogir, Croatia",
"updated": "2026-09-30T10:05:00Z",
"visibility": "unlisted"
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's · 400 a wrong body, the field named
/api/v1/collections/{id}writeDelete a collection
The collection goes; its pieces stay.
curl -X DELETE https://heimdall3d.com/api/v1/collections/14 \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/collections/14", {
method: "DELETE",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's
/api/v1/webhooksreadList the webhooks
curl -X GET https://heimdall3d.com/api/v1/webhooks \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/webhooks", {
method: "GET",
headers: { Authorization: "Bearer h3d_your_key" },
});
const answer = await response.json();{
"webhooks": [
{
"created": "2026-09-30T10:02:00Z",
"events": [
"exhibit.published"
],
"id": 128,
"last_at": "2026-09-30T10:06:00Z",
"last_status": "200 OK",
"url": "https://heimdall3d.com/u/trogir-museum/…"
}
]
}
401 no key · 403 a key without the read scope · 429 past 120 a minute
/api/v1/webhookswriteAdd a webhook
An https address told of events by a signed POST: X-Heimdall-Signature is t=<unix seconds>,v1=<hex HMAC-SHA256 of "<t>.<body>" under the secret>. The secret is answered once. A delivery that is not answered 2xx within ten seconds is tried again, eight times over about two days.
curl -X POST https://heimdall3d.com/api/v1/webhooks \
-H "Authorization: Bearer h3d_your_key" \
-H "Content-Type: application/json" \
-d '{"events":["exhibit.published"],"url":"https://museum.example/hooks/heimdall"}'
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/webhooks", {
method: "POST",
headers: { Authorization: "Bearer h3d_your_key", "Content-Type": "application/json" },
body: JSON.stringify({
"events": [
"exhibit.published"
],
"url": "https://museum.example/hooks/heimdall"
}),
});
const answer = await response.json();{
"secret": "whsec_…",
"webhook": {
"created": "2026-09-30T10:02:00Z",
"events": [
"exhibit.published"
],
"id": 128,
"last_at": "2026-09-30T10:06:00Z",
"last_status": "200 OK",
"url": "https://heimdall3d.com/u/trogir-museum/…"
}
}
401 no key · 403 a key without the write scope · 429 past 120 a minute · 400 a wrong body, the field named
/api/v1/webhooks/{id}writeRemove a webhook
curl -X DELETE https://heimdall3d.com/api/v1/webhooks/3 \ -H "Authorization: Bearer h3d_your_key"
In JavaScript
const response = await fetch("https://heimdall3d.com/api/v1/webhooks/3", {
method: "DELETE",
headers: { Authorization: "Bearer h3d_your_key" },
});
// 204, with no body, when it worked(no body)
401 no key · 403 a key without the write scope · 429 past 120 a minute · 404 not the key's
The same routes as OpenAPI 3.1, for code generators and assistants: /api/v1/openapi.json. The exhibit format: /api/v1/schema/exhibit.json.