Bridge API

Updated October 2026 · Studio developers

Describes the local bridge that connects DousenDesktop, the DCC addons and the Unreal plugin on one workstation, and how a studio tool can register with it, publish events and receive them.

The bridge is an authenticated RPyC service inside DousenDesktop. It listens on 127.0.0.1, port 8766 by default, so only programs on the same machine can reach it. Clients send string-keyed requests and receive events over a long-lived push stream. The bridge doesn’t use HTTP. The dousen.bridge.BridgeClient Python class that ships with Desktop handles both. For data on the studio server, use the DousenCore REST API.

Check the normal workflow first

Before you test your tool, check that a normal publish works on the same workstation. Artists publish from the DCC panels described in Publishing assets.

  1. Start Desktop, sign in, and select the project. Launch a supported DCC from Desktop, and start Unreal Editor with the DousenUE plugin enabled.
  2. In the Publish history tab, check that the DCC and Unreal appear under Connected apps.
  3. Publish one instance from the DCC and wait for Imported and saved in Unreal.
  4. Run your tool. After it publishes, check Publish history and the asset in Unreal.

Connect and authenticate

BridgeClient() connects to 127.0.0.1 on the port in DOUSEN_BRIDGE_PORT, or on 8766 if the variable isn’t set. You can pass an address instead, such as rpyc://127.0.0.1:8766 or a loopback host and port. The client raises an error for any other host.

Every request carries a bridge token, which is separate from the DousenCore bearer token. Each time Desktop starts, it generates a new 64-character token and saves it in a bridge.token file that only the current user can read. Desktop deletes the file when the bridge stops.

SystemToken file
Windows%APPDATA%\Dousen\DousenDesktop\bridge.token
Linux ~/.local/share/Dousen/DousenDesktop/bridge.token, or under $XDG_DATA_HOME if it is set
macOS~/Library/Application Support/Dousen/DousenDesktop/bridge.token

The client reads the token again for every request, so your tool keeps working after Desktop restarts with a new token, unless you set DOUSEN_BRIDGE_TOKEN. Don’t copy the token into scripts or documentation.

Environment variableWhat it does
DOUSEN_BRIDGE_PORTThe bridge port. The default is 8766.
DOUSEN_BRIDGE_TOKEN The bridge token. If it is set, the client uses it as is instead of reading bridge.token, so it stops working when Desktop restarts with a new token.
DOUSEN_PYTHON_PATH Set for the tools that Desktop launches. It is the folder to add to sys.path for the dousen package.
DOUSEN_RPYC_PYTHON_PATH The folder that contains your own copy of rpyc, if you don’t use the one in the Desktop package.

Python client example

This example registers a tool called studio_tool, lists the connected apps, and closes the connection. It needs the dousen package and rpyc 6.0.1. The package that ships with Desktop includes rpyc.

PYTHONfrom dousen.bridge import BridgeClient

with BridgeClient() as bridge:
    if not bridge.register("studio_tool", "Studio Tool"):
        raise RuntimeError("Bridge registration failed")
    apps = bridge.list_apps()
    print([app["app_id"] for app in apps])

HttpBridgeClient is an alias for BridgeClient that keeps old imports working. It uses RPyC too.

Client methods

MethodWhat it does
register(app_id, name='', **capabilities) Registers your app and returns True or False. Extra keyword arguments, such as command_port, are sent as request fields. A registration expires after 90 seconds, so register again within that time. subscribe and subscribe_async do this every 10 seconds.
deregister(app_id=None) Removes a registration by sending unregister. Without an argument, it uses the ID from your last successful register. It returns nothing and doesn’t raise an error if Desktop can’t be reached.
list_apps() Returns a list of dictionaries with app_id and subscription_count (always 0) for the apps in Desktop’s app list and the apps registered over the bridge. The list is empty if the bridge can’t be reached.
publish(event_type, source_app, file_path, target_app='', metadata=None, task_id='', changelist_id='', event_id=None) Publishes one event. Copies task_id and changelist_id into metadata, and sets metadata["event_id"] to the event_id argument, to the event_id already in metadata, or to a random UUID, in that order. Returns False if Desktop rejects the event or the request fails. See Limits and failure handling before you publish again.
subscribe(subscriber_app, event_type_filter='', on_event=None) Registers your app, opens the event stream, and blocks until stop() or close() is called from another thread.
subscribe_async(subscriber_app, event_type_filter='', on_event=None, reconnect=True) Runs subscribe on a background thread. The stream reconnects if it drops, whatever the value of reconnect.
wait_until_connected(timeout=5) Waits until the event stream is connected and returns True or False. A registration doesn’t mean the stream is connected.
request(operation, payload=None, timeout=None) Sends one request and returns the response body. The default timeout is 15 seconds. For any status other than 2xx, it raises BridgeError, which carries the status and the response body. See Read an error response.
stop() / close() stop() ends the subscription. close() calls deregister() and then stop(). The with statement calls close() when the block ends.

Receive events

PYTHONfrom dousen.bridge import BridgeClient

bridge = BridgeClient()

def on_event(event):
    # Queue work onto the host application's main thread.
    # Don't call Maya, Blender or Houdini scene APIs from this callback.
    print(event.event_type, event.file_path)

bridge.subscribe_async("studio_tool", "mesh.*", on_event)
if not bridge.wait_until_connected():
    bridge.close()
    raise RuntimeError("Subscription handshake failed")

# Keep bridge alive while your tool is running, and call bridge.close()
# when your tool shuts down.

An empty filter matches every event type. A filter that ends in .*, such as mesh.*, matches every event type with that prefix. Any other filter must match the event type exactly. Desktop doesn’t send an app its own events, and it delivers an event that has a target_app only to that app.

Your callback runs on a background thread. Move scene changes to the DCC’s main thread, and Qt UI changes to the Qt main thread through a queued call.

When your stream connects, BridgeClient first calls your handler with an empty task.context event, if your filter matches it. Desktop then sends the latest task.context, if it has one. The same empty event arrives if the stream drops, reconnects or overflows. Each time, discard the task context you cached, and wait for a new one before you do work that depends on the task.

Event fields

FieldMeaning
event_id Identifies the event. Desktop uses it to drop duplicates, and to match an import result to a publish.
event_type What happened. See Event types.
source_app / target_app The app that sent the event, and an optional single recipient. If target_app is empty, every subscriber can receive the event. Otherwise it must be an app ID of 1 to 64 letters, digits, _ or -. Desktop accepts any other value in a publish request, but never delivers the event. If source_app is empty, Desktop sets it to rpyc.
file_pathThe file the event is about, if there is one.
metadata The event-specific fields, as a dictionary. On the wire this is a JSON string named metadata_json.
task_id / changelist_id The task and changelist the event belongs to, as top-level fields or as keys in metadata. The publish request accepts only metadata_json, so publishers put them in metadata.
timestampThe time of the event, in Unix seconds.

Event types

The relay accepts any event_type. The events below are the ones that DousenDesktop, the DCC addons and the Unreal plugin send and act on. Validation rule IDs such as mesh.poly_count look similar, but they belong to asset validation and are never bridge events.

Export events from DCCs

A DCC addon sends one of these events for each instance you publish, and the Unreal plugin reads it to import the asset. The Desktop install defines these fields in python/dousen/addons/_ue_contract.py.

Event Required metadata Optional metadata What it publishes
mesh.export suggested_name, source_file suggested_ue_path, convention_tags, cv_tags, references, material_assignment A static or skeletal mesh FBX. A skeletal mesh also produces its skeleton.
texture.export suggested_name (the target mesh), textures (a list of absolute paths) suggested_ue_path, master_material, material_assignment, import_mode, mesh_slot A set of textures.
animation.export suggested_name, source_file suggested_ue_path, skeleton, convention_tags, cv_tags, references An animation clip FBX.

anim.export is an older name for animation.export. Desktop and the plugin still accept it, but new tools should use animation.export. The metadata fields work as follows:

FieldHow it is used
source_file and the event’s file_path Set the event’s file_path to the exported file. Unreal reads source_file from the metadata, or file_path if source_file is missing.
suggested_name If it is missing, Unreal uses the base name of the exported file.
convention_tags, cv_tags Objects with string values. Unreal reads the asset type from convention_tags.asset_type and stamps the tags on the imported asset as metadata. Convention tags are stored as Dousen.Convention.<key>, and CV tags as dousen_<key>, for example dousen_cv_poly_tier. The cv_tags are searchable in the Content Browser. Desktop adds cv_tags to a mesh or animation export itself when the metadata has geometry_stats. See Auto-tagging on export.
references The files the scene references. Desktop reports them to DousenCore as asset dependencies. For an animation, Unreal also uses them to find the skeleton.
skeleton For animation.export. If it isn’t set, Unreal follows references from the rig scene to the skeletal mesh imported from it, and from there to its skeleton. If that finds nothing, Unreal uses the *_Skeleton asset in the destination folder.
import_mode, mesh_slot For texture.export. With mesh_materials, the default, Unreal binds the textures into the material slots of the target mesh, and mesh_slot names the slot to bind. With textures, Unreal imports them as standalone texture assets.
jira_key, source_scene_path, source_depot_path, source_repo_role, sync_manifest Optional. Where the source files live in version control. Desktop uses them to register the source asset with DousenCore, which needs an active project. See Source repositories.
published_depot_path, published_repo_role Optional. Desktop also uses them to register the source asset. Unreal stamps them on the imported asset for the Reimport (sync source) menu item.

The material_assignment object

material_assignment assigns catalog materials to mesh slots. It is an alternative to the flat textures and master_material fields, and Unreal checks it first. Each entry in assignments describes one material slot:

JSON"material_assignment": {
  "catalog_version": 1,
  "assignments": [
    {
      "mesh_slot": "CrateBody",
      "material_id": "coated_metal",
      "material_path": "/Game/Materials/M_CoatedMetal",
      "textures": {"BaseColor": "C:/Work/Textures/T_Crate_BaseColor.tga"},
      "scalar_params": {"RoughnessMult": 0.6},
      "vector_params": {"Tint": [1, 0.9, 0.9, 1]}
    }
  ]
}
FieldRules
mesh_slot The material slot or texture set name. Use an empty string for an asset with one slot.
material_id, material_path Each assignment needs one of them. Unreal skips an assignment that has neither. Unreal looks up material_id in the project’s material catalog first. If that finds no entry, it loads material_path. If that fails too, it uses the default master material.
vector_params Each vector needs at least three numbers. If you leave out the fourth, Unreal sets the alpha to 1.
allow_nanite Unreal never reads it from the event. The Nanite setting comes from Allow Nanite on the project’s catalog entry.

The catalog is created in Unreal. See Materials and textures.

Tracked and untracked exports

An export event (mesh.export, texture.export or animation.export) is a tracked publish when its metadata contains both event_id and instance_id. A tracked publish appears in Publish history and can be retried. It is marked Imported and saved in Unreal only after Desktop receives a matching import receipt from Unreal. Desktop sends a tracked publish to Unreal itself, either to an open editor or through a headless import, so other subscribers never receive it.

An export event without those two keys is untracked. Desktop sends it to subscribers and registers the source asset, but it creates no history entry, so Desktop can’t offer a retry and never reports whether an import worked. Desktop doesn’t import an untracked event in the background. An open Unreal editor that receives it reimports an existing asset, or offers an Import prompt for a new one.

To make a tracked publish, send these metadata fields:

Metadata fieldRequirement
event_id A UUID, or 32 to 128 hexadecimal characters. Use a new value for each publish attempt. Desktop rejects a repeated event_id unless the event is identical to the first one. With BridgeClient.publish(), pass it as the event_id argument or put it in metadata.
instance_id The ID of the instance you are publishing, up to 256 characters. Desktop shows it in the history entry.
bundle_id Optional, up to 128 characters. Use the same value for instances you publish together.
publish_identity An object with credential_id, server_url, account_id and tenant_id that matches the user signed in to Desktop. Desktop rejects the publish if it is missing or different. The DCC addons read these values from the credential file that Desktop passes to the processes it launches, through DOUSEN_CREDENTIAL_FILE, DOUSEN_CREDENTIAL_ID and DOUSEN_CORE_URL.
publish_history Optional. {"validated": true} records that validation ran before the publish.

Set file_path to the exported file, and leave target_app empty. Desktop records the file’s size and modification time, and doesn’t offer a retry if the file changes. The whole metadata_json value must be 16 KiB or smaller.

Desktop keeps only the metadata fields below in the history entry and in the event it sends to Unreal, and drops any others. Maps hold at most 128 entries, and lists at most 256.

GroupFields kept
Publish tracking event_id, parent_event_id, bundle_id, instance_id, publish_history, publish_identity
Unreal import suggested_name, suggested_ue_path, source_file, skeleton, asset_type, format, references
Materials and textures master_material, import_mode, mesh_slot, textures, material_assignment
Tagsconvention_tags, cv_tags
Source files jira_key, source_scene_path, source_depot_path, published_depot_path, source_repo_role, published_repo_role, sync_manifest, storage_type, storage_ref

The event has no field for the target project. Desktop sends a tracked publish to the Unreal project configured in Desktop, and records the publish as unconfirmed if none is configured. Before it sends the import, Desktop checks that the connected editor has that project open.

If your tool can’t export an instance, send a publish.failed event instead of an export event. Use the event_id, instance_id, bundle_id and publish_identity that the export would have used, and leave file_path empty. With BridgeClient.publish(), pass the event_id as the argument and the other three in metadata. Desktop lists the instance as failed in Publish history.

Events from Unreal

When you save an asset in the editor, the Unreal plugin publishes an event with source_app set to unreal. These events reuse the names of DCC export events, but their metadata is different.

EventSent when you save
mesh.exportA static mesh or skeletal mesh.
texture.exportA Texture2D.
material.exportA material or material instance.

All three events carry the same metadata:

Metadata fieldMeaning
asset_pathThe package path in Unreal.
asset_classThe asset class, for example StaticMesh.
package_fileThe path of the .uasset file.

They have no suggested_name, source_file or event_id, so they are never tracked. If you handle mesh.export or texture.export, check whether source_app is unreal to tell the two kinds apart.

dcc.open

dcc.open asks an app to open a file. Desktop and Unreal handle it in two forms.

Sent byMetadataWhat happens
The Open in DCC menu in the Unreal Content Browser. It leaves target_app empty. jira_key, asset_name, preferred_dcc Desktop handles every dcc.open event, whatever its target_app. It fetches the asset’s source record from DousenCore, finds the file on the local disk, and launches the app whose ID is preferred_dcc. If preferred_dcc is empty, it launches the DCC recorded when the asset was published. Desktop ignores the event if jira_key or asset_name is empty.
Any client, with target_app set to unreal or left empty asset_path Unreal looks up the asset in the Asset Registry and shows it in the Content Browser. If asset_path is missing, the plugin uses file_path. No built-in addon sends this form.

Houdini, Substance Painter and Substance Designer also subscribe to dcc.open. A running session opens the event’s file_path, or metadata.file_path if you set it. Blender, Maya and 3ds Max don’t handle dcc.open.

Desktop launches only the apps in its own app list (Settings → DCC Apps). A tool that only calls register still appears in the dccs response, in Connected apps and in Unreal’s Open in DCC menu, but choosing it in that menu does nothing. To make the tool a launch target, add it to the app list. See Launching DCCs.

task.context

Each time you select a different task in My tasks, Desktop publishes task.context with source_app set to dousen_desktop. Other apps use it to tag their output with the active task without asking Jira. task_id and changelist_id are both top-level event fields and metadata keys. The metadata has these fields:

FieldMeaning
task_idThe ID of the selected task.
task_titleThe task’s title.
jira_keyThe task’s Jira key, for example DOU-123.
changelist_idThe DousenCore ID of the task’s changelist.
vcs_changelist_idThe Perforce changelist number.
vcs_changelists An object with a ue entry and a source entry. Each has a changelist_id and a vcs_changelist_id. In a project without a separate source repository, both entries have the same values.
project_idThe ID of the project that is active in Desktop.

When you sign out, Desktop publishes an empty context. The DCC addons keep the latest context they receive instead of relying on environment variables set when the DCC started.

import.result

After a tracked publish is imported, Desktop gets an import receipt from Unreal. For an open editor, the receipt is the reply to the import that Desktop sends. For a background import, Desktop reads it from the headless import. Desktop uses the receipt to update the history entry. It accepts the same receipt as an import.result event, and doesn’t forward that event to subscribers. Your tool doesn’t need to send it.

Metadata fieldMeaning
event_id, bundle_id, instance_id Copied from the publish. Desktop rejects the event if event_id is missing or no history entry matches.
dispatch_generation A number that Desktop adds to each delivery and each retry. Desktop rejects the event if it is missing or doesn’t match the latest delivery.
ok, saved, asset The publish becomes Imported and saved in Unreal only if ok and saved are true and asset holds the path of the imported asset.
may_have_imported, retry_safe, error If the import isn’t confirmed and may_have_imported is true, Desktop records the publish as unconfirmed. A missing may_have_imported counts as true. Otherwise it records a failure, and retry_safe says whether a retry is allowed.

Request operations

Send a request with BridgeClient.request(operation, payload). The bridge accepts the operations in the table below. For registration and publishing, use the client methods. The events operation returns a short diagnostic history. Live events arrive on the stream.

Request payload reference

OperationPayload fieldsResponse
register id, and optionally name, command_port and capabilities. {"ok": true}. Status 400 unless the ID is 1 to 64 letters, digits, underscores or hyphens, the name is up to 256 characters, and the command port is from 0 to 65535. Status 429 when 64 apps are already registered. An expired registration still counts toward the 64 until Desktop next answers dccs. exclusive_publish_dispatch is the only capability Desktop reads, and only the Unreal plugin sends it.
unregister id. {"ok": true}, whether or not the app was registered.
dccs An empty object. A JSON array of {id, name} objects for the apps in Desktop’s app list and the apps registered over the bridge. A registered app has a command_port if it sent one that isn’t 0.
status An empty object. {"apps": [{"app_id", "subscription_count"}]}, a diagnostic list of the apps that published an event in the last 30 seconds, including apps that publish over the bridge and dousen_desktop. An app stays listed for 30 seconds after its last publish. To find the registered apps, use dccs or list_apps(). subscription_count is always 0.
publish event_type, and optionally source_app, target_app, file_path and metadata_json. All of them must be strings. {"ok": true}, or status 409 with {"ok": false, "error_code": "publish_rejected"}. There are no task_id or changelist_id fields, so put them in metadata_json.
events Optional since, in Unix seconds. {"events": [...], "timestamp": ...} with up to the last 200 events newer than since, except events that Desktop dispatches itself.
history Optional limit. The default is 20, and values outside 1 to 50 are clamped to that range. {"version": 1, "entries": [...], "capabilities": {"headless_auto_import": ...}}. The entries are the publishes of the user signed in to Desktop. Each has event_id, bundle_id, instance_id, event_type, state, error_code, hint, validated_at, exported_at, delivered_at, imported_at, dispatch_generation and retry_allowed. The state is validated, exported, delivered, imported, failed or unconfirmed.
retry event_id of the original publish, in the same format as for a publish. Status 202 with {"ok": true, "error_code": "started", "dispatch_generation": ...} when the retry starts. Otherwise status 404 (not_found), 409 or 503 (persistence_failed), with error_code saying why. For 409 it is one of session_invalid, already_imported, reconciliation_required, not_retryable, already_in_flight, source_missing, source_changed and target_project_mismatch.
source/ensure-mapped path, a depot path or a local path. Maps the folder for the path’s repository in the Perforce workspace, so you can add a new file there. Returns ok, role, depot_path and, if known, local_path.
source/sync depot_path, and optionally role. Without role, Desktop works it out from the path. Syncs the file and its .dousen_meta.json sidecar, if the depot has one. Returns ok, local_path and synced. Fails if the local file has unsubmitted edits.

Besides the statuses in the table above, the bridge returns these:

StatusMeaning
400Malformed JSON or an invalid field.
404Unknown operation.
503Desktop can’t handle the request yet, or didn’t answer within 15 seconds.

Once Desktop accepts a source/* request, it reports a Perforce or routing failure as status 200 with "ok": false and an error message. A missing path or depot_path returns 400. A request that takes longer than 15 seconds returns 503, but the sync may still finish. Use these operations with the source-repository workflow.

Read an error response

BridgeClient.request returns the response body for any 2xx status. A successful retry returns status 202, so you can read error_code and dispatch_generation from the return value. For any other status it raises dousen.bridge.BridgeError, a subclass of OSError. Its status and body attributes hold the status and the response body, so an existing except OSError handler still catches it.

PYTHONfrom dousen.bridge import BridgeClient, BridgeError

with BridgeClient() as bridge:
    try:
        reply = bridge.request("retry", {"event_id": event_id})
        print(reply.get("error_code"))
    except BridgeError as error:
        print(error.status, error.body)

Limits and failure handling

A True result from publish means that Desktop accepted the event. It doesn’t mean that Unreal imported the asset. After a timeout or a False result, Desktop may still have accepted the event, so don’t publish again automatically. Check Publish history and the destination in Unreal first. See Recover from a failed publish.

If Desktop restarts, it generates a new bridge token and closes the stream. Your subscription reconnects with the new token. Discard any task context you cached.

Source files

The Desktop install includes the Python client, python/dousen/bridge.py, and its RPyC transport, python/dousen/rpc.py. To start a tool with a DCC, see Loading studio tools.