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.
3.0 API version support will be dropped for 4.0.
-
3.0API- Fully documented in the
3.0.xdocumentation pages. HTTPon http://localhost:10000/3.0/.Websocketson port10000.
- Fully documented in the
-
3.xAPI- Fully documented in the
3.xdocumentation pages. HTTPon http://localhost:10001/.Websocketson port10001.- Improved functionality and faster integration with game engines.
- Fully documented in the
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 path | Replacement |
|---|---|
POST /force_scale | POST /settings/devices/force_scale |
POST /gravity_compensation | POST /{type}/{id}/config/gravity_compensation |
POST /torque_scaling | POST /{type}/{id}/config/torque_scaling |
POST /device_handedness | POST /{type}/{id}/config/handedness |
POST /serial_enable | POST /settings/system/serial_enable |
POST /experimental/features/grip_dropped_simulation_stopper | POST /settings/features/grip_hook/enabled |
POST /experimental/features/screensaver_enable | POST /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_origin → inverse3[*].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_origin | New preset |
|---|---|
device_base | arm_front (or defaults) |
workspace_center | arm_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_basis → session.configure.basis
// Old — deprecated
{ "session": { "set_basis": { "basis": { "permutation": "X-ZY" } } } }
// New — canonical
{ "session": { "configure": { "basis": { "permutation": "XZ-Y" } } } }
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.
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:
- HTTP (runtime)
- Config file (persistent)
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.
Add the key to haply-inverse-service-config.json:
{
"serialization/wireless_verse_grip/legacy_mode": false
}
Config-file locations (one of):
| Platform | Path |
|---|---|
| Windows | C:\ProgramData\Haply\Inverse\haply-inverse-service-config.json |
| macOS | /Library/Application Support/Haply/Inverse/haply-inverse-service-config.json |
| Linux | /etc/haply-inverse-service/haply-inverse-service-config.json |
Restart the service (or the Haply Hub) for the change to take effect.
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:
config.typeis the parent family, per entry —"wireless_verse_grip"for stylus rows,"custom_verse_grip"for custom rows (Ruko, Kingfisher, prototype).config.sub_typeis a new field (opt-in shape only) carrying the physical hardware subtype —"stylus","prototype","ruko", or"kingfisher". It serializes thewireless_verse_grip::subtypeenum directly, distinct fromconfig.type'sdevice_typeenum. Previously both stylus and prototype serialized as"wireless_verse_grip"; each now has its own value. Legacy mode does not emitsub_type.- Custom grips share
wireless_verse_grip[]by default when you opt in — the stylus array also contains the custom variants. The separatecustom_verse_griparray is emitted only when you flipexplicit_custom = true, in which case that array becomes custom-only (stylus never appears there). - Custom-grip payloads stay as raw extension bytes.
extended_data/raw_data(defaulttrue) keeps the rawstate.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 flagextended_data/custom_fieldscan 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.
- Legacy (3.5 default)
- Opt-in (legacy_mode = false, defaults)
{
"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]
}
}
]
}
{
"wireless_verse_grip": [
{
"device_id": "1615",
"config": { "type": "wireless_verse_grip", "sub_type": "stylus", "…": "…" },
"state": {
"buttons": { "a": false, "b": false, "c": false },
"hall": 16,
"orientation": { "x": 0, "y": 0, "z": 0, "w": 1 }
}
},
{
"device_id": "1419",
"config": { "type": "custom_verse_grip", "sub_type": "ruko", "…": "…" },
"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]
}
}
]
}
In this example all three other knobs are at their defaults
(explicit_custom = false, raw_data = true, custom_fields = false),
so customs share the wireless_verse_grip array, the separate
custom_verse_grip array is not emitted, and the ruko entry carries the
raw extension_data byte array for the client to translate locally.
Key differences when you opt in:
| Aspect | Legacy (3.5 default) | Opt-in (legacy_mode = false) |
|---|---|---|
config.type for custom rows under wireless_verse_grip | subtype name (e.g. "ruko") | parent family "custom_verse_grip" |
config.sub_type | absent | present — "stylus" / "prototype" / "ruko" / "kingfisher" |
custom_verse_grip array emitted | always (when a custom grip is connected) | only when explicit_custom = true |
| Custom-grip state schema | raw extension_data bytes + a/b/c buttons | same 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_grip | never | never |
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 = 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 = falseto have any effect.
Controls whether custom grips get their own dedicated array.
false(default) — customs share thewireless_verse_grip[]array alongside stylus entries; no separatecustom_verse_griparray is emitted.true— customs are also emitted under a dedicatedcustom_verse_grip[]array (and continue to appear underwireless_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 = falseto 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 = falseto have any effect.
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_custom | raw_data | custom_fields | wireless_verse_grip[] contains | custom_verse_grip[] contains |
|---|---|---|---|---|
false (default) | true (default) | false (default) | stylus (plain) + customs (raw bytes) | (not emitted) |
false | false | true | stylus (plain) + customs (translated) | (not emitted) |
false | true | true | stylus (plain) + customs (raw bytes + translated) | (not emitted) |
true | true | false | stylus (plain) + customs (raw bytes) | customs (raw bytes) |
true | false | true | stylus (plain) + customs (translated) | customs (translated) |
true | true | true | stylus (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
- Settings Reference — the four
serialization/wireless_verse_grip/*keys. - WebSocket Protocol — full snapshot and streaming frame layout.
- AsyncAPI Reference — machine-readable schema for all v3.1 payloads.
- HTTP API Reference — Swagger UI for the current HTTP routes and their replacements for the deprecated endpoints listed above.