Fantago Developer Guide — Custom Handlers and REST API
Extend Fantago with custom device plugins or integrate it with any external system via REST API.
Overview
Fantago is built to grow — anyone can add support for new devices or integrate it with external systems.
Beyond the 50+ built-in device handlers, Fantago exposes a full plugin API that lets developers integrate any hardware or software — no matter how niche or proprietary. Plugins are standard files placed in the userData/handlers/ directory: Fantago detects and loads them automatically at startup, making them available everywhere in the Dashboard, Panel Builder, and Stream Deck Builder just like native handlers.
There are two development paths, each suited to different complexity levels:
userData/handlers/.
If you build a handler for a device not yet covered by Fantago, consider sharing it. Community-contributed handlers expand what every user can do with the platform — and your work could save dozens of hours for someone in the next studio.
To submit a handler, send us the .js file (or export of your JSON dynamic handler) via the contact page. We will review it for quality and security, and if it meets the standards it will be published on the Fantago website and made available for download to all users in a future release.
Terms for submitted plugins
- You keep your copyright. Ownership of your code stays with you.
- Open source (MIT). Accepted plugins are published under the MIT License so that any user can download, use, and build on them freely.
- Full attribution. Your name is embedded in the plugin's
authormetadata field — visible in the Fantago UI to every user who selects your handler — and credited in the public listing on this website, for the entire duration of publication. - Review and discretion. Submission does not guarantee publication. We reserve the right to decline or later remove any plugin at our discretion.
- No compensation. Contributions are voluntary. No payment or royalty is due for accepted submissions.
- Author warranty. By submitting, you confirm that you are the original author, that the code does not infringe third-party rights, and that it contains no malicious code.
The complete terms are in EULA §5 — Community Plugin Contributions. By submitting a plugin you accept those terms.
author field in your plugin's metadata is shown in the Fantago UI whenever a user selects your handler. Your contribution is always credited by name.Beyond device plugins, Fantago exposes a REST API and extended TCP/UDP listener commands that allow any external system to interact with the server — read and write variables, trigger sequences and flows, or send commands to connected devices.
This is the integration layer for tools like Home Assistant, Node-RED, custom Python scripts, touch-screen controllers, or any hardware that can make an HTTP request or open a TCP connection.
Dynamic Handlers (JSON)
Configuration-driven handlers — no code required.
A Dynamic Handler is a structured JSON definition that describes how Fantago should communicate with a device: what commands to send, how parameters are composed into strings, and how to parse responses back into variables. The Dynamic Handler Engine interprets this definition at runtime — no JavaScript compilation, no server restart after edits.
Dynamic handlers are created and edited directly inside Fantago's Handler Builder (Dashboard → Handlers section). The Builder provides a visual interface to define actions, data sources, and response parsers without writing a single line of code.
Each action in a dynamic handler maps to a command string that is sent to the device. The string can contain placeholder tokens in double-curly-brace syntax that are replaced with the values the user configures in the Panel Builder or Stream Deck Builder:
Placeholders can reference action parameters (configured per-button), system variables (live values from the variable store), or device config fields defined in extraConfigFields.
Dynamic handlers can parse device responses using regular expressions and write the extracted values into system variables. Those variables then update Panel buttons and Stream Deck keys in real time via the standard variable injection mechanism ({{variable_name}} in button labels).
For each action you can define a response parser: a regex with named capture groups that map directly to variable names. Example: a router that replies SRC 03 DST 07 can be parsed with SRC\s+(\d+)\s+DST\s+(\d+) to populate router_source and router_destination automatically.
Dynamic handlers support data sources — named lists (e.g. inputs, outputs, presets) that can be populated at runtime by querying the device and used to fill dropdown menus in the Panel Builder. When a user configures a button that uses a "Set Input" action, they see a dropdown with the actual input labels fetched from the device, not a generic text field.
Data sources can be of type fixed (defined statically in the handler definition) or dynamic (populated by a sync action that queries the device). The Sync button shown on the device card in the Dashboard triggers the refresh.
- The device uses a simple text-based protocol
- Commands are predictable strings with parameters
- Responses follow a consistent, parseable pattern
- No complex state machine or multi-step handshake is needed
- The protocol is binary or highly stateful
- You need async feedback on a persistent TCP connection
- Multiple instances need shared state
- Complex logic (retries, timeouts, computed values) is required
JavaScript Plugins
Full-power Node.js modules for complex device integrations.
A Fantago plugin is a single .js file placed in userData/handlers/. Fantago scans this directory at startup and loads every .js file it finds. User-created plugins in userData/handlers/ take precedence over built-in handlers with the same id, making it easy to override and improve existing handlers.
Below is the full annotated structure of a plugin. Only the fields marked REQUIRED are mandatory — all others unlock additional features. See the handlerInfo Reference panel for a complete description of every field.
When a user presses a button, Fantago calls execute(device, action, ...). The action object contains everything configured on the button:
When a device sends unsolicited data on a persistent TCP connection (state changes, tally updates, status pushes), Fantago calls processAsynchronousResponse automatically for every incoming message.
connectionConfig.persistent: true in handlerInfo).getFeedbackSuggestion
When a user creates a button in the Panel or Stream Deck Builder, Fantago calls this method to suggest an automatic feedback rule — pre-filling the feedback configuration so the button highlights when the device state matches.
Available feedback styles
| Style class | Visual effect |
|---|---|
feedback-highlight | Blue border |
feedback-success | Green border |
feedback-warning | Yellow border |
feedback-danger | Red border |
feedback-overlay-blue | Blue color overlay |
feedback-overlay-green | Green color overlay |
feedback-overlay-yellow | Yellow color overlay |
feedback-overlay-red | Red color overlay |
feedback-pulse-blue | Pulsing blue border |
feedback-pulse-green | Pulsing green border |
For data used frequently at runtime (label caches, connection flags, counters), use a module-level Map keyed on device.name. This avoids hitting the database on every call and is the recommended pattern for all non-trivial handlers.
Persistent state across server restarts
For data that must survive restarts (downloaded labels, router crosspoint maps), combine the in-memory Map with dbManager.setHandlerData:
Debouncing database writes
When receiving many updates in quick succession (e.g. bulk label sync), debounce writes to avoid database overhead:
handlerInfo Reference
All fields of the handlerInfo metadata object.
| Field | Type | Status | Description |
|---|---|---|---|
id | string | REQUIRED | Unique identifier in snake_case. Used as an internal key. Never change once published — it breaks existing device assignments. |
displayName | string | REQUIRED | Human-readable name shown in the Dashboard and Handler Builder. |
version | string | REQUIRED | Semantic version string (e.g. "1.2.0"). Displayed in Plugin Information. |
author | string | REQUIRED | Author name shown in Plugin Information and handler list. |
beta | boolean | optional | If true, a yellow BETA badge appears next to the handler name. Omit for stable releases. |
description | string (HTML) | optional | Setup instructions rendered as HTML in the Plugin Information panel. Supports <b>, <code>, lists. |
| Field | Type | Status | Description |
|---|---|---|---|
connectionConfig | object | optional | Pre-fills the device creation form. Sub-fields: protocol (TCP/UDP/HTTP/HTTPS/OSC), port, terminator, persistent (boolean — required for async feedback on persistent TCP). |
extraConfigFields | array | optional | Additional fields shown during device creation. Each item: { name, label, type, default }. Types: "text", "number", "password". Values accessed at runtime via device.handler_params.fieldName. |
syncConfig | object | optional | Adds a Sync button to the device row in the Dashboard. Sub-fields: buttonLabel (string), actionId (must match a key returned by getActions()). |
integrationGuide | string (HTML) | optional | HTML content rendered in the "Command Integration Guide" tab in the Handler Builder. Document how to trigger commands from external systems (HTTP, TCP, UDP). |
Each key returned by getActionOptions() defines a UI control shown in the Panel Builder and Stream Deck Builder when a user configures a button.
| Definition | UI Control | Notes |
|---|---|---|
{ label, type: 'text', default: '' } | Text input | Free text entry. |
{ label, type: 'number', default: 0 } | Number input | Numeric entry. |
{ label, choices: [{ id, name }] } | Dropdown | id is the value passed to execute(); name is shown to the user. |
{ label, choices: [...], isPrimary: true } | Dropdown (primary) | Marks this as the main parameter — used for auto-fill of button text suggestions. |
Essential Modules
Internal libraries available to every JavaScript plugin via require().
The primary tool for sending commands to devices. Handles protocol differences automatically — the same call works for TCP, UDP, HTTP, and OSC.
sendCommand returns the response body for HTTP requests. For persistent TCP connections, incoming messages arrive asynchronously via processAsynchronousResponse().Use to store device state as system variables (visible to all panels and buttons in real time) and to persist data across server restarts.
const prefix = device.name.toLowerCase().replace(/[^a-z0-9_-]/g,'') + '_';All log levels are routed to the Fantago Dashboard log viewer and to the application log file. Use structured logging for easier debugging.
Push live progress updates or notifications to connected browsers during long sync operations. You can target a specific browser session or broadcast to all connected clients.
External REST API
HTTP endpoints for integrating Fantago with any external system.
If Require API Key is enabled in Settings, include the key in every request via header or query parameter:
API Keys are created and managed in Variables & Logic → API Keys. Each key is shown only once at creation — copy it immediately.
Returns server status and current timestamp.
Returns all system variables as a key-value object.
Returns the value of a single named variable.
Sets a variable value. Triggers any Logic Flows watching that variable.
Toggles a boolean variable between true and false. Triggers flows.
Deletes a system variable permanently.
Returns the list of all configured devices.
Returns info for a single device.
Sends a command to a device. Execution is asynchronous (202 Accepted). For handler-based devices, command is the action ID defined in the handler.
Returns all sequences with their current status.
Starts a sequence. Asynchronous (202 Accepted).
Requests a stop of a running sequence.
Returns all logic flows with enabled/disabled status.
Manually triggers a flow, bypassing trigger conditions. The flow must be enabled.
Enables a logic flow.
Disables a logic flow.
TCP/UDP Commands
Plain-text commands over the existing listener ports — no HTTP stack required.
In addition to the standard DEVICE:COMMAND format, the TCP and UDP listeners support an extended set of plain-text commands for variables, sequences, and flows. This makes integration trivial for any hardware or software that can open a socket — no HTTP library needed.
Default listener ports: 3002 (TCP) and 3003 (UDP). Both are configurable in Settings → External Access & Security.
Variable commands
| Command | Description | Example |
|---|---|---|
SET varname value | Sets a system variable. Triggers flows watching it. | SET slide_index 5 |
TOGGLE varname | Toggles a boolean variable (true ↔ false). Triggers flows. | TOGGLE camera_1_pgm |
GET varname | Returns the variable value (TCP only). | GET slide_index → 5 |
Sequence commands
| Command | Description | Example |
|---|---|---|
SEQ name START | Starts the named sequence. | SEQ Morning Show START |
SEQ name STOP | Stops the named sequence. | SEQ Morning Show STOP |
Flow commands
| Command | Description | Example |
|---|---|---|
FLOW name ENABLE | Enables a logic flow. | FLOW Lights On ENABLE |
FLOW name DISABLE | Disables a logic flow. | FLOW Lights On DISABLE |
FLOW name TRIGGER | Manually triggers a flow (bypasses conditions). | FLOW Lights On TRIGGER |
The same commands work from Python, Node.js, shell scripts, touch panels, or any hardware automation system with network output capability.
Security
Configure access control for both the REST API and the TCP/UDP listeners.
All security settings are in Settings → External Access & Security.
| Setting | Description |
|---|---|
| TCP / UDP Allowed IPs | everyone = no restriction (trusted LAN). Otherwise, a comma-separated list of authorized IPs: 192.168.1.10,192.168.1.20. Connections from unauthorized IPs are rejected before any command is processed. |
| Allow variable commands (TCP/UDP) | If disabled, only the original DEVICE:COMMAND format is accepted. SET / TOGGLE / GET / SEQ / FLOW commands are ignored. |
| Require API Key (REST) | If enabled, all /api/ext requests must include a valid X-API-Key header. Manage keys in Variables & Logic → API Keys. |
| Environment | Suggested Setup |
|---|---|
| Fully trusted LAN (isolated production network) | IP Allowlist = everyone, API Key not required, variable commands enabled. |
| Mixed / semi-public LAN | IP Allowlist = specific IPs only, API Key required for REST. |
| Exposed to internet or VPN with external clients | IP Allowlist = authorized IPs only, API Key required, variable commands limited to known sources. |
Testing & Debugging
Development workflow, log reading, common errors, and a complete production example.
- Place your
.jsfile inuserData/handlers/. - Open the Handler Builder (Handlers in the top navigation).
- Click the Advanced (JS) tab, then click Reload Plugins to load your file without restarting the server.
- Your plugin should now appear in the Advanced Plugins list.
- Add a device in the Dashboard using your new handler.
- Use the Execute button on the device row to trigger a test command.
All logger.log() calls appear in the Dashboard log panel (bottom section). Filter by your plugin name using the search box. Log levels useful during development:
| Level | Use for |
|---|---|
DEBUG | Verbose trace messages — disabled in production by default |
INFO | Normal operational messages (connection events, sync completed) |
SENT / RECEIVED | Raw command traffic to/from device — essential for protocol debugging |
WARN | Unexpected but recoverable conditions |
ERROR | Exceptions and failures |
To monitor variables written by your plugin, open Logic Builder → Variable Monitor. Filter by your device name prefix to watch variables update in real time as feedback arrives from the device.
| Symptom | Likely cause |
|---|---|
| Plugin not visible in device type list | handlerInfo.id or handlerInfo.displayName missing, or the file has a syntax error. Check the server log for load errors. |
processAsynchronousResponse never called | Persistent Connection not enabled on the device, or connectionConfig.persistent not set to true. |
| Variables written but buttons not updating | Variable name mismatch between what you write in setPanelVariable and what the button feedback rule monitors. Check exact spelling in Variable Monitor. |
Cannot find module '../../lib/...' | Wrong relative path. From userData/handlers/, all Fantago lib modules are at ../../lib/module_name. |
| Button works once then stops | Unhandled exception inside execute(). Wrap your code in try/catch and log the error to identify the failure. |
| Data sources not populating dropdowns | dataSources not declared in definition, or sync action not writing to dbManager.setHandlerData(device.name, 'data_sources', ...). |
A complete walkthrough of the Evertz Quartz handler — a production-grade plugin managing a professional video matrix router via TCP, with dynamic label sync, crosspoint status tracking, real-time updates, and progress feedback. Every advanced pattern in Fantago plugin development is demonstrated here in a single file.
1 — Imports and initial setup
handlerState is a module-level Map that caches labels and internal flags per device, avoiding constant database hits during high-frequency feedback processing.
2 — Definition and UI
dataSources tells Fantago that this device provides "sources" and "destinations" lists, used to populate dropdowns in the Panel Builder and Stream Deck Builder. onLoadActions runs a route-status check every time a panel opens.
3 — Dynamic dropdowns with fallback
The "route" command builds dropdowns from the cached labels. If labels have not been synced yet, it falls back to numbered placeholders so the button remains configurable immediately after device creation.
4 — Execution logic
Each action is translated into the Quartz protocol. The X-Y routing pattern stores the selected destination in a system variable so route_to_selected can retrieve it later from any panel.
5 — Asynchronous response parsing
Incoming data from the matrix is parsed line by line using RegExp. Route changes and label updates are written to system variables, which immediately trigger button feedback updates on all connected panels.
6 — Feedback suggestion
When a "Route" button is configured in the builder, Fantago calls this to pre-fill the feedback rule. The suggestion monitors the destination's current source variable and highlights the button when the routed source matches the one configured on the button.
7 — Synchronization with progress updates
_refreshAll iterates through all sources and destinations, requesting their labels one by one. A 40 ms delay between requests prevents flooding the matrix TCP buffer. Progress is sent to the browser every 5 steps so the Dashboard can show a live progress bar.