FrightMapsDownload for the best experience
Developers

FrightLink Trigger API

Fire a hub from show software — xLights/FPP, Companion, Vixen, a Pi, a shell script — and get a webhook when one of your sensors trips.

Pre-release. This surface is built and tested but not yet switched on in production. GET /v1/health tells you the truth for the server you are pointed at.

What it is

Base URLhttps://frightlink-app-web-app.web.app/api/v1
AuthAuthorization: Bearer flk....
A key canfire ONE hub’s Fire Groups and ports. Nothing else.

A key cannot read your account, cannot reach another hub, and cannot change a setting. The only thing we will ever send your hub on behalf of a key is a single fire command.

  • Not frame-accurate. Each call crosses the internet and then opens a fresh secure link to your hub. Use it for cues, not for beat-matched chases.
  • Not a delivery guarantee. A 200 means the message broker took the command — see What a 200 means.
  • Not a way to re-fire one prop quickly. A port ignores new triggers for about 5 seconds after it fires. That is the hub protecting the prop.

Getting a key

FrightMaps app → your hub’s settings → Show ControlCreate a key.

  • The key is shown once. We store a one-way hash of it, so there is no “show it again” — lose it, revoke it, mint another.
  • Creating keys is a Pro feature. Keys already issued keep working if Pro lapses; a show going dark mid-night over a billing hiccup is not an acceptable failure mode.
  • Revoking is instant on our side, but allow up to 30 seconds for every server to notice.
  • Treat a key like a password. Anyone holding it can fire that hub.

Endpoints

GET /v1/health no auth

curl https://frightlink-app-web-app.web.app/api/v1/health

{"ok":true,"service":"frightlink-trigger-api","version":"v1",
 "configured":true,"serverTimeMs":1754870000000}

configured:false means the server has no signing secret installed yet, and every authenticated call will answer 503 unconfigured.

GET /v1/targets

What this key may fire, with the ids to paste into your sequence. Run it when you set up, not per cue.

curl -H "Authorization: Bearer $FRIGHTLINK_KEY" \
  https://frightlink-app-web-app.web.app/api/v1/targets

{
  "ok": true,
  "hub": "FrightLink_8857215CFFB0",
  "hubName": "Front Yard",
  "groups": [
    {"target": "group:grp_1754870000000", "name": "Graveyard", "memberCount": 3}
  ],
  "ports": [
    {"target": "port:AUX"}, {"target": "port:PROP1"}, {"target": "port:PROP2"}
  ],
  "portModeKnown": false
}

portModeKnown:false is not a placeholder. Whether PROP2 is currently an output or an input lives only in the hub’s live state, which this endpoint does not consult — so all three are listed and the hub stays the judge. Firing an input-mode port does nothing; it is not a hazard.

POST /v1/fire

curl -X POST \
  -H "Authorization: Bearer $FRIGHTLINK_KEY" \
  -H "Content-Type: application/json" \
  -d '{"target":"group:grp_1754870000000"}' \
  https://frightlink-app-web-app.web.app/api/v1/fire

Exactly one target per call, in either form:

FieldExampleMeaning
target"group:grp_1754870000000"a Fire Group, expanded at fire time
target"port:PROP1"one hub port
target"sequence:PROP1,DELAY:500,PROP2"a literal sequence
group / port / sequencethe same three as separate fields
relayPulseMs500how long an Executioner holds closed (50–5000). Does not change a hub port’s pulse width — that is a per-port setting in the app.
hub"FrightLink_…"optional; must match the key’s hub. It can confirm, never widen — a mismatch is a 403.
{
  "ok": true,
  "hub": "FrightLink_8857215CFFB0",
  "groupName": "Graveyard",
  "sequence": "PROP1,DELAY:500,RELAY>Relay_5024:500",
  "parsedSteps": 3,
  "delivery": "broker_ack",
  "hubAck": "not_observed"
}

parsedSteps is how many steps we parsed, not how many the hub ran.

Sequence grammar

sequence: targets and Fire Group members share one vocabulary — exactly what the hub firmware parses. Steps are separated by commas, and anything else is refused with a 400 that names the offending token.

StepMeaning
AUX / PROP1 / PROP2fire that hub port (case-insensitive)
TRIGGER:PROP1identical to the bare port name
DELAY:<ms>pause, 1–5000 ms
RELAY><sensorId>fire an Executioner for 500 ms
RELAY><sensorId>:<ms>fire an Executioner for that long (50–5000)

Limits: 24 steps, 512 characters, 15 000 ms of total delay.

DELAY blocks the hub. While it waits it is not processing anything else, including your next command. Put long gaps in your show timeline, not inside one API call.

Errors

Every failure is {"ok":false,"error":"…","message":"…"}.

HTTPerrorMeaning
400bad_targetno target, two targets, or a malformed one
400bad_sequencea step the hub would not understand (the message names it)
401unauthorizedmissing or forged key, or not registered on that hub
401revokedrevoked in the app
403forbiddenthe hub in the body is not this key’s hub
404not_foundunknown group or port, or the hub is no longer registered
429rate_limitedthis key is going too fast — honour Retry-After
429hub_busythis hub was commanded moments ago
502publish_failedthe broker did not take it. Nothing fired.
503unconfiguredthe API is not switched on for this server

429 carries a Retry-After header in seconds, plus retryAfterSeconds in the body. Back off; do not hammer.

Rate limits

LimitValue
Per keyburst 20, then about 1 per second
Per hubone command per 250 ms
Server-wide damper600 calls per minute

These are counted in memory per running server instance. Under load several instances run at once, each with its own counters, so the real ceiling is higher than the numbers above. They exist to stop a runaway sequence, not to meter precisely.

The limit you will actually feel is in the hardware: a port ignores a new trigger for about 5 seconds after it fires (adjustable per hub in the app). A music-synced sequence cannot re-fire the same prop faster than that no matter what the API says.

Why a 200 is not “the prop moved”

We publish your command to the hub and report success only once the message broker acknowledges it. Three things can still leave the prop still:

  1. The FrightMaps app is connected to the hub over Bluetooth. The hub drops its cloud connection whenever the app attaches — it does not have the memory for both — so it is not listening and the command is discarded. Close the app during a show. This is the most common cause of “the API isn’t working”.
  2. The 5-second port throttle.
  3. The port is configured as an input, which the hub will not fire.

The hub does publish its own verdict — fired, or understood-and-did- nothing — to its private event stream. This endpoint does not read that, which is why it reports "hubAck":"not_observed" instead of claiming an execution it did not witness.

A sensor starting your show

FrightMaps app → hub settings → Show ControlSensor starts your show. Give us a URL; when a sensor on that hub trips we POST:

{
  "source": "frightlink",
  "version": 1,
  "hubId": "FrightLink_8857215CFFB0",
  "eventId": "0f3c…",
  "event": "MOTION_DETECTED",
  "sensorId": "PIR_5188",
  "sensorName": "Front Walk",
  "portName": "PROP1",
  "ts": 1754870000000
}

Triggering events: MOTION_DETECTED, MMWAVE_GREEN, MMWAVE_BLUE, MMWAVE_PURPLE, PROP_TRIGGERED, INPUT_TRIGGERED. Battery warnings and fault events are deliberately excluded — those are diagnostics, not cues.

Rules you have to live with

  • Your URL must be reachable from the public internet. This is sent from our servers, not from your phone, so a 192.168.x address on your own network cannot work and is refused when you save it. Use a tunnel, a reverse proxy, or a small relay you control.
  • Redirects are refused, not followed. Give us the final URL.
  • One attempt, 3-second timeout, never retried. A haunt night is hundreds of motion events; retrying into a box that is down would be a storm, not a recovery.
  • At most one POST per second per hub by default.

Verifying it came from us

Each POST carries X-FrightLink-Timestamp and X-FrightLink-Signature: sha256=<hex>. The signing secret is in the app under Show Control.

import hmac, hashlib
expected = 'sha256=' + hmac.new(
    secret.encode(),
    (request.headers['X-FrightLink-Timestamp'] + '.' + raw_body).encode(),
    hashlib.sha256).hexdigest()
# compare with hmac.compare_digest against X-FrightLink-Signature

Sign the raw body bytes, before any JSON parsing and re-serialisation.

What we do not offer, and why

Direct read-only access to your hub’s live event stream would be the zero-latency version of this. We do not hand it out: the broker credentials today are a single shared account embedded in every hub’s firmware, with no per-user access rules, so giving them to one integrator would give them every hub’s traffic. That changes when per-user credentials exist; until then this webhook is the honest option.

xLights + FPP recipe

xLights authors sequences; FPP plays them. So the FrightLink call belongs in FPP, on the timeline xLights hands over, or in an FPP event.

POST https://frightlink-app-web-app.web.app/api/v1/fire
Authorization: Bearer flk....
Content-Type: application/json

{"target":"group:grp_1754870000000"}

If your command field cannot set headers

Some “URL command” fields accept only a URL. The key can then go in the query string:

POST https://frightlink-app-web-app.web.app/api/v1/fire?key=flk....

Only if you have to. A key in a URL ends up in server logs, proxy logs and shell history. The response tells you when you have done it. The header is the supported form.

Placing cues

  1. Call /v1/targets once and note the target string for each Fire Group you care about.
  2. In xLights, mark where the scare should land.
  3. In FPP, attach the HTTP command at that point in the sequence.
  4. Budget cues at least 5 seconds apart per prop, and place the trigger slightly ahead of the beat you are aiming at rather than exactly on it. Different props can fire back to back.

The other direction

For “a FrightLink sensor starts an FPP sequence”, use the webhook above pointed at something you control that can reach FPP’s local API. FPP itself sits behind your router; our servers cannot reach it, and nothing here pretends otherwise.

Two things to check in FPP’s own documentation rather than take from us: whether your FPP version can set request headers on an HTTP command, and the exact menu path and command name for issuing one. We have not verified those against a running FPP, and guessing them is how people lose an evening in October.

Need a hub first?

FrightLink is the wireless trigger system behind all of this — sensors that fire animatronic props with no wires across the yard.

See FrightLink