Integrations
Use Cymo from AI coding assistants, terminal agents and Next.js apps, with your key kept on the server.
AI assistants
Connect your assistant to the Cymo MCP server and it can read rooms, run scenarios and set up webhooks while it writes your code. Each client has its own setup:
Terminal agents
Agents that run shell commands can drive the CLI instead. Have the agent start with cymatics agent guide, then discover commands with agent schema. Neither needs a key, and --dry-run shows a request without sending it.
$ cymatics agent guide # workflow, exit codes
$ cymatics agent schema --group rooms # JSON Schemas
$ cymatics rooms get --room room_bedroom --dry-runKeep the key on the server
A Cymo key can read every room in its project, so it belongs in a server environment variable, never in the browser. In Next.js, a variable prefixed NEXT_PUBLIC_ is inlined into client code, so leave the prefix off and import the SDK only from server code. The server-only package makes a client import fail the build.
# Server only: never prefix these with NEXT_PUBLIC_
CYMO_API_KEY=cym_test_…
CYMO_WEBHOOK_SECRET=whsec_…// lib/cymo.ts
import "server-only";
import { Cymo } from "@cymos/sdk";
const apiKey = process.env.CYMO_API_KEY;
if (!apiKey) throw new Error("Set CYMO_API_KEY");
export const cymo = new Cymo({ apiKey, client: "my-app/1.0.0" });Read from Server Components and Route Handlers, and send the browser only what it shows:
// app/rooms/[room]/page.tsx
import { cymo } from "@/lib/cymo";
export default async function RoomPage({ params }: PageProps<"/rooms/[room]">) {
const { room } = await params;
const { data } = await cymo.rooms.get({ room_id: room });
return <p>{data.person_count} in {data.room_id}</p>;
}Next.js webhooks
Register your route once, and keep the signing secret it returns as CYMO_WEBHOOK_SECRET:
const { data, meta } = await cymo.webhooks.create({
url: "https://example.com/hooks/cymo",
events: [
"bed.exited"
],
enabled: true
});Then verify every delivery against the raw body before trusting it, and do the work after answering:
// app/api/cymo-webhook/route.ts
import { after } from "next/server";
import {
CymoError,
verifyWebhookSignature,
type Envelope,
type Event,
} from "@cymos/sdk";
const rejected = new Set([
"invalid_webhook_signature",
"webhook_timestamp_out_of_tolerance",
]);
export async function POST(request: Request) {
// The exact bytes received: re-serialised JSON fails verification.
const payload = await request.text();
try {
await verifyWebhookSignature({
payload,
header: request.headers.get("cymo-signature"),
deliveryId: request.headers.get("cymo-delivery-id"),
// whsec_…, returned once by webhooks.create
secret: process.env.CYMO_WEBHOOK_SECRET ?? "",
});
} catch (error) {
if (error instanceof CymoError && rejected.has(error.code)) {
return new Response(error.code, { status: 400 });
}
throw error;
}
const { data: event } = JSON.parse(payload) as Envelope<Event>;
// Answer within 10 seconds; do the work after the response.
after(() => handleEvent(event));
return new Response(null, { status: 204 });
}
async function handleEvent(event: Event) {
// At least once: skip an event.id already stored at this revision or later.
}Retries and deduplication explains why the handler answers fast and skips repeated events.
Develop webhooks locally
cymatics listen forwards your project's events to the route on your machine, signed like production deliveries. Set CYMO_WEBHOOK_SECRET to the signing_secret it prints first, and the same handler verifies them.
$ cymatics listen --forward-to http://localhost:3000/api/cymo-webhook