A Node.js/Express server that receives jettyd webhook events and triggers commands in response. Build reactive automations without polling — respond to device events in real time.
mkdir jettyd-webhook-agent && cd jettyd-webhook-agent
npm init -y
npm install express
npm install --save-dev typescript @types/express @types/node ts-node
// src/server.ts
import express, { Request, Response } from 'express';
const app = express();
app.use(express.json());
const JETTYD_API_BASE = 'https://api.jettyd.com/v1';
const JETTYD_API_KEY = process.env['JETTYD_API_KEY'] ?? '';
if (!JETTYD_API_KEY) {
console.error('JETTYD_API_KEY is required');
process.exit(1);
}
// ── jettyd API helper ─────────────────────────────────────────────────────────
async function sendCommand(
deviceId: string,
action: string,
params: Record<string, unknown> = {},
): Promise<void> {
const res = await fetch(`${JETTYD_API_BASE}/devices/${deviceId}/commands`, {
method: 'POST',
headers: {
Authorization: `Bearer ${JETTYD_API_KEY}`,
'Content-Type': 'application/json',
},
body: JSON.stringify({ command_type: action, payload: params }),
});
if (!res.ok) {
const text = await res.text();
throw new Error(`Command failed ${res.status}: ${text}`);
}
console.log(`[${new Date().toISOString()}] command sent: ${action}`, params);
}
// ── Webhook handler ───────────────────────────────────────────────────────────
interface JettydWebhookEvent {
event: string;
device_id: string;
payload: Record<string, unknown>;
timestamp: string;
}
app.post('/webhook/jettyd', async (req: Request, res: Response) => {
const event = req.body as JettydWebhookEvent;
console.log(`[${new Date().toISOString()}] event=${event.event} device=${event.device_id}`);
// Acknowledge immediately — jettyd retries on non-2xx within 30s
res.status(200).json({ ok: true });
// Handle events asynchronously so we don't block the response
handleEvent(event).catch((err) => {
console.error('handleEvent error:', err);
});
});
async function handleEvent(event: JettydWebhookEvent): Promise<void> {
const { event: eventType, device_id, payload } = event;
switch (eventType) {
case 'telemetry.alert': {
// A JettyScript threshold rule fired on the device
const metric = payload['metric'] as string | undefined;
const value = payload['value'] as number | undefined;
if (metric === 'soil.moisture' && value != null && value < 25) {
console.log(`Low moisture alert on ${device_id}: ${value}%`);
await sendCommand(device_id, 'valve.on', { duration_s: 180 });
}
break;
}
case 'device.offline': {
// Device went offline — log and optionally notify
console.warn(`Device ${device_id} went offline at ${event.timestamp}`);
// Add your alerting logic here (Slack, PagerDuty, email, etc.)
break;
}
case 'device.online': {
// Device came back online — re-apply config if needed
console.info(`Device ${device_id} is back online`);
break;
}
default:
console.log(`Unhandled event: ${eventType}`);
}
}
// ── Start ─────────────────────────────────────────────────────────────────────
const PORT = Number(process.env['PORT'] ?? 3000);
app.listen(PORT, () => {
console.log(`jettyd webhook agent listening on :${PORT}`);
});
# Terminal 1 — start the server
export JETTYD_API_KEY=tk_your_key_here
npx ts-node src/server.ts
# Terminal 2 — expose it publicly
ngrok http 3000
ngrok prints a public URL like https://abc123.ngrok.io. Use that as your webhook URL.
curl -X POST https://api.jettyd.com/v1/webhooks \
-H "Authorization: Bearer $JETTYD_API_KEY" \
-H "Content-Type: application/json" \
-d '{
"url": "https://abc123.ngrok.io/webhook/jettyd",
"events": ["telemetry.alert", "device.offline", "device.online"]
}'
curl -X POST http://localhost:3000/webhook/jettyd \
-H "Content-Type: application/json" \
-d '{
"event": "telemetry.alert",
"device_id": "your-device-uuid",
"payload": { "metric": "soil.moisture", "value": 18 },
"timestamp": "2025-01-15T10:30:00Z"
}'
Deploy to any Node.js host (Railway, Render, Fly.io, AWS Lambda). Replace ngrok with your production URL and update the webhook registration in jettyd.
X-Jettyd-Signature header to prevent spoofed events.
Next: LangChain Integration →