This is 2026.9 documentation. Coverage here is incremental: a page exists only where 2026.9
differs or is newly documented. For any other topic, switch to Current (2026.8 and earlier) in
the version selector.
success. This page assumes you know what a bundle and a
bundle builder are from Building integrations in 2026.9.
How a publish creates a version
The version id is allocated before any bytes move, and the version is born on the server. Every Speckle producer follows this sequence.1
Open a model ingestion
Call the ingestion create mutation with the model, the source application slug and version, a
progress message, and an idle timeout. The outcome is an ingestion id and a reserved version id.
If the response has no version id, the server predates 2026.9. Stop; there is no fallback on
this rail.
2
Build the bundle under that version id
Run your conversion and write the files with the reserved id as their stem. The outcome is a
complete bundle on local disk that you can validate before anything is uploaded.
3
Sign the uploads
Post the list of bare filenames to the sign endpoint. The outcome is one presigned URL per file.
4
Upload each file
Put each file to its URL with no extra headers, and keep the returned ETag exactly as received.
The outcome is every file in storage and an ETag per filename.
5
Complete the upload
Post the ETags, the bundle reference as the root id, the object count, and the version message.
The outcome is the server accepting the bundle and starting ingestion. The response echoes the
version id.
6
Wait for the ingestion to finish
Subscribe to ingestion updates or poll until the status is terminal. The outcome on
success is
a version that answers queries, has a viewer .dat, and fires version_created.- GraphQL
- REST
- Python
- .NET
Variables for IngestionCreate
Forward the ETag byte for byte. Object storage wraps it in double quotes and the complete endpoint
compares against that exact value. Stripping the quotes fails the publish with an ETag mismatch
that looks like a server fault.
Close the ingestion on every exit. Report
failWithError, failWithInvalid, or failWithCancel
through the ingestion mutations, using a request that cannot itself be cancelled. An ingestion you
leave open sits until the idle timeout fires.cancellationRequested on every ingestion response.
Rules that decide whether your bundle is correct
The server checks only that every file arrived with a matching ETag. Correctness is checked by the bundle spec’s validator before upload and by consumers after. These are the rules producers most often get wrong.Identity
applicationIdmust be stable across publishes and unique per placement. A linked file placed twice needs a per-placement suffix, or its objects and their topology collide.- Key geometry on the placed object, never on a shared source block. Two objects sharing one mesh describe different world-space geometry.
- Derive geometry and definition keys from content, quantized, not from random ids. Random keys make material bindings fragile and defeat definition deduplication.
- An object with no properties, no geometry, and no relations is noise. Do not intern it.
Properties
- One value per row: exactly one of the string, number, or boolean columns is set. Numbers must be finite.
- Paths are dotted. Nesting is rebuilt from the path, never stored as a blob.
- Stamp the root scalars on every object:
speckle_type,name,units, and the host’s owntype. - Parameters shared by a type go to the type tables once, linked from each object, not repeated per object.
- Decide a policy for multi-valued properties. The reference walk drops arrays; joining them is a valid alternative when your host emits them.
Geometry
- Write the SGEO header exactly: magic, version, primitive type, flags, the spec’s unit code, and a CRC32 over the body, all little-endian. Consumers verify the checksum. See Geometry encoding in 2026.9.
DISPLAY,SOLID, andCENTERLINEeach keep their own ordinal counter per object.- Ship the solid beside the display mesh, not instead of it. Foreign hosts read the mesh.
CENTERLINEis never a render edge. A consumer that drew every geometry an object owns would draw the axis through the duct.- Shard the geometry table at 1536 MiB of uncompressed content. Shard zero keeps the canonical name. Readers glob all shards.
Instancing
- A definition owns its geometry once. Each placement is an instance node with a transform of 16 row-major doubles and its own units.
- When you convert transform units, scale only the translation column. Scaling all sixteen values resizes the instance.
- Definition members get no render edge. They keep an object row for properties and membership, and
join back through
DEFINES_MEMBERandPLACES. A member with a render edge draws twice. - Create definition nodes before you walk placements, so a placement never references a nameless definition.
Grouping
- Membership is an edge (
IN_COLLECTION,IN_MODEL,IN_GROUP,IN_SYSTEM). Nesting of containers is a parent reference on the container node. Do not conflate them. - Containers are one node kind with a subtype (Layer, Collection, Folder, Model, Group, MEP System, Network). Key them by full path so two branches with the same leaf name stay distinct.
- The tree a viewer shows is a scene view: a recipe of tiers, some relations and some property
paths. Declare one, or accept the default over
IN_COLLECTION. - Emit no container that would end up empty in the view.
Appearance
- Bind materials to geometry with
HAS_MATERIAL. The object plane only fills where geometry has none, and is the only option for a placement. HAS_COLORcan start from geometry or from an object, andordsays which. Without it, the color lands on an unrelated element.- Resolve inherited, by-layer appearance yourself. Land the layer’s material on the layer node and on each inheriting object’s own geometry.
Topology
- Emit an edge only when both ends are objects you sent. Resolve cross-object links in a second pass against your key map.
ordis an ordinal on ordered relations and a scope tag onCONNECTS_TO. Read the catalog.HOSTED_ONruns hosted to host. Ownership (SUBELEMENT) wins over hosting when both apply.- Never reuse a retired relation id, and never invent one.
Provenance and column binding
- Address parquet columns through the generated constants for your language. A spec column insertion must be a compile or import error, not a silent row shift. If your parquet library has no schema object, add an arity assertion yourself.
- Stamp
schema_versionas the spec’s semver string from the generated constant, pin the spec by commit, and write realproduced_byandproducer_versionvalues. Leave the SDK columns null when you use none. - Treat a dropped row as a failed publish. Count rows written, not rows attempted.
The spec is public at
speckle-bundle-spec. Build against the
generated constants under
generated/ for your language and pin to a commit. The vendored copies
in specklepy, the Archicad connector, and the SketchUp connector show what that looks like, and
every bundle carries its own rel_types and node_kinds catalogs.Run the spec’s validator on every bundle before you upload, from a checkout of the repository:It checks file presence, live ids, column sets, and referential integrity. The server checks only
ETags. Close the loop in CI as well: publish to a test project, wait for
success, receive with
an SDK, and check for unknown relations and decode errors.Suggested build order
1
Authenticate and check capability
Use a personal access token or your own OAuth flow. Check the ingestion permission on the model,
then open an ingestion and confirm the reserved version id is present. The outcome is a server
you know accepts bundles.
2
Write a one-object bundle and validate it
One object, one mesh, one container, a default scene view, and a correct
meta row. Run the
spec validator on the directory. The outcome is a bundle that passes before any upload.3
Publish it end to end
Sign, upload, complete, then wait for
success. Open the version in the web app. The outcome is
a version you can see, with the publishing client attributed correctly.4
Map identity and properties
Stable
applicationId per placement, root scalars, type-level parameters, a policy for arrays.
The outcome is a property store that filters and aggregates the way your users expect.5
Add geometry and appearance
SGEO encoding for each primitive you support, solids beside meshes, materials on geometry,
by-layer resolution. The outcome is a model that renders with the right appearance.
6
Add instancing and topology
Definitions, placements, member suppression, then hosting, containment, and connectivity in a
second pass. The outcome is a model Speckle Intelligence can answer questions about.
7
Harden the publish
Throttled progress, cooperative cancellation inside the upload, failure reporting on every exit,
geometry sharding, and retries with backoff on transient errors. The outcome is a producer that
survives large models and unreliable networks.
FAQ
Can I keep publishing the old object graph?
Can I keep publishing the old object graph?
For now, yes. Upgraded servers accept it with deprecation warnings and convert it to a bundle
before the version appears, because thousands of existing integrations depend on that path. It
is not the path to build new work on: the server stores the graph and then converts it, and that
support will end. No removal date is set, and one will be announced before it is. A deployment
set up as 2026.9-only already rejects it with
LEGACY_SEND_UNSUPPORTED. Treat that as the wrong
rail, not a transient error. See Publish through model
ingestions.