Docs / Open API & Integrations / Worked Example (Node.js)

Worked Example (Node.js)

A complete, runnable integration: list actions, stream live mixer state, and mute a channel by name. This is the same shape a Stream Deck plugin or Bitfocus Companion module would use — both run as ordinary local Node.js processes, which is exactly what this example is.

Setup

mkdir aura-open-api-demo && cd aura-open-api-demo
npm init -y
npm install @grpc/grpc-js @grpc/proto-loader
mkdir -p proto/google/protobuf

Copy mixer.proto and actions.proto (see Overview & Setup) into proto/. They both import "google/protobuf/empty.proto" — most protobuf toolchains resolve this automatically, but @grpc/proto-loader doesn't bundle it, so create proto/google/protobuf/empty.proto yourself:

syntax = "proto3";
package google.protobuf;
message Empty {}

Your proto/ folder should now have mixer.proto, actions.proto, and google/protobuf/empty.proto.

index.js

const path = require('path');
const grpc = require('@grpc/grpc-js');
const protoLoader = require('@grpc/proto-loader');

const PROTO_DIR = path.join(__dirname, 'proto');

const packageDefinition = protoLoader.loadSync(
  path.join(PROTO_DIR, 'actions.proto'),
  {
    keepCase: true,   // keep snake_case field names (action_id, target_id, ...) as declared
    longs: String,
    enums: String,
    defaults: true,
    oneofs: true,
    includeDirs: [PROTO_DIR],
  }
);
const proto = grpc.loadPackageDefinition(packageDefinition).auramixer.actions;

const PORT = process.argv[2] || '51477';
const TOKEN = process.argv[3];
if (!TOKEN) {
  console.error('usage: node index.js <port> <token>');
  process.exit(1);
}

// grpc.credentials.createInsecure() talks plain HTTP/2 (h2c) by default in @grpc/grpc-js —
// no extra flag needed here (the .NET client in this repo's own test tooling needs an explicit
// AppContext switch for the same thing; Node doesn't).
const client = new proto.ActionsService(`127.0.0.1:${PORT}`, grpc.credentials.createInsecure());

function authMetadata() {
  const md = new grpc.Metadata();
  md.set('authorization', `Bearer ${TOKEN}`);
  return md;
}

// 1. List every action once at startup — build your picker from this, don't hardcode it.
client.ListActions({}, authMetadata(), (err, response) => {
  if (err) return console.error('ListActions failed:', err.message);
  console.log(`${response.actions.length} actions available:`);
  for (const a of response.actions) {
    console.log(`  ${a.id.padEnd(28)} ${a.display_name}`);
  }
});

// 2. Stream live mixer state — this is how you resolve a name to a target_id, and how you'd
//    keep a button icon's mute/solo/fader indicator current.
let channels = [];
const stateStream = client.StreamState({}, authMetadata());
stateStream.on('data', (snapshot) => {
  channels = snapshot.entities;
});
stateStream.on('error', (err) => console.error('StreamState error:', err.message));

// 3. Invoke an action once we've resolved a real target_id from the state stream.
function muteByName(name) {
  const target = channels.find((e) => e.name === name);
  if (!target) {
    console.error(`"${name}" not found — is StreamState connected yet, and does that name exist?`);
    return;
  }

  client.InvokeAction(
    { action_id: 'mixer.setMuted', target_id: target.id },
    authMetadata(),
    (err, response) => {
      if (err) return console.error('InvokeAction transport error:', err.message);
      // InvokeAction's real success signal is response.ok, not the absence of a gRPC error —
      // see the Actions API reference.
      if (!response.ok) return console.error('InvokeAction failed:', response.error);
      console.log(`Muted "${name}".`);
    }
  );
}

// Give the first StreamState snapshot a moment to arrive, then fire once as a demo.
// Replace "Mic Ext" with a channel name that actually exists in your project.
setTimeout(() => muteByName('Mic Ext'), 1000);

Running it

  1. In Aura Mixer: Settings → Integrations, enable Open API, copy the port and token.
  2. node index.js <port> <token>

You should see the action catalog printed, and — if a channel named Mic Ext exists in the project — it mutes, visible live in the app.

Where to go from here

  • Swap the hardcoded muteByName('Mic Ext') for whatever your integration actually triggers on (a Stream Deck button press, a Companion action, a hotkey from your own tool).
  • Use the StreamState data to drive a button icon's visual state (mute/solo indicator) instead of just logging it.
  • If the Actions API doesn't cover what you need, the same connection/auth pattern here works identically against any service in the Mixer API — just load mixer.proto instead of (or alongside) actions.proto, and construct the relevant *ServiceClient (e.g. proto.auramixer.ChannelsService) the same way.
stuck? ask on Discord
An unhandled error has occurred. Reload 🗙

Rejoining the server...

Rejoin failed... trying again in seconds.

Failed to rejoin.
Please retry or reload the page.

The session has been paused by the server.

Failed to resume the session.
Please retry or reload the page.