Skip to main content
Version: 3.5.x

Migrating to 3.5

This page is the consolidated upgrade guide for Inverse Service 3.5. It covers every deprecation the service still carries — the legacy 3.0 wire format slated for removal in 4.0, the deprecated HTTP endpoints, and the deprecated session-channel simulation commands — alongside the one new migration introduced in 3.5: the opt-in Wireless Verse Grip output shape.

All deprecations listed here are still accepted on the wire for backward compatibility. Nothing breaks on upgrade — plan to migrate at your convenience.


3.0 vs 3.x API versions

The service exposes two concurrent JSON formats: the legacy 3.0 format on port 10000 and the current 3.x format on port 10001. Both stay available for backward compatibility — 3.0 integrations continue to run unchanged on upgrade.

warning

3.0 API version support will be dropped for 4.0.

  • 3.0 API

  • 3.x API

    • Fully documented in the 3.x documentation pages.
    • HTTP on http://localhost:10001/.
    • Websockets on port 10001.
    • Improved functionality and faster integration with game engines.

Upgrade at your convenience — no disruption to existing workflows.


Deprecated HTTP endpoints

The following endpoints are still accepted but emit a deprecation warning. They will be removed in 4.0. Use the replacement routes instead.

Deprecated pathReplacement
POST /force_scalePOST /settings/devices/force_scale
POST /gravity_compensationPOST /{type}/{id}/config/gravity_compensation
POST /torque_scalingPOST /{type}/{id}/config/torque_scaling
POST /device_handednessPOST /{type}/{id}/config/handedness
POST /serial_enablePOST /settings/system/serial_enable
POST /experimental/features/grip_dropped_simulation_stopperPOST /settings/features/grip_hook/enabled
POST /experimental/features/screensaver_enablePOST /settings/features/screensaver/enabled

Each deprecated route triggers a http-route-deprecated event on the events channel, carrying both the old route and its replacement.


Deprecated session-channel commands

The two session-level simulation commands below are deprecated and will be removed in a future major version. They are still accepted on the wire; new integrations should use the configure entries listed in the replacement column.

Each deprecated command triggers a command-deprecated event on the events channel.

session.set_coordinate_origininverse3[*].configure.preset

// Old — deprecated
{ "session": { "set_coordinate_origin": { "coordinate_origin": "workspace_center" } } }

// New — canonical
{ "inverse3": [ { "device_id": "…", "configure": { "preset": { "preset": "arm_front_centered" } } } ] }

Value mapping:

Old coordinate_originNew preset
device_basearm_front (or defaults)
workspace_centerarm_front_centered

Presets are applied per-device via inverse3[*].configure.preset (or configure.preset on verse grip / wireless verse grip device entries), not as a session-wide switch. See the Configure section in the simulation reference for the full list of preset names.

session.set_basissession.configure.basis

// Old — deprecated
{ "session": { "set_basis": { "basis": { "permutation": "X-ZY" } } } }

// New — canonical
{ "session": { "configure": { "basis": { "permutation": "XZ-Y" } } } }
Axis-sign convention changed

The axis-sign interpretation differs between the two commands. A permutation that produced the correct mapping under session.set_basis can produce an inverted transformation under session.configure.basis — you may need to negate the sign on one or more axes when migrating.

Example: a session that ran correctly with session.set_basis + "permutation": "X-ZY" will typically need "permutation": "XZ-Y" under session.configure.basis. Always re-verify the resulting transform before shipping a migration.


Wireless Verse Grip output shape (3.5, opt-in)

Service version 3.5 adds an opt-in JSON shape for Wireless Verse Grip devices (including the Ruko and Kingfisher custom variants) in both the full snapshot and streaming frames on the v3.1 simulation channel.

This migration is optional

3.5 ships with serialization/wireless_verse_grip/legacy_mode = true by default. Existing pre-3.5 clients keep working with no config change and no code change — you can upgrade the service without touching your integration.

Migrate only if you want the new shape: a cleaner config.type / config.sub_type split and explicit control over whether custom grips are duplicated across the wireless_verse_grip and custom_verse_grip arrays. To opt in, set serialization/wireless_verse_grip/legacy_mode = false and follow the rest of this section.

The change is additive on the wire — the v3.1 payload version is unchanged — and entirely controlled by four runtime settings under serialization/wireless_verse_grip/. Clients that want the pre-3.5 shape get it automatically; clients that opt into the new shape can tune three further knobs to shape the output.

Who might want to migrate

Any client parsing the wireless_verse_grip or custom_verse_grip arrays of a v3.1 payload may benefit from the new shape:

  • Unity plugin integrations that read WVG entries through JsonUtility
  • TouchDesigner integrations consuming the WebSocket stream
  • Python / C++ sample code keyed off config.type (e.g. "ruko" / "kingfisher")

Clients that only parse inverse3 or verse_grip arrays are not affected regardless of settings.

Opting in

Flip legacy_mode to false to activate the new shape. Two ways:

curl -X POST http://127.0.0.1:10001/settings/serialization/wireless_verse_grip/legacy_mode \
-H 'Content-Type: application/json' \
-d 'false'

The service re-emits a full snapshot immediately so mid-stream clients see the new shape on the next frame.

Once legacy_mode = false, the other knobs (explicit_custom, extended_data/raw_data, extended_data/custom_fields) become active. See the knob matrix below.

What the new shape looks like

When you opt in (legacy_mode = false), the payload lays out axes that were previously entangled:

  1. config.type is the parent family, per entry — "wireless_verse_grip" for stylus rows, "custom_verse_grip" for custom rows (Ruko, Kingfisher, prototype).
  2. config.sub_type is a new field (opt-in shape only) carrying the physical hardware subtype — "stylus", "prototype", "ruko", or "kingfisher". It serializes the wireless_verse_grip::subtype enum directly, distinct from config.type's device_type enum. Previously both stylus and prototype serialized as "wireless_verse_grip"; each now has its own value. Legacy mode does not emit sub_type.
  3. Custom grips share wireless_verse_grip[] by default when you opt in — the stylus array also contains the custom variants. The separate custom_verse_grip array is emitted only when you flip explicit_custom = true, in which case that array becomes custom-only (stylus never appears there).
  4. Custom-grip payloads stay as raw extension bytes. extended_data/raw_data (default true) keeps the raw state.extension_data: [...] byte array on custom-grip entries — matching the legacy shape. Clients are expected to translate those bytes into subtype-specific fields (Ruko wheel/trigger, Kingfisher buttons, …) in their own code; a dedicated advanced flag extended_data/custom_fields can emit pre-translated fields inside the service, but it is not part of the recommended migration path — see the note on that knob before enabling it.

Legacy (default) vs opt-in example

Assume one stylus (1615) and one Ruko (1419) are connected.

{
"wireless_verse_grip": [
{
"device_id": "1615",
"config": { "type": "wireless_verse_grip", "…": "…" },
"state": {
"buttons": { "a": false, "b": false, "c": false },
"hall": 16,
"orientation": { "x": 0, "y": 0, "z": 0, "w": 1 }
}
},
{
"device_id": "1419",
"config": { "type": "ruko", "…": "…" },
"state": {
"buttons": { "up": false, "down": false, "left": false, "right": false },
"trigger": 7,
"wheel": 4,
"hall": 16,
"orientation": { "x": 0, "y": 0, "z": 0, "w": 1 }
}
}
],
"custom_verse_grip": [
{
"device_id": "1419",
"config": { "type": "custom_verse_grip", "…": "…" },
"state": {
"buttons": { "a": false, "b": false, "c": false },
"hall": 16,
"orientation": { "x": 0, "y": 0, "z": 0, "w": 1 },
"extension_data": [0, 6, 1, 183, 5, 6, 7, 8, 9, 10, 11, 12]
}
}
]
}

Key differences when you opt in:

AspectLegacy (3.5 default)Opt-in (legacy_mode = false)
config.type for custom rows under wireless_verse_gripsubtype name (e.g. "ruko")parent family "custom_verse_grip"
config.sub_typeabsentpresent — "stylus" / "prototype" / "ruko" / "kingfisher"
custom_verse_grip array emittedalways (when a custom grip is connected)only when explicit_custom = true
Custom-grip state schemaraw extension_data bytes + a/b/c buttonssame raw extension_data bytes by default; raw_data can be disabled, and custom_fields is an advanced opt-in — see below
Stylus under custom_verse_gripnevernever

Adapting your parser

Once you opt in, treat config.sub_type as the hardware identity and config.type as the family bucket. Clients that previously keyed off config.type == "ruko" should key off config.sub_type == "ruko":

- const isRuko = entry.config.type === "ruko";
+ const isRuko = entry.config.sub_type === "ruko";

For parsers that need to support both pre-3.5 service versions (or 3.5 services still in legacy mode) and 3.5 services in opt-in mode in the same binary, check either field:

const subtype = entry.config.sub_type ?? entry.config.type;
const isRuko = subtype === "ruko";

Any client that previously relied on translated per-subtype fields inside a Ruko or Kingfisher entry (buttons.{up,down,left,right}, trigger, wheel, buttons.{a..f}, …) should now translate the raw state.extension_data[] bytes on its own side. The service keeps the legacy in-service translation behind the extended_data/custom_fields flag, but that flag is intended for very specific internal clients only and the in-service translation is planned to move out of the service entirely in a future release — build the byte decoder on the client side from the start to avoid another migration later.

Staying on legacy (no action required)

The default in 3.5 is legacy mode. If you have a frozen client you cannot update, or you just don't need the new shape yet, do nothing — upgrading to 3.5 will not change your payload.

Legacy mode is transitional

legacy_mode = true is the default in 3.5 to give internal Haply tooling (including the Hub) and first-party integrations time to migrate. It is not deprecated today, but the default is scheduled to flip back to false in a future minor release, and the setting itself is expected to be removed before the 4.0 major bump. Plan to migrate when convenient — don't wait for a hard deadline.

Full knob matrix

All four knobs live under serialization/wireless_verse_grip/ and can be toggled at runtime via the settings HTTP API.

Knob: legacy_mode

  • Type: bool
  • Default: true (3.5 ships with legacy mode on)

When true, the payload matches the pre-3.5 shape byte-for-byte and the other knobs become no-ops. When false, the new shape is active and the three knobs below take effect. See Opting in.

Knob: explicit_custom

  • Type: bool
  • Default: false
  • Requires legacy_mode = false to have any effect.

Controls whether custom grips get their own dedicated array.

  • false (default) — customs share the wireless_verse_grip[] array alongside stylus entries; no separate custom_verse_grip array is emitted.
  • true — customs are also emitted under a dedicated custom_verse_grip[] array (and continue to appear under wireless_verse_grip[] too).

The previous merged_in_wireless setting inverted this: it defaulted to true (customs duplicated into wireless_verse_grip) and had to be set to false to get a clean split. The new naming is positive — explicit_custom = true reads as "emit customs explicitly under their own array" — and the default was flipped so that out-of-the-box opt-in mode produces a single unified wireless_verse_grip[] array, cutting per-tick serialization cost.

Knob: extended_data/raw_data

  • Type: bool
  • Default: true
  • Requires legacy_mode = false to have any effect.

When true (default), custom-grip entries include the raw state.extension_data: [...] byte array — the stable shape for reflection-based deserializers like Unity's JsonUtility and for clients driving a custom binary protocol over the extension channel. When false, the byte array is omitted. Independent from custom_fields; see the matrix below for the four combinations.

No-op on the plain stylus subtype (no extension channel).

Knob: extended_data/custom_fields

  • Type: bool
  • Default: false
  • Requires legacy_mode = false to have any effect.
Advanced — not part of the recommended migration

custom_fields is kept for a handful of specific internal clients that still rely on the service to translate raw extension bytes into subtype-specific fields. In-service translation is planned to move out of the service in a future release, so new integrations should not enable this flag. Keep custom_fields = false (the default) and do the byte-to-fields translation on the client side.

When true, custom-grip entries gain the translated per-subtype schema — buttons.{up,down,left,right} + trigger + wheel for Ruko; buttons.{a..f} + trigger for Kingfisher. When false (default), only the generic buttons.{a,b,c} are present and subtype-specific state must be decoded from state.extension_data[]. Independent from raw_data.

No-op on the plain stylus subtype and on the prototype subtype — the prototype has no translated schema and always falls back to extension_data bytes (if raw_data = true) or to plain a/b/c buttons only (if raw_data = false).

Combined matrix (opt-in mode)

With legacy_mode = false:

explicit_customraw_datacustom_fieldswireless_verse_grip[] containscustom_verse_grip[] contains
false (default)true (default)false (default)stylus (plain) + customs (raw bytes)(not emitted)
falsefalsetruestylus (plain) + customs (translated)(not emitted)
falsetruetruestylus (plain) + customs (raw bytes + translated)(not emitted)
truetruefalsestylus (plain) + customs (raw bytes)customs (raw bytes)
truefalsetruestylus (plain) + customs (translated)customs (translated)
truetruetruestylus (plain) + customs (raw + translated)customs (raw + translated)

(Rows with raw_data = false and custom_fields = false are permitted but emit only the generic a/b/c buttons — typically not useful.)

Prototype subtype

The prototype custom-grip subtype is treated as a custom for routing purposes — it appears under wireless_verse_grip[], and also under custom_verse_grip[] when explicit_custom = true. Because there is no translated schema for prototype, custom_fields is a no-op on prototype entries; their state comes entirely from extension_data bytes (when raw_data = true) or not at all (when raw_data = false).

In opt-in mode, prototype entries report config.sub_type = "prototype" — a dedicated enum value distinct from "stylus". In 3.4 both subtypes serialized as "wireless_verse_grip"; 3.5 opt-in mode gives each its own value. Legacy mode does not emit sub_type at all.


Reference