What changed
Creating a version used to be synchronous: the mutation wrote the version, and you could query it, open its URL, or receive it immediately. Now every version-creating call goes through the same processing pipeline as file uploads, called a model ingestion:- Your call returns immediately with a reserved version id. The response shape is unchanged, so existing code keeps compiling and running.
- The server processes your already-uploaded objects in the background (packing, then building the new-format data bundle).
- The version is created when processing completes. Only then does it appear in queries and version lists, resolve at its URL, and fire
version_createdwebhooks and subscriptions.
SendPipeline in the .NET SDK.
This applies to app.speckle.systems and to self-hosted servers running v2026.9 or later. It is
part of the wider data model migration.
What to call instead
specklepy scripts
client.version.create keeps working and returns the same Version object, but its id is a reserved id: the version does not exist yet.
.NET scripts (Speckle.Sdk)
client.Version.Create keeps working the same way: the returned Version.id is reserved, not yet queryable.
The supported successor is the ingestion upload path the desktop connectors use: create an ingestion, run SendPipeline, and let the server create the version when processing completes. The ingestion is your progress and readiness surface, and there is no separate Version.Create call at all.
Example
client.Subscription.CreateProjectModelIngestionUpdatedSubscription for push-based updates instead of polling.
If you build the new-format data bundle yourself, an upcoming Speckle.Sdk release also exposes the v2 upload rail (uploads/complete), where the version is created as soon as your upload completes. Most integrations should use SendPipeline and let the server do the conversion.
Raw GraphQL and v1-era scripts
BothversionMutations.create and the legacy commitCreate mutation (still used by SketchUp-era tooling and v1 scripts) return the id synchronously. Treat it as reserved: do not build “view it online” links, query the version, or hand the id to another system until the ingestion completes. Use the queries and subscription in Wait for the version.
Automate functions
CreateNewVersionInProject (.NET) and create_new_version_in_project (Python) keep working unchanged:
Example
version_created are unaffected: the trigger already fires only when the version exists. Your function is only affected if it reads back a version it just created in the same run. In that case, wait for it as shown below. An ingestion-backed publish helper for the Automate SDKs is planned; until it ships, the helpers above remain the supported path.
Wait for the version
The ingestion is the readiness surface while your version is being processed. Its lifecycle:queued→processing→success,failed,cancelled,timeout, orinvalidInput- While
processing, legacy sends report the server-side phasesPACKING(raw objects into a packfile) andBUNDLING(packfile into the new-format bundle), with progress. - On
success, the status carries the version id, and the version now exists. - On any failure status, no version is ever created. Fix the problem (or just retry) and resend.
ModelIngestion.versionId(top-level field): the reserved id, present from the moment the ingestion is created. It matches the id your create call returned. Use it to find your ingestion, never to decide the version exists.ModelIngestionSuccessStatus.versionId(insidestatusData): only present once the ingestion succeeded. This is the signal that the version exists.
model.ingestionHistory instead of latestIngestion and pick the entry whose versionId matches yours.
For push-based updates instead of polling, use the projectModelIngestionUpdated subscription with a modelId reference and react when statusData becomes a success or failure status. See Real-time subscriptions for a complete example.
version_created webhooks also fire only at completion, so existing webhook consumers need no change: by the time your handler runs, the version is queryable.
No removal date
Legacy send stays accepted. The deprecation warnings mean “no longer the recommended path”, not “scheduled for removal”. Retirement will be criteria-driven and announced well in advance; no date is set. You can keep publishing through the deprecated calls as long as they are accepted, provided your code handles the deferred version visibility described above.FAQ
Why does reading a version right after creating it fail?
Why does reading a version right after creating it fail?
The id you got back is reserved, not yet a real version. Add a wait as shown in Wait for the
version.
Does operations.send still work?
Does operations.send still work?
Yes, unchanged. Sending objects and receiving them by object id works exactly as before. Only
the version-creating step became asynchronous.
How do I know a publish failed?
How do I know a publish failed?
The ingestion ends in a failure status (
failed, cancelled, timeout, or invalidInput) and
no version is created. Polling the version alone cannot distinguish “still processing” from
“failed”, so check the ingestion status when a wait runs long.What happens if I parse the success status off completeWithVersion's response?
What happens if I parse the success status off completeWithVersion's response?
It breaks: that mutation now returns the ingestion in
processing with the reserved id on the
top-level versionId field, not a success status. Read the reserved id from there, then wait
for the ingestion to succeed. The mutation was already deprecated; new uploads should complete
through the v2 upload rail.