Developers

Mokxi from code

Your projects over a REST API, circuits run headlessly on the same engine as the editor, and the tools to put both in CI or in your own editor.

Quick start

Three ways in: plain HTTP, the CLI, or a GitHub Action that fails the build when the circuit misbehaves.

The CLI and the Action are not on npm or the Actions marketplace yet. Until they are, both live in the Mokxi repository, in packages/mokxi-cli and packages/simulate-action.

curl
# Make a token at mokxi.com/account#developer, then:
export MOKXI_TOKEN=mkx_...

curl -s https://mokxi.com/api/v1/me \
  -H "Authorization: Bearer $MOKXI_TOKEN"

# Run the built-in Blink for three simulated seconds and check pin 13
curl -s https://mokxi.com/api/v1/simulate \
  -H "Authorization: Bearer $MOKXI_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "example": "uno-blink",
    "duration": "3s",
    "assertions": [
      { "type": "pin-toggles", "pin": "uno1:13", "count": 6 },
      { "type": "pin-level", "pin": "uno1:13", "atMs": 250, "level": "high" }
    ]
  }'
The mokxi CLI
# The CLI lives in packages/mokxi-cli in the Mokxi repository
mokxi login                     # approve it in the browser, no token to paste
mokxi projects ls
mokxi projects pull 3fQ9x2bLk7Hq blink
mokxi simulate 3fQ9x2bLk7Hq --for 5s \
  --expect-serial "ready" --expect-toggles "uno1:13>=4"
echo $?                         # 0 passed, 1 failed, 2 usage, 3 API error
mokxi watch blink --open        # push on save; the editor tab reruns
GitHub Action
# .github/workflows/circuit.yml
name: Circuit
on: [push]
jobs:
  simulate:
    runs-on: ubuntu-latest
    steps:
      - uses: actions/checkout@v4
      - run: arduino-cli compile -b arduino:avr:uno --output-dir build .
      - uses: mokxi/simulate-action@v1
        with:
          token: ${{ secrets.MOKXI_TOKEN }}
          project: 3fQ9x2bLk7Hq
          firmware: build/sketch.ino.elf
          duration: 5s
          expect-serial: ready
          expect-toggles: uno1:13>=4

Authentication

A personal access token in the Authorization header, on every request.

Make a token in Settings > Developer on your account page and send it as Authorization: Bearer mkx_.... It is shown once, when it is made; Mokxi keeps only a hash of it. A token carries only the scopes you give it, and revoking it stops it at once. The API never reads your browser session, so a web page cannot use your account through it.

ScopeWhat it allows
projects:readRead your projects and their files
projects:writeCreate, change and delete your projects
simulateRun simulations

Tools that should not ask anybody to paste a token (the CLI, editor plugins) use the device login: they ask for a short code, you approve it at mokxi.com/device while signed in, and the tool gets its own token, which then shows up in your list like any other.

The device login
POST /api/v1/auth/device   { "clientName": "Mokxi for VS Code" }
  -> { deviceCode, userCode: "BDFK-QRTW", verificationUri, interval: 5 }

Show userCode, open verificationUri (https://mokxi.com/device).

POST /api/v1/auth/token    { "deviceCode": "..." }   every 5 s
  -> 400 authorization_pending ... then 200 { accessToken: "mkx_..." }

Simulation

Your circuit runs on Mokxi’s servers, on exactly the engine the editor runs in your browser.

POST /v1/simulate takes a saved project, a built-in example, a .mokxi.json document or a diagram with its files. It runs for the simulated time you ask for and answers with what every board printed, the pins you traced, and a pass or fail per assertion: text on the serial port, a pattern, a pin level at a moment, or how many times a pin changed in a window. Send Accept: text/event-stream to watch the serial output arrive as it is printed.

The server does not compile sketches. The editor compiles in your browser with its own toolchain, which is too big for a server request. So each board runs, in this order: an image you send (firmware), the image stored with the project (the editor stores one every time you press Run on a saved project, and PUT /v1/projects/{id}/firmware/{board} stores one from CI), an ELF uploaded into the diagram, or the shipped image of a built-in example you have not edited. A stored image only runs while the project’s files are still the ones it was built from. Anything else is a 422 that names the board.

Every board in the catalog runs server-side. Images can be ELF for every board, Intel HEX for AVR and Arm boards, UF2 for the Pico, the XIAO and the micro:bit, and a raw flash image for AVR. GET /v1/boards lists each board’s formats.

Rate limits

Per token, by the plan of the account that owns it, and a monthly allowance of simulated time per account.

PlanRequests a minuteSimulations an hourSimulated time per runServer time per runTokensSimulation minutes a month
Free603010 s5 s5100
Pro, Student, Teacher and Classroom60060060 s20 s252,000

Every answer to a request with a token carries RateLimit-Limit, RateLimit-Remaining, RateLimit-Reset (seconds) and RateLimit-Policy. A simulation also counts against its own hourly window, reported in X-Simulations-Remaining. Past a limit the answer is 429 with Retry-After. The catalog routes (examples, boards, parts) work without a token at a lower limit.

Simulation minutes. Each account gets a monthly allowance of simulated time, counted across all of its tokens and reset at midnight UTC on the first of each month. Free includes 100 minutes a month. Pro and Student include 2,000 minutes. Teacher and Classroom include 2,000 minutes shared by the class: the teacher and the students in the teacher’s paid classes draw from one pool. Schools and universities can have more; talk to us at https://mokxi.com/schools/quote.

The allowance is checked when a run starts, so a run that starts under it always finishes. Once it is used up, POST /v1/simulate answers 402 with the code quota_exceeded and a message that says when it resets. Nothing is ever charged. Every simulate answer carries X-Quota-Limit-Minutes, X-Quota-Used-Minutes and X-Quota-Reset (an ISO 8601 time), and Settings > Developer shows the same numbers.

Errors

One shape for every failure, with a real status code.

Every error
{
  "error": {
    "code": "insufficient_scope",
    "message": "this token does not have the simulate scope",
    "requestId": "req_7kq2..."
  }
}

Branch on code, show message, and quote requestId (also in the X-Request-Id header) if you write to support.

StatusCodesMeaning
400bad_body, bad_spec, bad_firmware, bad_pathThe request could not be read, or asks for something impossible.
401unauthorizedNo token, or one that is wrong, revoked or expired.
402plan_limit, quota_exceededThe plan’s project cap is reached, or this month’s simulation minutes are used up. Nothing is ever charged.
403insufficient_scopeThe token does not carry the scope the route needs.
404not_foundNo such thing, or a private project that is not yours: the two look the same on purpose.
412precondition_failedAn If-Match or If-None-Match did not hold: somebody else wrote first.
413too_largeOver a size cap: 1.5 MB for a project, 1.5 MB for an image, 8 MB for a simulate request.
422firmware_required, bad_circuit, unknown_pinThe request is sound but the circuit cannot run as asked.
429rate_limitedOver the plan’s limit. Retry-After says how long to wait.
503api_unavailableThe API is not switched on for this site yet.

Your own editor

Keep the code in VS Code, Sublime or anything else, and the circuit in Mokxi.

Files by path. GET /v1/projects/{id}/files lists diagram.json and every board’s files as paths like uno1/sketch.ino, each with an ETag. Write one with PUT /v1/projects/{id}/files/{path} and send the ETag you last read as If-Match: if somebody changed it since, the answer is 412 and nothing is overwritten. If-None-Match: * creates a file only if it is not there.

Watch mode. GET /v1/projects/{id}/events is a Server-Sent Events stream that says updated after every write to the project, whoever made it. Open the project at /p/<id>?watch=1 and the editor tab reloads and reruns on every save; mokxi watch pushes each file as you save it.

A panel inside the editor. https://mokxi.com/embed/local?origin=<your webview origin> is the simulator with nothing around it. Put it in an iframe, wait for mokxi:ready, and post it the circuit; post it again on save. It only listens to its parent window, and only from the origin you named. An edited sketch with no image is compiled right there, by the same in-browser compiler the editor uses. No account and no cookies are involved; nothing is saved.

The embed protocol, version 1
<iframe src="https://mokxi.com/embed/local?origin=vscode-webview://<id>"></iframe>

// The page says it is ready:
{ type: "mokxi:ready", version: 1 }
// Send it the circuit, again on every save:
{ type: "mokxi:load", diagram, files, firmware }   // firmware optional, base64 by board
{ type: "mokxi:serial-input", board: "uno1", text: "hello\n" }
{ type: "mokxi:stop" }
// It answers with:
{ type: "mokxi:status", state: "building" | "running" | "stopped" | "error", message }
{ type: "mokxi:serial", board: "uno1", text: "..." }

GET /v1/boards and GET /v1/parts are the catalog, for completions: every part type, its pins and its properties.

Endpoint reference

Every route, from the OpenAPI 3.1 document at /api/v1/openapi.json. Paths are under https://mokxi.com/api.

Download openapi.json for Postman, Insomnia or a client generator.

Account

Who the token belongs to.

GET/v1/me

The account and token making the request

Any valid token.

Answers: 200, 401, 403, 429, 503

Projects

Your projects: list, read, create, change, delete, export.

GET/v1/projects

List projects

Your own projects by default, newest change first; scope=public lists everyone’s public ones. Paged with an opaque cursor: pass nextCursor back as cursor until it is null.

Scope: projects:read.

  • scope (query, mine | public)
  • visibility (query, all | public | private) With scope=mine only.
  • board (query, string) A board type from GET /v1/boards.
  • q (query, string) Matches the project name.
  • sort (query, latest | loved) loved with scope=public only.
  • limit (query, integer)
  • cursor (query, string)

Answers: 200, 400, 401, 403, 429, 503

POST/v1/projects

Create a project

From a diagram and files, from a .mokxi.json document, as a copy of a built-in (example), or as a fork of a project you can read (fork). Counts against your plan’s project cap (402 plan_limit). Private unless you say public: true.

Scope: projects:write.

Answers: 201, 400, 401, 402, 403, 404, 413, 429, 503

GET/v1/projects/{id}

Get a project, with its diagram and files

Yours, or anybody’s public one. A private project that is not yours is a 404, the same as one that does not exist. The ETag header (also etag in the body) goes in If-Match on a later write; If-None-Match answers 304 when nothing changed.

Scope: projects:read.

  • id (path, required, string) The project id, as in /p/<id>.

Answers: 200, 304, 401, 403, 404, 429, 503

PATCH/v1/projects/{id}

Change a project

Any of name, description, public, pinned, and the body (diagram and files, or a document). Each change to the body is a checkpoint in the version history. Send If-Match to refuse the write when somebody else changed the project first (412).

Scope: projects:write.

  • id (path, required, string) The project id, as in /p/<id>.
  • If-Match (header, string)

Answers: 200, 401, 403, 404, 409, 412, 413, 429, 503

DELETE/v1/projects/{id}

Delete a project

With its history, hearts and stored firmware. If-Match is honored.

Scope: projects:write.

  • id (path, required, string) The project id, as in /p/<id>.

Answers: 200, 401, 403, 404, 429, 503

GET/v1/projects/{id}/export

Export as .mokxi.json

Exactly the file the editor’s Download JSON writes, with its files inside. POST /v1/projects with { "document": ... } imports it again.

Scope: projects:read.

  • id (path, required, string) The project id, as in /p/<id>.

Answers: 200, 401, 403, 404, 429, 503

GET/v1/projects/{id}/events

Follow changes to a project (Server-Sent Events)

A text/event-stream. The first event is state (the project as it is now); then updated after every write, whoever made it, gone if it is deleted or made private, and bye after ten minutes. EventSource reconnects by itself. mokxi watch and the editor’s watch mode (/p/<id>?watch=1) are built on it.

Scope: projects:read.

  • id (path, required, string) The project id, as in /p/<id>.

Answers: 200, 401, 403, 404, 429, 503

Files

A project’s files by path, with ETags for safe sync from an editor.

GET/v1/projects/{id}/files

List a project’s files

diagram.json and every board’s files, each with its size and ETag, and the project’s ETag.

Scope: projects:read.

  • id (path, required, string) The project id, as in /p/<id>.

Answers: 200, 401, 403, 404, 429, 503

GET/v1/projects/{id}/files/{path}

Read one file

The file’s text, with its ETag. If-None-Match answers 304.

Scope: projects:read.

  • id (path, required, string) The project id, as in /p/<id>.
  • path (path, required, string) diagram.json, <board id>/<file> (like uno1/sketch.ino), or a bare file name when the project has one board or a loose sketch.

Answers: 200, 304, 401, 403, 404, 429, 503

PUT/v1/projects/{id}/files/{path}

Write one file

The body is the file’s new text. If-Match: <etag> writes only over that version and If-None-Match: * only creates, so two editors saving at once get a 412 rather than a lost update. A new file on a board that has none starts that board’s project. Writing diagram.json replaces the diagram and leaves the files alone.

Scope: projects:write.

  • id (path, required, string) The project id, as in /p/<id>.
  • path (path, required, string) diagram.json, <board id>/<file> (like uno1/sketch.ino), or a bare file name when the project has one board or a loose sketch.
  • If-Match (header, string)
  • If-None-Match (header, string)

Answers: 200, 201, 400, 401, 403, 404, 412, 413, 429, 503

DELETE/v1/projects/{id}/files/{path}

Delete one file

A board keeps at least one file (409 last_file), and diagram.json cannot be deleted.

Scope: projects:write.

  • id (path, required, string) The project id, as in /p/<id>.
  • path (path, required, string) diagram.json, <board id>/<file> (like uno1/sketch.ino), or a bare file name when the project has one board or a loose sketch.
  • If-Match (header, string)

Answers: 200, 401, 403, 404, 409, 412, 429, 503

Firmware

Built images stored with a project, for the simulator.

GET/v1/projects/{id}/firmware

List stored images

Which boards have an image stored, and whether each was built from the files as they are now (current). A stale image is never run.

Scope: projects:read.

  • id (path, required, string) The project id, as in /p/<id>.

Answers: 200, 401, 403, 404, 429, 503

PUT/v1/projects/{id}/firmware/{board}

Store a built image for a board

The body is the image (ELF, Intel HEX, UF2, or a raw AVR image), at most 1.5 MB. X-Source-Hash is the SHA-256 of the files it was built from (see the /developers page for how it is computed); without it, the files as stored now are assumed.

Scope: projects:write.

  • id (path, required, string) The project id, as in /p/<id>.
  • board (path, required, string) The board’s part id, like uno1.
  • X-Source-Hash (header, string)

Answers: 201, 401, 403, 404, 413, 429, 503

DELETE/v1/projects/{id}/firmware/{board}

Forget a stored image

Scope: projects:write.

  • id (path, required, string) The project id, as in /p/<id>.
  • board (path, required, string) The board’s part id, like uno1.

Answers: 200, 401, 403, 404, 429, 503

Simulation

Run a circuit headlessly.

POST/v1/simulate

Run a circuit headlessly

Runs on the server, on the same wasm engine the editor uses, for the simulated time you ask (capped by your plan). Returns what the boards printed, the pins you traced, and a pass or fail for each assertion. passed is true only when every assertion passed and the run was not cut short.

Send Accept: text/event-stream to get Server-Sent Events instead: start, then serial as the boards print and progress now and then, then one result with the same body the JSON answer has.

Each run also counts against a per-hour simulation window, reported in X-Simulations-Limit, X-Simulations-Remaining and X-Simulations-Reset.

Each run's simulated time counts toward the account's monthly simulation minutes, reported in X-Quota-Limit-Minutes, X-Quota-Used-Minutes and X-Quota-Reset. The check is made when the run starts: a run that starts under the limit finishes even if it goes over. Once the month's minutes are used, the answer is 402 quota_exceeded until the first of next month (UTC). Nothing is ever charged.

Scope: simulate.

Answers: 200, 400, 401, 402, 403, 404, 413, 422, 429, 503

Catalog

Built-in projects, boards and parts. No token needed.

GET/v1/examples

Built-in projects

Every project that ships with Mokxi. full=1 adds each one’s diagram and files. Fork one with POST /v1/projects { "example": "<slug>" }, or run it with POST /v1/simulate { "example": "<slug>" }.

No token needed.

  • board (query, string)
  • full (query, 0 | 1)

Answers: 200, 429

GET/v1/boards

Boards

Every board: pins, the firmware formats the simulator takes, whether it runs server-side, and where its sketches compile.

No token needed.

Answers: 200, 429

GET/v1/parts

Parts

The whole part catalog: every type, its pins and its properties. Stable, for completions in an editor.

No token needed.

Answers: 200, 429

Auth

The device login, for tools that should not ask you to paste a token.

POST/v1/auth/device

Start a device login

Step one of the device login. Show the person userCode and send them to verificationUri (or verificationUriComplete); then poll POST /v1/auth/token. No token needed.

Answers: 200, 429

POST/v1/auth/token

Poll a device login

Every interval seconds, until it answers 200 with the token (once). Until then it is a 400 whose code says why: authorization_pending, slow_down, access_denied or expired_token.

Answers: 200, 400