moxzi
Docs / Server / Calling actors

Calling actorsalpha

Call actor methods over HTTP with JSON or Candid, handle errors, and set the caller.

Every public method of an actor hosted by moxzid can be called over HTTP. This page uses this actor:

shop.mo
import Array "mo:core/Array";
import Runtime "mo:core/Runtime";

actor {
  type Item = {
    name : Text;
    price : Nat;
    tags : [Text];
    note : ?Text;
  };

  type AddResult = { #ok : Nat; #err : Text };

  var items : [Item] = [];

  public func add(item : Item) : async AddResult {
    if (item.price == 0) return #err "price must be positive";
    items := Array.concat(items, [item]);
    #ok(items.size())
  };

  public query func find(name : Text) : async ?Item {
    Array.find<Item>(items, func(i) { i.name == name })
  };

  public query func count() : async Nat {
    items.size()
  };

  public func addThenFail(item : Item) : async () {
    items := Array.concat(items, [item]);
    Runtime.trap("something went wrong");
  };

  public shared ({ caller }) func whoami() : async Principal {
    caller
  };

  public query func double(n : Nat) : async Nat {
    n * 2
  };
};

JSON endpoints#

EndpointFor
POST /call-json/ACTOR/METHODUpdate methods (public func)
POST /query-json/ACTOR/METHODQuery methods (public query func)

The request body is a JSON array with one element per argument, [] if there are none. The response is a JSON array with one element per result. ACTOR is the name from the manifest or the actor's hex id.

curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/add \
  -d '[{"name": "lamp", "price": 30, "tags": ["home"], "note": null}]'
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/add \
  -d '[{"name": "free lamp", "price": 0, "tags": [], "note": "promo"}]'
[{"ok":1}]
[{"err":"price must be positive"}]
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/find -d '["lamp"]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/find -d '["sofa"]'
[{"name":"lamp","note":null,"price":30,"tags":["home"]}]
[null]

moxzid converts between JSON and Candid using the method's Candid types, so the JSON has to match them:

Candid typeJSON
nat, int, and fixed-size integersA number. Values beyond ±2⁵³ are returned as strings; strings are always accepted
float32, float64A number, or "NaN", "Infinity", "-Infinity"
booltrue or false
textA string
principalA string in text form, such as "2vxsx-fae"
blobA base64 string. An array of byte values is also accepted
opt Tnull or the value. A missing record field of type opt T is treated as null
vec TAn array
record { … }An object. Unknown fields are an error
Tuples (record { nat; text })An array
variant { … }An object with one key: {"ok": 1}, {"err": "…"}. A variant without a value is {"tag": null}
opt opt T{"$some": …} for the outer value, so that null and opt null stay different

Large numbers:

curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/double -d '[21]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/double -d '["9007199254740993"]'
[42]
["18014398509481986"]

Errors#

A request the actor can't accept gets HTTP 400 and a JSON body naming the problem:

curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/add -d '[{"name": "lamp"}]'
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/nothing -d '[]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/add -d '[]'
{"code":400,"message":"args[0].tags: missing required field"}
{"code":400,"message":"no method \"nothing\" in the interface (it has: add, addThenFail, count, double, find, whoami)"}
{"code":400,"message":"\"add\" is an update method; use /call-json"}

If a method traps, its changes are rolled back and the call fails with 400. Here addThenFail adds an item and then traps, and the count is unchanged:

curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/addThenFail \
  -d '[{"name": "chair", "price": 50, "tags": [], "note": null}]'
curl -s -w '\n' http://127.0.0.1:7000/query-json/shop/count -d '[]'
{"code":400,"message":"canister trapped: something went wrong"}
[1]
StatusMeaning
200The method returned
400Bad arguments, an unknown method, or the method trapped or rejected
401Missing or wrong token
402The actor has run out of cycles. Add more with POST /cycles/ACTOR
404No such actor
413The public actor request body is too large (8 MiB limit)
503The server is still starting. Wait for /health/ready

Setting the caller#

Calls arrive from the anonymous principal unless you set an x-caller header:

curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/whoami -d '[]'
curl -s -w '\n' http://127.0.0.1:7000/call-json/shop/whoami -d '[]' \
  -H 'x-caller: rrkah-fqaaa-aaaaa-aaaaq-cai'
["2vxsx-fae"]
["rrkah-fqaaa-aaaaa-aaaaq-cai"]

x-caller isn't authenticated. Any client with the server's token can claim any principal, so don't use msg.caller to protect data in actors hosted by moxzid.

OpenAPI#

GET /openapi/ACTOR returns an OpenAPI 3.1 description of the actor's JSON endpoints, generated from its Candid interface. Use it to generate a typed client, or load it into a tool such as Swagger UI.

curl -s http://127.0.0.1:7000/openapi/shop | head -n 12
{
  "components": {
    "schemas": {
      "AddResult": {
        "oneOf": [
          {
            "additionalProperties": false,
            "properties": {
              "ok": {
                "oneOf": [
                  {
                    "minimum": 0,

Candid endpoints#

/call/ACTOR/METHOD and /query/ACTOR/METHOD take and return raw Candid bytes. Use them when you already have Candid encoding, for example from @dfinity/candid and the idlFactory that dfx generate writes. This example uses didc to encode and decode:

didc encode '(21 : nat)' | xxd -r -p > arg.bin
curl -s --data-binary @arg.bin http://127.0.0.1:7000/query/shop/double | xxd -p | didc decode
(42 : nat)

The body is always a complete Candid message. A method with no arguments takes the six bytes DIDL\x00\x00, not an empty body.

From JavaScript#

The JSON endpoints work with fetch, in Node 18 or later and in browsers:

client.mjs
const base = "http://127.0.0.1:7000";

async function call(method, args) {
  const res = await fetch(`${base}/call-json/shop/${method}`, {
    method: "POST",
    body: JSON.stringify(args),
  });
  if (!res.ok) {
    const { message } = await res.json();
    throw new Error(`${method} failed (${res.status}): ${message}`);
  }
  return res.json();
}

const [added] = await call("add", [{ name: "desk", price: 120, tags: ["office"], note: null }]);
console.log(added);

try {
  await call("add", [{ name: "desk" }]);
} catch (e) {
  console.log(e.message);
}
node client.mjs
{ ok: 2 }
add failed (400): args[0].tags: missing required field

If the server has a token, add an Authorization: Bearer TOKEN header to each request. moxzid allows cross-origin requests from any origin, so a page served from elsewhere can call it; the token still controls access.

Limitations#

LimitationWhat to do
Updates aren't deduplicated, so a retried update can run twiceRetry an update only if it's safe to run twice
No certificates, so @dfinity/agent's Actor class doesn't workUse the JSON endpoints, or @dfinity/candid with the Candid endpoints
The caller isn't authenticatedAuthenticate in front of moxzid, and pass the result as x-caller
On this pageJSON endpointsErrorsSetting the callerOpenAPICandid endpointsFrom JavaScriptLimitations