Skip to main content
Version: 1.0

RPC Handlers

User scripts can register custom RPC methods on the device using Script.addRpcHandler and Script.removeRpcHandler. Registered methods are callable via HTTP, WebSocket, MQTT, or any other RPC channel, just like built-in component methods.

Overview​

Registered methods use the Script.<MethodName> namespace and are routed to the correct script instance via the id parameter in the RPC request. They are callable over any RPC channel and follow the standard RPC protocol, just like built-in component methods. Multiple scripts can register handlers for the same method name; the id parameter determines which script handles the call.

This lets a script expose a clean, structured RPC API to external integrations, instead of parsing raw HTTP requests with HTTPServer. Responses go through the standard RPC machinery, so error codes, authentication, and channel framing all work the same way as for native device methods.

Quick Example​

Script.addRpcHandler("Hello", function (request, params) {
request.result({greeting: "Hello, " + (params.name || "world") + "!"});
});

Call it:

curl http://<device_ip>/rpc/Script.Hello -d '{"id":0,"name":"Alice"}'
# {"greeting":"Hello, Alice!"}

API Reference​

Script.addRpcHandler()​

Registers a handler for a custom RPC method.

Script.addRpcHandler(method, callback[, userdata]) -> number

Parameters​

PropertyTypeDescription

method

string

Method name (max 32 characters). Exposed as Script.MethodName.

callback

function

Handler function, called as callback(request, params, userdata)

PropertyTypeDescription

request

object

The RPCall object used to respond to the call. See RPCall object.

params

object

The params object from the RPC request (includes the routing id).

userdata

any

The userdata passed during registration.

userdata

any

Optional data passed as the third argument to the callback.

Return Value​

Returns a handler handle (a number greater than 0) on success, or 0 on error.

Errors​

The following conditions raise a JavaScript exception (and return 0):

  • Method name is empty or longer than 32 characters.
  • Method name is reserved. Reserved names are the built-in Script component methods: GetStatus, GetConfig, SetConfig, Start, Stop, Eval, PutCode, GetCode, Create, Delete, List.
  • Too many handlers are already registered (default limit: 5 per script).

Script.removeRpcHandler()​

Removes a previously registered RPC handler.

Script.removeRpcHandler(handle) -> boolean

Parameters​

PropertyTypeDescription

handle

number

Handle returned by Script.addRpcHandler.

Return Value​

Returns true if the handler was removed, false if no handler with that handle exists.

RPCall object​

The request argument passed to the handler callback is an RPCall object representing the incoming call. It carries metadata about the caller and provides the methods used to respond.

Properties​

PropertyTypeDescription

method

string

Full RPC method name (e.g. "Script.Toggle").

src

string

Source address of the caller.

dst

string

Destination address.

user

string

Authenticated username, if any.

ch_type

string

Channel type the call arrived on (e.g. "http", "ws", "mqtt").

request.result()​

Sends a successful response.

request.result(data) -> boolean
PropertyTypeDescription

data

any

Any JSON-serializable value (object, array, string, number, boolean, or null) to return to the caller.

Returns true if the response was sent, false if the call is no longer around - it was already answered, or its deadline passed and the framework answered with a timeout on the handler's behalf.

request.error()​

Sends an error response.

request.error(code, message) -> boolean
PropertyTypeDescription

code

number

Error code (e.g. -1, 404, 500).

message

string

Error message.

Returns true if the response was sent, false under the same conditions as result().

request.setTimeout()​

Sets the deadline for answering this call, counted from the moment of the call to setTimeout(). Use it when a handler needs longer than the default 30 seconds, or when it should give up sooner.

request.setTimeout(timeout_ms) -> boolean
PropertyTypeDescription

timeout_ms

number

New deadline in milliseconds. Any positive value is accepted; 0 removes the deadline altogether.

Returns true if the deadline was set, false if the call has already been answered. A negative value raises a JavaScript exception.

With setTimeout(0) the call stays open until the handler answers it or the script stops. It occupies one of the concurrent call slots for as long as it does, so a handler that never answers permanently reduces how many calls the script can serve - prefer a generous deadline over no deadline.

Script.addRpcHandler("Poll", function (request, params) {
request.setTimeout(60000); // This one can take a while.
Shelly.call("HTTP.GET", {url: "http://10.0.0.7/rpc/Shelly.GetStatus"},
function (result, code, msg) {
if (code !== 0) {
request.error(code, msg);
} else {
request.result(JSON.parse(result.body));
}
});
});
caution

Exactly one of result() or error() must be called for each request. If neither is called before the deadline, the framework responds with a timeout error (-104, "timeout") and drops the call; result() and error() then return false and do nothing.

Outbound Shelly.call requests have no timeout of their own, so a handler waiting on one - especially on HTTP.GET to a device that may be slow or unreachable - should raise its own deadline with setTimeout() rather than rely on the default.

Limits​

LimitDefaultBuild variable
Max handlers per script5SHELLY_SCRIPT_MAX_RPC_HANDLERS
Max concurrent in-flight calls5SHELLY_SCRIPT_MAX_RPC_HANDLER_CALLS
Handler response timeout30 sSHELLY_SCRIPT_RPC_HANDLER_TIMEOUT_MS
Method name max length32SHELLY_SCRIPT_RPC_METHOD_MAX_LEN

The response timeout is a default, not a cap: a handler can set any deadline it likes, or none, with setTimeout().

Lifecycle​

  • Handlers are automatically unregistered when the script stops or crashes.
  • In-flight calls to a stopped script receive a 503 (Unavailable) error.
  • A call that passes its deadline is answered with a timeout error and dropped. The handler may still hold a reference to it - in a timer or an outbound call's callback - but answering it then has no effect and returns false.
  • Handlers are per-script-instance; restarting a script clears all of its handlers, so the script must re-register them on startup.

Complete Examples​

Basic handler​

Script.addRpcHandler("Hello", function (request, params) {
request.result({greeting: "Hello, " + (params.name || "world") + "!"});
});

Call: curl http://<device_ip>/rpc/Script.Hello -d '{"id":0,"name":"Alice"}'

Response: {"greeting":"Hello, Alice!"}

Toggle a switch​

Script.addRpcHandler("Toggle", function (request, params) {
let sw = Shelly.getComponentStatus("switch", 0);
if (!sw) {
request.error(-1, "switch not found");
return;
}
Shelly.call("Switch.Toggle", {id: 0}, function (result, code, msg) {
if (code !== 0) {
request.error(code, msg);
} else {
request.result({was: sw.output, now: !sw.output});
}
});
});

Using userdata​

let config = {multiplier: 2};

Script.addRpcHandler("Multiply", function (request, params, ud) {
if (typeof params.value !== "number") {
request.error(-1, "value must be a number");
return;
}
request.result({result: params.value * ud.multiplier});
}, config);

Error handling​

Script.addRpcHandler("Divide", function (request, params) {
if (typeof params.a !== "number" || typeof params.b !== "number") {
request.error(-1, "a and b must be numbers");
return;
}
if (params.b === 0) {
request.error(-1, "division by zero");
return;
}
request.result({result: params.a / params.b});
});

Removing a handler​

let h = Script.addRpcHandler("Temp", function (request, params) {
request.result({ok: true});
});

// Later, when no longer needed:
Script.removeRpcHandler(h);