Quick start — Voke ESM
End-to-end integration walkthrough against a live Voke org. Estimated time: 15 minutes.
This guide walks you from zero to live telemetry against a Voke org. By the end you will have AMQP queues confirmed, a command published, and a telemetry consumer running — all using credentials minted from the Connect partner wizard at /orgs/<orgId>/settings/connections.
Prerequisites
- A running Voke instance (local:
bun run docker:up && bun run dev:api, or a remote deployment). - An org admin on the target Voke organisation who can mint partner keys for you.
- Node 18 or later for running the TypeScript examples.
Step 1: Get partner credentials from your org admin
Production partners do not enable AMQP themselves. Ask your org admin to mint a key for your integration via the Connect partner wizard:
- Sign in to
https://voke.lovinka.comand pick the right organisation in the org switcher. - Open Organization settings → Connections (
/orgs/<orgId>/settings/connections?tab=api-keys). - Click Connect partner and walk through the three-step wizard. Each step has its own URL so you can deep-link or refresh without losing progress; state is held in
sessionStorageand cleared automatically on successful Generate or Cancel:- Use case —
/orgs/<orgId>/settings/connections/connect-partner— pick the preset that matches your integration (Trading platform partner, Telemetry consumer (read-only), HTTP read-only, Internal tool, or Custom). - Permissions —
.../connect-partner/permissions— review the preselectedApiKeyScopeset; tighten it if needed. - Network —
.../connect-partner/network— optionally pin the key to a CIDR allowlist, fill in the human-readable name + partner ID, and click Generate. Voke runs a server-side broker self-test before redirecting to the API key detail page where the one-time reveal banner appears.
- Use case —
- Hand the partner the bundle from the reveal banner: the AMQPS URI, the raw API key, and (when
vcp:write:*scope is present) the HMAC signing key. Click I've saved these to dismiss; refreshing the detail page after that will not re-show the secrets. Code samples on the Quickstart tab of the same page are pre-filled with the org slug — copy them as a starting point.
The bundle is shown exactly once. Save the AMQPS URI, raw key, and signing key into a secret manager before clicking I've saved these on the reveal screen. There is no recovery — if any value is lost, rotate the key (mint a new one, revoke the old one). See API keys & auth for the rotation pattern.
The Quickstart tab on /orgs/<orgId>/settings/connections?tab=quickstart is the source of
truth for ready-to-paste Node, Python, and curl snippets — they are server-assembled with the
org's real slug and a <your-key> placeholder. The samples below mirror that output with extra
inline commentary. See Organization settings routes
for the complete admin route map.
Step 2: Set up your integrator project
Create a fresh Node project and install the AMQP client library:
mkdir voke-esm-quickstart && cd voke-esm-quickstart
bun init
bun add amqplib
bun add -d @types/amqplib tsx typescriptStep 3: Add the connection helper
Create amqp-connect.ts. The simplest path is to pass the AMQPS URI straight from the reveal screen — it already encodes the org slug as the AMQP username, the raw key as the password, and the per-key vhost (partner-{keyId}) as the path:
// amqp-connect.ts
// Connect to Voke's RabbitMQ using the AMQPS URI from the Connect partner wizard.
// The URI shape is:
// amqps://{org-slug}:{url-encoded-key}@mqtt.voke.lovinka.com:5671/partner-{keyId}
import * as amqp from 'amqplib';
export interface VokeAmqpCreds {
/**
* The full AMQPS URI from the Connections reveal screen.
* Contains org slug (username), raw API key (password), host, port, and
* per-key vhost (`partner-{keyId}`) — no manual assembly required.
*/
amqpsUri: string;
/** Your organization slug — used for queue/routing-key prefixes only. */
orgSlug: string;
}
export async function connectVoke(creds: VokeAmqpCreds) {
const conn = await amqp.connect(creds.amqpsUri);
const ch = await conn.createChannel();
const prefix = `vcp.${creds.orgSlug}`;
// Queues are asserted (and bound to the `vcp` exchange) by Voke when
// trading is enabled for your org. checkQueue verifies they exist before
// any publish or consume.
const inbound = {
commands: `${prefix}.command`,
config: `${prefix}.config`,
schedule: `${prefix}.schedule`,
};
const outbound = {
telemetry: `${prefix}.event.telemetry`,
status: `${prefix}.event.status`,
alarm: `${prefix}.event.alarm`,
execution: `${prefix}.event.execution`,
};
for (const q of [...Object.values(inbound), ...Object.values(outbound)]) {
await ch.checkQueue(q);
}
return { conn, ch, exchange: 'vcp', prefix, inbound, outbound };
}Step 4: Verify the connection
Create run-connect.ts and run it to confirm that AMQP authentication works and all queues exist:
// run-connect.ts
import { connectVoke } from './amqp-connect';
(async () => {
const { conn, inbound, outbound } = await connectVoke({
// Paste the AMQPS URI from the Connections reveal screen.
amqpsUri: 'amqps://YOUR-ORG-SLUG:URL-ENCODED-KEY@mqtt.voke.lovinka.com:5671/partner-XXXX',
orgSlug: 'YOUR-ORG-SLUG',
});
console.log('inbound queues OK:', inbound);
console.log('outbound queues OK:', outbound);
await conn.close();
})();npx tsx run-connect.ts
# Expected:
# inbound queues OK: { commands: 'vcp.acme.command', config: 'vcp.acme.config', schedule: 'vcp.acme.schedule' }
# outbound queues OK: { telemetry: 'vcp.acme.event.telemetry', status: 'vcp.acme.event.status', ... }If checkQueue throws, the per-org queues are not asserted. The wizard's broker self-test should have caught this before the bundle was revealed — if you hit it in the wild, ask your org admin to use the Test connection button on the API key detail page (/orgs/<orgId>/settings/connections/api-keys/<apiKeyId>) to re-run the probe and surface the broker error.
Step 5: Add the command publisher
Create publish-command.ts. Commands are published to the vcp topic exchange with routing key {slug}.command.{commandType} — Voke's consumer picks them up off vcp.{slug}.command. Site setpoints are authenticated by the AMQP credential pair and vcp:write:setpoint scope. Higher-risk routes (command.device, command.mode, schedule.*) also require an HMAC signature; see VCP message integrity.
// publish-command.ts
// Publish a VCP command by routing it through the `vcp` exchange.
import { randomUUID } from 'node:crypto';
import { connectVoke, type VokeAmqpCreds } from './amqp-connect';
export interface VcpCommandInput {
commandType: string; // e.g. 'site-setpoint', 'mode-change'
siteId: string; // target Voke site / plant identifier
source: string; // your partner ID (e.g. 'obzor')
payload: Record<string, unknown>; // shape depends on commandType
correlationId?: string; // echoed back in event.status for correlation
}
export async function publishCommand(creds: VokeAmqpCreds, input: VcpCommandInput) {
const { ch, exchange, conn } = await connectVoke(creds);
const envelope = {
version: '1.1' as const,
messageId: randomUUID(),
correlationId: input.correlationId,
timestamp: new Date().toISOString(),
source: input.source,
siteId: input.siteId,
payload: input.payload,
};
const routingKey = `${creds.orgSlug}.command.${input.commandType}`;
ch.publish(exchange, routingKey, Buffer.from(JSON.stringify(envelope)), {
contentType: 'application/json',
persistent: true,
messageId: envelope.messageId,
correlationId: envelope.correlationId,
timestamp: Date.parse(envelope.timestamp),
});
await ch.close();
await conn.close();
}Then create run-publish.ts to send a site-setpoint command to your fake plant:
// run-publish.ts
import { publishCommand } from './publish-command';
(async () => {
await publishCommand(
{
amqpsUri: 'amqps://YOUR-ORG-SLUG:URL-ENCODED-KEY@mqtt.voke.lovinka.com:5671/partner-XXXX',
orgSlug: 'YOUR-ORG-SLUG',
},
{
commandType: 'site-setpoint',
siteId: 'YOUR-FAKE-PLANT-EXTERNAL-ID',
source: 'YOUR-PARTNER-ID',
payload: {
type: 'POWER',
targetValueKw: 10,
direction: 'EXPORT',
includeConsumption: true,
priority: 'NORMAL',
validFrom: new Date().toISOString(),
},
},
);
console.log('command published');
})();npx tsx run-publish.ts
# Expected: command publishedCheck the Voke admin under Plants → [your plant] → Commands to see the command logged with status EXECUTED or REJECTED (rejected means the fake plant is not linked to a real Voke plant yet — that is fine for this step).
Step 6: Add the telemetry consumer
Create consume-telemetry.ts. Voke publishes telemetry events to vcp.{slug}.event.telemetry using routing keys {slug}.event.telemetry.realtime and {slug}.event.telemetry.meter. Each message is a VCP envelope containing the telemetry payload.
// consume-telemetry.ts
// Subscribe to telemetry events emitted by Voke on your org's outbound
// event.telemetry queue.
import { connectVoke, type VokeAmqpCreds } from './amqp-connect';
export interface VcpEnvelope<T = unknown> {
version: '1.1';
messageId: string;
correlationId?: string;
timestamp: string;
source: string;
siteId: string;
payload: T;
}
export async function consumeTelemetry(
creds: VokeAmqpCreds,
onMessage: (envelope: VcpEnvelope) => void,
) {
const { ch, outbound } = await connectVoke(creds);
await ch.consume(outbound.telemetry, (msg) => {
if (!msg) return;
try {
const envelope = JSON.parse(msg.content.toString()) as VcpEnvelope;
onMessage(envelope);
ch.ack(msg);
} catch {
// Malformed payload — dead-letter rather than redeliver.
ch.nack(msg, false, false);
}
});
}Then create a runner that logs telemetry for 30 seconds:
// run-consume.ts
import { consumeTelemetry } from './consume-telemetry';
(async () => {
console.log('Listening for telemetry for 30 seconds...');
await consumeTelemetry(
{
amqpsUri: 'amqps://YOUR-ORG-SLUG:URL-ENCODED-KEY@mqtt.voke.lovinka.com:5671/partner-XXXX',
orgSlug: 'YOUR-ORG-SLUG',
},
(envelope) => {
console.log('telemetry siteId:', envelope.siteId);
console.log('telemetry payload:', JSON.stringify(envelope.payload, null, 2));
},
);
await new Promise((resolve) => setTimeout(resolve, 30_000));
process.exit(0);
})();npx tsx run-consume.tsYou will only receive telemetry messages once a Voke plant has externalPlantId set to match the
fake plant's externalPlantId and that plant is publishing live data over MQTT. For a
zero-partner smoke test, see the PLC quick start to simulate a
plant sending telemetry.
Next steps
- Per-org AMQP queues — full queue semantics, routing keys, durability, and dead-letter config.
- VCP data model — canonical payload shapes for commands and telemetry envelopes.
- Voke ESM sandbox — managing fake plants, time-config blocks, and the credentials-shown-once pattern.