How to build Signal integration through cloud API
Step-by-step guide to controlling a Luxafor Signal device from any script, app, or service using share codes and our cloud API.
Heads up — public API in progress. The Signal cloud API and its documentation are still being finalised, so small changes may happen between now and general availability. This article will be kept up to date; the OpenAPI spec linked at the end is always the source of truth.
In this article
- Before you start
- How Signal control actually works
- What a share code is, in plain language
- Step 1 — Generate and redeem your share code
- Step 2 — Confirm what your account can see
- Step 3 — Check that a device is online
- Step 4 — Send a command to the device
- Step 5 — Give up access when you no longer need it
- The command vocabulary
- Worked example — turn the light red, then play a sound
- Integration checklist
- Where to look next
- Need more help?
Before you start
You need three things ready before you write a single line of integration code:
- A configured Signal device. Set it up once over USB through luxafor.app and make sure it connects to your Wi-Fi. Full instructions are in our Signal setup guide.
- Any audio files you plan to play, already uploaded to the device. Audio cannot be streamed over the API — it has to live on the device's onboard storage. See Adding audio files to your Signal.
- A share code, generated from the dashboard. Section Step 1 below walks through this.
You do not need prior programming experience to follow the conceptual sections of this article. To actually call the API you'll need to be comfortable making HTTP requests with curl , Postman, or your language of choice.
How Signal control actually works
Every command you send travels through three pieces:
- Your integration — any script, app, or service holding a valid access token.
- The Luxafor cloud at
services.luxafor.io— keeps track of devices, accounts, and access permissions. - The Signal device itself, identified by a
deviceIdstring.
Your script ──HTTP──▶ Luxafor cloud ──Wi-Fi──▶ Signal device
You never connect to the device's IP address directly. You send commands to a URL on services.luxafor.io , the cloud delivers them to the device over Wi-Fi, and the device executes them.
Important. Commands sent to a disconnected device are silently dropped — the cloud does not queue them for later. Always check device status (Step 3) before time-sensitive alerts.
What a share code is, in plain language
A share code is a short, single-use token that grants another party — another browser, another app, your own integration — the ability to control a Signal device that has already been configured.
It's how Luxafor hands control of a device to something other than the original configurator, without exposing usernames, passwords, or device serial numbers.
The lifecycle:
- You configure your Signal once at luxafor.app over USB.
- From the dashboard, you generate a share code starting with
share_. - You redeem that code from your integration. The cloud returns an
accessTokenand adeviceId. - From then on, your integration uses the access token to send commands.
Each share code is meant to be redeemed once, fairly soon after it is generated.
If you lose the access token, treat your integration as a fresh installation: generate a new share code from the dashboard and redeem it without an
Authorizationheader. The response will include a brand-new access token tied to a brand-new account.
Step 1 — Generate and redeem your share code
Generate the share code
- Open luxafor.app and sign in.
- Find the device you want the integration to control on the dashboard.
- Open the device's view and click Create share code.
- A popup appears with a short string beginning with
share_. Copy it.
If your integration is something a user installs, this is the value they paste into your setup screen. If you're building a personal integration, paste it directly into your script's configuration.
Redeem the share code
Send the code to the registry with a POST request. This endpoint has two modes, and the difference matters:
Mode A — First redemption (no existing account)
Send the request without an Authorization header. The registry creates a brand-new account, attaches the device to it, and returns an accessToken you'll use for every subsequent call.
POST https://services.luxafor.io/signal/registry/v1/shares/redeem Content-Type: application/json { "code": "share_aBc123XyZ" }
Successful response:
{ "deviceId": "dev_5f7c9e2b", "userId": "usr_8a1d4b6e", "accessToken": "lxr_tk_91c8f4a2b6e7d3..." }
Save the access token immediately. The registry will never return it again. If you lose it, you have to start over with a fresh share code.
Mode B — Adding another device to the same account
Include your existing access token in the Authorization header. The registry attaches the new device to the account that token already represents.
POST https://services.luxafor.io/signal/registry/v1/shares/redeem Content-Type: application/json Authorization: Bearer lxr_tk_91c8f4a2b6e7d3... { "code": "share_dEf456WvU" }
Successful response (no accessToken field, because you already have one):
{ "deviceId": "dev_d2a4b8c1", "userId": "usr_8a1d4b6e" }
Notice the userId matches the value from your earlier redemption — the new device is sitting next to the old one on the same account.
What you should have after Step 1
Persisted somewhere safe — environment variable, secrets manager, encrypted database column — you should now have:
- One
accessToken - One or more
deviceIdstrings
Everything from here uses them.
Step 2 — Confirm what your account can see
This step is optional but useful for sanity-checking Step 1 and for rebuilding state when your local database has drifted out of sync with the registry.
GET https://services.luxafor.io/signal/registry/v1/users/me Authorization: Bearer lxr_tk_91c8f4a2b6e7d3...
Successful response — every device attached to the account in one list:
{ "uid": "usr_8a1d4b6e", "devices": ["dev_5f7c9e2b", "dev_d2a4b8c1"] }
The uid value is the same userId that Step 1 returned. If a device you expected to see is missing, it usually means the redemption in Step 1 went to a different account than you thought — typically because the wrong access token (or no token) was sent.
Step 3 — Check that a device is online
Before sending commands, you usually want to know whether the device is currently reachable over Wi-Fi. This is especially important before user-facing notifications or alerts.
GET https://services.luxafor.io/signal/registry/v1/devices/dev_5f7c9e2b Authorization: Bearer lxr_tk_91c8f4a2b6e7d3...
Successful response:
{ "id": "dev_5f7c9e2b", "connected": true }
If connected is false , the device is offline — typically powered off, out of Wi-Fi range, or waiting for credentials to be re-entered through the Luxafor web app. There is nothing the API can do about that; the device has to come back online on its own. Depending on your integration, you may want to surface a warning to the user, retry later, or fail loudly.
Checking your permissions on a device (optional)
Most integrations don't need this — it exists for setups where a device is shared with multiple parties at different access levels.
GET https://services.luxafor.io/signal/registry/v1/devices/dev_5f7c9e2b/membership Authorization: Bearer lxr_tk_91c8f4a2b6e7d3...
Successful response:
{ "role": "owner" }
Step 4 — Send a command to the device
This is the step that actually drives the light. You POST one command at a time to the command service. The body is a JSON object with a single command field, whose value is one of the AT-style strings described in The command vocabulary.
POST https://services.luxafor.io/signal/api/device/dev_5f7c9e2b/commands Authorization: Bearer lxr_tk_91c8f4a2b6e7d3... Content-Type: application/json { "command": "AT+SC=4095,FF0000" }
A successful response is a 2xx status with an empty body. The bitmask 4095 selects all 12 LEDs on the current hardware variant — the example above turns the whole ring solid red. (Older 11-LED variants use 2047 .)
Two things worth knowing about this endpoint
- The path prefix is
/signal/api, not/signal/registry/v1. The command service is a sibling of the registry, running on the same host. It is not described in the registry's OpenAPI document, but it is still mandatory for any integration that actually drives the light. - No batch endpoint. If you need to do several things in a row — set a color, set the volume, play a sound — send one request per command in the order you want them to take effect. The worked example below shows this.
The same Authorization: Bearer <accessToken> you obtained in Step 1 works here. There is no separate token for the command service.
Step 5 — Give up access when you no longer need it
When your integration no longer needs the device — a customer uninstalls your app, the device is removed from your admin dashboard, or you're cleaning up after a test — let the registry know by deleting your own membership.
DELETE https://services.luxafor.io/signal/registry/v1/devices/dev_5f7c9e2b/users/me Authorization: Bearer lxr_tk_91c8f4a2b6e7d3...
A successful call returns 204 No Content : status code 204, empty body.
After this, the device is no longer attached to your account, and any other endpoint you call for that deviceId with the same access token will return 401 or 403 .
The device itself is not affected. It stays configured on the original owner's account, and the owner can re-share it at any time by generating a new share code.
If your access token starts returning 401 or 403 on every call without your having issued this DELETE yourself, the user has probably revoked the share or deleted their account on the Luxafor side. Stop using the stored token, ask the user for a fresh share code, and start again from Step 1 in Mode A.
The command vocabulary
Commands sent through the /commands endpoint use a small text protocol inspired by the AT command set used in modems. Each command is sent as a string inside { "command": "..." } .
Solid color
AT+SC=<ledBits>,RRGGBB
<ledBits>is a bitmask selecting which LEDs to light: each bit corresponds to one LED in the ring.- Current Signal hardware has 12 LEDs, so the "all on" value is
4095(binary111111111111). - Older units have 11 LEDs and use
2047(binary11111111111). - If your integration may run against both variants, store the LED count per device and compute the bitmask:
2^n − 1fornLEDs.
- Current Signal hardware has 12 LEDs, so the "all on" value is
RRGGBBis a hexadecimal color without the#prefix.
Examples:
| Goal | Command |
|---|---|
| All 12 LEDs calm blue | AT+SC=4095,3366FF |
| All 12 LEDs solid red | AT+SC=4095,FF0000 |
| First 5 LEDs blue | AT+SC=31,0000FF |
Built-in patterns
The device firmware ships with 25 built-in patterns, each referenced by a numeric ID from 0 to 24. Triggering one is a two-command sequence — first select the pattern with AT+DP , then start playback with AT+PP :
AT+DP=<patternId> AT+PP=<enable>,<repeatCount>
<enable>is1to start,0to stop.<repeatCount>is between1and255. The value255means "play indefinitely until stopped."
Pattern catalog (firmware 1.0):
| ID | Pattern | ID | Pattern | ID | Pattern |
|---|---|---|---|---|---|
| 0 | Police | 9 | Red Running | 18 | Blue Pulses |
| 1 | Traffic Lights | 10 | Green Running | 19 | Yellow Pulses |
| 2 | Red Flashes | 11 | Blue Running | 20 | Magenta Pulses |
| 3 | Green Flashes | 12 | Yellow Running | 21 | Cyan Pulses |
| 4 | Blue Flashes | 13 | Magenta Running | 22 | White Pulses |
| 5 | Yellow Flashes | 14 | Cyan Running | 23 | Rainbow |
| 6 | Magenta Flashes | 15 | White Running | 24 | Alert |
| 7 | Cyan Flashes | 16 | Red Pulses |
|
|
| 8 | White Flashes | 17 | Green Pulses |
|
|
If you need the authoritative list at runtime — for example, to populate a dropdown in your UI — the device returns it in response to AT+GDPL .
Example — play the police pattern five times:
AT+DP=0 AT+PP=1,5
Audio playback
Audio files have to live on the device itself before they can be played. There is no API for streaming audio. To get a file onto a device, open the device's settings at luxafor.app and use the file upload control.
The device ships with seven pre-loaded sound effects. You can add additional MP3 files until on-device storage runs out — the maximum file size is governed by free storage, not a fixed limit (though the web app may enforce its own limit at upload time).
Once a file is on the device, play it with:
AT+VOL=<0-100> AT+PLAY=<filename>
AT+VOLtakes a percentage from 0 to 100 (firmware default is 20). It's optional — if you don't send it, the device uses its current volume, which persists across reboots.AT+PLAYalso accepts an optional repeat count:AT+PLAY=<filename>,<repeatCount>, where<repeatCount>is between 1 and 255.- To stop playback before it finishes, send
AT+STOP.
Worked example — turn the light red, then play a sound
This assumes you have already redeemed a share code and saved the resulting accessToken and deviceId .
Request 1 — Set the light to solid red:
POST https://services.luxafor.io/signal/api/device/{deviceId}/commands Authorization: Bearer <accessToken> Content-Type: application/json { "command": "AT+SC=4095,FF0000" }
Request 2 — Set the volume to 60%:
POST https://services.luxafor.io/signal/api/device/{deviceId}/commands Authorization: Bearer <accessToken> Content-Type: application/json { "command": "AT+VOL=60" }
Request 3 — Play the alert sound:
POST https://services.luxafor.io/signal/api/device/{deviceId}/commands Authorization: Bearer <accessToken> Content-Type: application/json { "command": "AT+PLAY=alert.mp3" }
Each command is a separate HTTP request. Send them in the order you want them to take effect.
Equivalent curl call for the first request
curl -X POST \ "https://services.luxafor.io/signal/api/device/<DEVICE_ID>/commands" \ -H "Authorization: Bearer <ACCESS_TOKEN>" \ -H "Content-Type: application/json" \ -d '{"command":"AT+SC=4095,FF0000"}'
Integration checklist
The complete surface area required to drive a Signal device end-to-end:
- Configure the device once through luxafor.app using a USB cable. Confirm it can be controlled from the dashboard.
- Upload any audio files you intend to use from the same dashboard. MP3, under 1 MB each.
- Generate a share code from the dashboard for each integration you build.
- Redeem the share code with
POST /signal/registry/v1/shares/redeem. Save theaccessTokenanddeviceIdsomewhere safe — environment variable, secrets manager, or an encrypted column in your application's database. - (Optional) Confirm the device is online with
GET /signal/registry/v1/devices/{deviceId}before each command burst. - Send commands to
POST /signal/api/device/{deviceId}/commandsusing the AT command strings from the vocabulary section. - Handle
401/403responses by treating the access token as invalid and prompting the user to generate a new share code.
That's the whole thing.
Where to look next
- Full registry API specification, including every response status code and field — browsable at services.luxafor.io/signal/registry/openapi.
- Raw machine-readable JSON (useful for generating a typed client) — services.luxafor.io/signal/registry/openapi/json.
- Signal setup guide — Initial Signal configuration
- Audio file management — Adding audio files to your Signal
- Shopify integration — Connecting Signal to Shopify
Need more help?
If something in this article isn't working as expected, or you're building an integration that needs a capability you don't see here, get in touch:
- Email: support@luxafor.com
(Mon–Fri, 09:00–17:30 EET)
When you write in, please include your deviceId , the exact HTTP request you sent, and the response you got back. That triages an integration question much faster than a screenshot.