success. This page assumes you know what a bundle and a bundle builder are
from Build integrations on the bundle format.
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; this path has no fallback.
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, adding only the headers the sign response asked for, 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 any optional
producer-computed summary. 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 arrives as CREATED on the
projectVersionsUpdated subscription.- GraphQL
- REST
- Python
- .NET
Variables for IngestionCreate
summary is exposed as Version.summary. It may be at most 16 KiB and must be a map
of namespaces to flat string, number, or boolean values. Every namespace and field key must begin
with an ASCII letter and contain only letters, numbers, or underscores. Omit the summary when the
producer has no summary facts; model and object properties still belong in the bundle.
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.
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, and colors withHAS_COLOR. Both start from geometry only. - Use
OBJECT_HAS_MATERIALandOBJECT_HAS_COLORfor the object plane. It fills only where geometry has none, and it is the only option for a placement. - 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 an exact spec version, 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 package for your language, pinned to an exact version; the repository README
lists one for TypeScript, Python, .NET, and C++. 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.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?
Not for new work. The object-graph publish calls are deprecated: servers that still accept them
store the graph and convert it to a bundle before the version appears, and that support will
end. No removal date is set, and one will be announced before it is. A server without
legacy-send support rejects the call with
LEGACY_SEND_UNSUPPORTED. Treat that as the wrong
publish path, not a transient error. See Publish through model
ingestions.