> ## Documentation Index
> Fetch the complete documentation index at: https://docs.evox.wraithesports.app/llms.txt
> Use this file to discover all available pages before exploring further.

# Writing an integration plugin

> Write Market-style integration plugins such as OBS, Twitch and Spotify with full Node power in T1: npm libraries, external service connections, key images and knobs.

This page explains how to write integration plugins **of the kind you see on the Market** (OBS scene switcher, Twitch/Spotify control, Discord, hotkey) from scratch.

<Warning>Integrations are written with **T1 (trusted-node)** and **are not a sandbox** — the plugin can access everything your Windows user account can access. Run only code you wrote/reviewed yourself. The public T2 sandbox model is different (see <a href="/en/sdk/trust-tiers">Trust tiers</a>).</Warning>

## Core model: T1 = full Node process

The one idea that matters: **evoX runs your T1 plugin as a separate, full Node.js process.** That means:

* You can use **any npm library you want** (`npm install obs-websocket-js ws node-fetch` …).
* You can connect to external services **directly** — WebSocket, HTTPS API, local port. evoX does not sit in between.
* **The SDK only manages the host side:** which event arrived (key pressed, knob rotated), what is shown on the key (title/image/state), settings and logs.

So the division of labor is: **external service logic is your Node code + your npm libraries; the key/settings/event bridge is the SDK.**

```text theme={}
[ Dış servis: OBS / Twitch / Spotify ]
        ▲  (sizin npm kütüphaneniz: ws, api client — doğrudan)
        │
   runtime.mjs  (T1 tam Node)
        │
        ▼  (@evoxapp/plugin-sdk: event + setTitle/setImage + settings)
[ evoX host  →  cihaz tuşu ]
```

## Project setup

```bash theme={}
npx @evoxapp/plugin-cli init obs-switcher --runtime trusted-node --name "OBS Sahne"
cd obs-switcher
npm install @evoxapp/plugin-sdk obs-websocket-js
```

Declare the action and its settings in `manifest.json`. Setting types are rich — `secret`, `oauth`, `select`, `color`, `number`, `toggle`, `hotkey`, `url`, `path` are supported:

```json theme={}
{
  "actions": [
    {
      "id": "scene",
      "name": "Sahne Seç",
      "controllers": ["Keypad"],
      "settingsSchema": [
        { "key": "host", "type": "text", "label": "OBS Host", "default": "127.0.0.1" },
        { "key": "port", "type": "number", "label": "Port", "default": 4455 },
        { "key": "password", "type": "secret", "label": "OBS Parolası" },
        { "key": "sceneName", "type": "text", "label": "Sahne adı" }
      ]
    }
  ]
}
```

## Pattern 1 — WebSocket integration (OBS)

`runtime.mjs`: set up the connection on the first `willAppear`, send commands on `keyDown`, clean up on `willDisappear`.

```js theme={}
import { EvoxPluginClient } from '@evoxapp/plugin-sdk';
import { createNodeProcessIpcPluginTransport } from '@evoxapp/plugin-sdk/node';
import OBSWebSocket from 'obs-websocket-js';

const client = new EvoxPluginClient(createNodeProcessIpcPluginTransport(), { sdkVersion: '0.1.0' });
const obs = new OBSWebSocket();
let connected = false;

async function ensureConnected(settings) {
  if (connected) return;
  await obs.connect(`ws://${settings.host || '127.0.0.1'}:${settings.port || 4455}`, settings.password || undefined);
  connected = true;
}

client.on('action.willAppear', async (event) => {
  if (event.context?.actionId !== 'scene') return;
  await client.setTitle(event.context.bindingInstanceId, 'OBS');
});

client.on('action.keyDown', async (event) => {
  if (event.context?.actionId !== 'scene') return;
  const id = event.context.bindingInstanceId;
  try {
    const settings = await client.getSettings(id);
    await ensureConnected(settings);
    await obs.call('SetCurrentProgramScene', { sceneName: settings.sceneName });
    await client.showSuccess(id);
  } catch (error) {
    console.error('[obs] ' + String(error?.message || error));
    connected = false;
    await client.showError(id);
  }
});

client.on('plugin.stop', async () => { try { await obs.disconnect(); } catch {} });

await client.start();
```

The `ws://127.0.0.1` connection is made **directly** — T1 is full Node, so no SDK "network capability" is needed. (This is not the case in the public T2 sandbox; there the host-brokered `network.fetch` is used.)

## Pattern 2 — API + OAuth (Twitch/Spotify style)

There are two ways:

1. **Your own token management:** T1 is full Node, so you can read the token from a `secret` setting and use it directly with `fetch`.
2. **Host-brokered OAuth:** With SDK `beginOAuth(provider)` / `getOAuthStatus(provider)` you use evoX's credential vault; the raw token never returns to your plugin, the host injects it into the request.

```js theme={}
client.on('action.keyDown', async (event) => {
  if (event.context?.actionId !== 'nowPlaying') return;
  const id = event.context.bindingInstanceId;
  const settings = await client.getSettings(id);
  const res = await fetch('https://api.spotify.com/v1/me/player/currently-playing', {
    headers: { Authorization: `Bearer ${settings.token}` },
  });
  const data = await res.json();
  await client.setTitle(id, data?.item?.name?.slice(0, 12) || '—');
});
```

## Key image — `setImage` (dynamic images such as album art)

Besides the title, you can draw a **dynamic image** on the key. Two formats:

```js theme={}
// 1) Pakete gömülü asset (manifest-hash id ile):
await client.setImage(id, { kind: 'asset', assetId: 'scene-live' });

// 2) Çalışma anında üretilen/indirilen raster (base64):
const bytes = Buffer.from(await (await fetch(albumArtUrl)).arrayBuffer());
await client.setImage(id, { kind: 'raster', mimeType: 'image/png', dataBase64: bytes.toString('base64') });
```

For multi-state actions, change the state (starting at 0) with `setState(id, n)`.

## Knob support

Declare the action with `"controllers": ["Knob"]` and listen to `action.dialRotate` / `action.dialDown` — ideal for continuous values such as volume/brightness.

```js theme={}
let volume = 50;
client.on('action.dialRotate', async (event) => {
  if (event.context?.actionId !== 'volume') return;
  // dialRotate payload'ı `delta` (işaretli adım) ve/veya `direction` ('LEFT'/'RIGHT') taşır.
  const step = Number(event.payload?.delta ?? (event.payload?.direction === 'LEFT' ? -1 : 1));
  volume = Math.max(0, Math.min(100, volume + step));
  await client.setTitle(event.context.bindingInstanceId, `%${volume}`);
});
```

## Lifecycle and cleanup

* Set up the **connection** lazily on `willAppear`; close it on `plugin.stop`.
* If **polling** is needed, start it on `willAppear` and stop it on `willDisappear`; use exponential backoff on errors.
* Wrap every handler in `try/catch`; log the error with `console.error` (visible in the Developer window) and notify the user with `showError`.
* The host assigns identity: do not write to any context other than `event.context.bindingInstanceId`; ignore an `actionId` you don't recognize.

## Next step

<Card title="Test in your own evoX" href="/en/sdk/quickstart">Load the project from the Developer window, assign it to a key, reload and see the logs.</Card>

References: <a href="/en/sdk/events-reference">Events and host-RPC</a> · <a href="/en/sdk/manifest">Manifest</a> · <a href="/en/sdk/capabilities">Capability model (T2)</a>.
