Proxy WebSocket signaling for a white-labeled integration while keeping WebRTC direct
Your platform runs a WebSocket proxy that sits between the end user and Decart for the control plane (signaling, prompts, session events). Media (video and audio) flows directly between the end user and Decart over WebRTC — your proxy never touches it.This gives you a white-labeled experience with full media quality and visibility into all control messages.
Your platform already runs WebSocket infrastructure
You want full visibility into the control plane (prompts, images, session events)
You want white-labeled endpoints — end users never see Decart
If your platform is HTTP-native and you’d prefer not to run stateful WebSocket connections, see HTTP Signaling — same quality, same white-label, but with stateless HTTP proxy instead.
Pump JSON messages between the client and Decart. Every message has a type field. You can log, validate, or modify messages as they pass through — see the message reference for the full protocol.
Copy
Ask AI
import asyncioimport jsonasync def client_to_upstream(client_ws, upstream_ws): async for raw_msg in client_ws: msg = json.loads(raw_msg) if msg["type"] == "prompt": log_prompt(msg["prompt"]) # Optional: validate or modify before forwarding elif msg["type"] == "set_image": log_image_upload() # Optional: validate image size / content await upstream_ws.send(raw_msg)async def upstream_to_client(upstream_ws, client_ws): async for raw_msg in upstream_ws: msg = json.loads(raw_msg) if msg["type"] == "session_id": log_session(msg["session_id"]) elif msg["type"] == "prompt_ack" and not msg["success"]: log_moderation_rejection(msg["error"]) elif msg["type"] == "generation_ended": log_session_end(msg["seconds"], msg["reason"]) await client_ws.send(raw_msg)await asyncio.gather( client_to_upstream(client_ws, upstream), upstream_to_client(upstream, client_ws),)
4
WebRTC connects directly
When the end user’s browser receives the SDP answer and ICE candidates through your proxy, it establishes a direct WebRTC connection to Decart via the IP:port in the ICE candidates. Your proxy is not involved in media.
5
Handle disconnects gracefully
When either side disconnects, close the other connection. Your proxy should handle both directions:
Forward as-is. The candidate format matches RTCIceCandidate.toJSON() — usernameFragment is optional and may be present depending on the browser. Signal end-of-candidates by sending null:
When present, turn_config provides fresh TURN credentials. Forward to the client — they should reconfigure their peer connection with the new TURN server and create a new SDP offer.
ICE restarts are server-initiated. If the restart fails, the session ends with a generation_ended message (reason: "error").
Prompts and images are moderated server-side before being applied to the model. Your proxy sees the result as acknowledgment messages:
Prompt accepted → prompt_ack with success: true
Prompt rejected → prompt_ack with success: false and an error string
Image accepted → set_image_ack with success: true
Image rejected → set_image_ack with success: false and an error string
Rejected content is not applied — the model continues with the previous prompt or image. Your proxy can add its own moderation layer before forwarding messages to Decart.
A complete WebSocket proxy using Python and websockets. This example authenticates the client, connects upstream, pumps messages with logging, and handles disconnects:
Copy
Ask AI
import asyncioimport jsonimport websocketsDECART_API_KEY = "your-api-key" # Load from environment in productionasync def handle_client(client_ws): # 1. Authenticate the client and extract model from query params model = parse_model_from_query(client_ws.request) user = authenticate_user(client_ws.request) if not user: await client_ws.close(4001, "Unauthorized") return # 2. Open upstream connection to Decart upstream_url = f"wss://api3.decart.ai/v1/stream?api_key={DECART_API_KEY}&model={model}" async with websockets.connect(upstream_url) as upstream_ws: session_id = None async def client_to_upstream(): async for raw_msg in client_ws: msg = json.loads(raw_msg) if msg["type"] == "prompt": print(f"[{session_id}] prompt: {msg['prompt'][:80]}") elif msg["type"] == "set_image": print(f"[{session_id}] set_image (has_prompt={bool(msg.get('prompt'))})") await upstream_ws.send(raw_msg) async def upstream_to_client(): nonlocal session_id async for raw_msg in upstream_ws: msg = json.loads(raw_msg) if msg["type"] == "session_id": session_id = msg["session_id"] print(f"Session started: {session_id}") elif msg["type"] == "prompt_ack": if not msg["success"]: print(f"[{session_id}] prompt rejected: {msg['error']}") elif msg["type"] == "set_image_ack": if not msg["success"]: print(f"[{session_id}] image rejected: {msg['error']}") elif msg["type"] == "generation_started": print(f"[{session_id}] generation started") elif msg["type"] == "generation_ended": print(f"[{session_id}] ended: {msg['reason']} ({msg['seconds']}s)") record_billing(user, session_id, msg["seconds"]) elif msg["type"] == "error": print(f"[{session_id}] error: {msg['error']}") await client_ws.send(raw_msg) # 3. Pump messages in both directions try: await asyncio.gather( client_to_upstream(), upstream_to_client(), ) except websockets.ConnectionClosed: pass print(f"[{session_id}] connection closed")async def main(): async with websockets.serve(handle_client, "0.0.0.0", 8080): await asyncio.Future() # Run foreverasyncio.run(main())
This proxy is stateless — it holds no session state between connections. Each WebSocket connection is an independent pass-through. You can scale horizontally by running multiple proxy instances behind a load balancer.
Message types and fields match the message reference above. In production, add reconnection logic for WebSocket drops and monitor pc.connectionState for WebRTC failures.