HTTP API

Learn the endpoints `optics serve` exposes for starting sessions, running keywords and streaming results.

optics serve runs a local HTTP server that lets any language or tool drive a device through Optics. You start a session, send keywords one at a time, and read screenshots, UI trees and events back.

For a guided introduction, see Drive Optics over HTTP. The machine-readable OpenAPI document is at https://optics-framework.org/openapi.json.

Start the server#

optics serve [--host HOST] [--port PORT] [--workers N]
FlagDefaultWhat it does
--host127.0.0.1The address to listen on.
--port8000The port to listen on.
--workers1Server processes. Keep it at 1. Sessions live in each process's memory, so with more workers a request can reach a process that doesn't know your session and get a 404.

The server prints its own log lines and Optics' debug logs to the terminal. Interactive docs are at http://127.0.0.1:8000/docs while it runs.

The server doesn't read config.yaml or your project's test files. Each session gets its engines from the request. Sessions also don't write screenshots or page sources to disk, and they log at DEBUG.

Endpoints#

MethodPathWhat it does
GET/ or /healthChecks the server is up. Returns {"status": "Optics Framework API is running", "version": "1.10.5"}.
POST/v1/sessions/startStarts a session from a SessionConfig and launches the app. Returns {"session_id", "driver_id", "status": "created"}.
POST/v1/sessions/{id}/actionRuns one keyword from an ExecuteRequest. Returns {"execution_id", "status": "SUCCESS", "data"}.
POST/v1/sessions/{id}/templatesUploads an image template: {"name", "image_base64"}. The image can be raw base64 or a data: URL. Use the name as an element for the rest of the session. Returns {"name", "status": "ok"}.
POST/v1/sessions/{id}/apiReplaces the session's API definitions for Invoke API. Send {"api": {"collections": ...}}. Returns 204.
GET/v1/sessions/{id}/screenshotThe current screen. data.result is a base64 PNG without a data: prefix, or "" if the capture failed.
GET/v1/sessions/{id}/driver-idThe driver's own session id in data.result.
GET/v1/sessions/{id}/variablesEvery variable in the session: {"variables": {"name": ["value", ...]}}.
GET/v1/sessions/{id}/elementsElements on screen in data.result. Query: filter_config (all, interactive, buttons, inputs, images, text; repeatable) and compact=true. compact ignores the filter. Appium and Playwright only.
GET/v1/sessions/{id}/sourceThe UI tree or HTML. data is {"page_source", "timestamp"} directly, not under result.
GET/v1/sessions/{id}/screen_elementsA screenshot and its elements from the same frame. data is {"screenshot", "elements"} directly, not under result.
GET/v1/sessions/{id}/eventsA stream of keyword events. See Streams.
GET/v1/sessions/{id}/workspace/streamA stream of screenshots and elements whenever the screen changes. See Streams.
GET/v1/keywordsEvery keyword, including flow control, with its description and parameters.
DELETE/v1/sessions/{id}/stopCloses the app and ends the session. Returns {"status": "terminated"}.

SessionConfig#

The body of POST /v1/sessions/start. Every field is optional, but a session needs a driver and at least one element source.

FieldTypeWhat it does
driver_sourceslistThe driver. Each item is a name ("playwright") or {"appium": {"url": ..., "capabilities": {...}}}. A listed item is enabled unless it says "enabled": false.
elements_sourceslistElement sources by name, for example ["appium_find_element", "appium_page_source", "appium_screenshot"]. There's no default here, so always list them.
text_detectionlistOCR engines, for example ["easyocr"].
image_detectionlistImage engines, for example ["templatematch"].
project_pathstringA project folder on the server. Optics loads its image templates. It doesn't load elements or test cases.
api_dataobjectAPI definitions for Invoke API, inline. File paths aren't supported.
ai_self_healbooleanTurns on AI self-heal. Defaults to OPTICS_AI_SELF_HEAL, else off.
llm_providerstringThe self-heal provider. Defaults to OPTICS_LLM_PROVIDER, else gemini.
llm_modelstringThe self-heal model. Defaults to OPTICS_LLM_MODEL, else gemini-2.5-flash.
strict_element_matchbooleanAccepts only exact text matches.
appium_url, appium_configDeprecated. Use driver_sources.

Engine names are the same as in config.yaml. LLM credentials never go in the request; set them in the server's environment.

ExecuteRequest#

The body of POST /v1/sessions/{id}/action.

FieldTypeWhat it does
modestringRequired. Must be "keyword".
keywordstringThe keyword. "Press Element" and "press_element" both work.
paramslist or objectPositional values as a list, or named values as an object: {"element": "text=Sign in"}. A value can be a list of strings: Optics tries each one until the keyword passes.
template_imagesobject{"name": "<base64>"} images available to this request only.

The session has no elements file, so pass locators directly, or define variables first with keywords such as Evaluate.

Responses#

A successful action returns:

{
  "execution_id": "4f1c2b7e-...",
  "status": "SUCCESS",
  "data": { "result": "Trail Runner 2" }
}
  • When the keyword returns a value that isn't an object, data is {"result": <value>}.
  • When the keyword returns an object, data is that object. This is why /source and /screen_elements put their fields directly in data.
  • When self-heal recovers a step, data is {"result", "healed": true, "heal_summary", "suggested_steps"}. suggested_steps lists the keywords and parameters that fixed it.

Unlike optics execute, the HTTP API returns keyword results, so Get Text, Assert Equality and Execute Script values come back to you.

Errors#

StatusWhenBody
400mode isn't "keyword", a template image isn't valid base64, or api_data is invalid.{"detail": "<message>"}
404The session doesn't exist.{"detail": "Session not found"}
404The keyword doesn't exist.{"detail": {"type": "optics:keyword", "code": "Code.E0402", "status": 404, "message": "Keyword ... not found", ...}}
4xx/5xxOptics raised a coded error. The status depends on the code.{"detail": {"type", "code", "status", "message", "details", "meta"}}
500A keyword failed.{"detail": "Execution failed: All fallback attempts failed for keyword '...': ..."}. The Optics code is inside the text.
500The session couldn't start.{"detail": "Session creation failed: ..."} or a coded error.
  • The code field can carry a Code. prefix, as in Code.E0402. It's the same code. See Error codes.
  • DELETE /stop on an unknown session returns 500 with Session termination failed: 404: Session not found.
  • If closing the app fails during DELETE /stop, the session isn't removed. Call it again.

Streams#

Both stream endpoints use server-sent events (SSE): the connection stays open and each event is a data: line of JSON.

**/events** sends one event when each keyword starts, passes or fails, including the automatic launch_app at start, close_and_terminate_app at stop, and the GET helpers such as /screenshot.

data: {"execution_id": "4f1c...", "status": "RUNNING", "message": "Starting keyword: press_element"}
data: {"execution_id": "4f1c...", "status": "SUCCESS", "message": "Keyword press_element executed successfully"}
  • Statuses are RUNNING, SUCCESS and FAIL, plus ERROR or CANCELLED when the stream itself stops.
  • After 15 seconds without events, it sends {"execution_id": "heartbeat", "status": "HEARTBEAT", ...}.
  • Events wait in a queue until someone reads them. With several clients, each event goes to only one of them, so connect one client per session.

**/workspace/stream** sends the screen whenever it changes.

QueryDefaultWhat it does
interval_ms2000How often to check the screen. Minimum 500.
include_sourcefalseAdds the UI tree or HTML as source.
filter_configFilters the elements, as for /elements.

Each event is {"screenshot", "elements", "screenshotFailed"}, plus source when requested. It's sent only when the screen or elements change. Otherwise a {"type": "heartbeat"} arrives every 15 seconds, and errors arrive as {"type": "error", "message"}. The stream ends when the session ends.

Walkthrough#

This drives the Birch app on an Android emulator. You need an Appium server on port 4723 and the appium extra.

  1. Start the server#

    $ optics serve --port 8000

    Leave it running and open a second terminal.

  2. Start a session#

    $ curl -s -X POST http://127.0.0.1:8000/v1/sessions/start \
    >   -H 'Content-Type: application/json' \
    >   -d '{"driver_sources": [{"appium": {"url": "http://127.0.0.1:4723",
    >          "capabilities": {"platformName": "Android", "automationName": "UiAutomator2",
    >            "deviceName": "emulator-5554", "appPackage": "com.birch.shop",
    >            "appActivity": "com.birch.shop.MainActivity"}}}],
    >        "elements_sources": ["appium_find_element", "appium_page_source", "appium_screenshot"],
    >        "text_detection": [], "image_detection": []}'
    {"session_id":"8d0e7c55-...","driver_id":"...","status":"created"}

    The app launches. Save the session id:

    $ SID=8d0e7c55-...
  3. Watch events (optional)#

    In a third terminal:

    $ curl -N http://127.0.0.1:8000/v1/sessions/$SID/events
  4. Run keywords#

    $ curl -s -X POST http://127.0.0.1:8000/v1/sessions/$SID/action \
    >   -H 'Content-Type: application/json' \
    >   -d '{"mode": "keyword", "keyword": "Press Element", "params": [["//*[@resource-id=\"com.birch.shop:id/search\"]", "text=Search"]]}'
    $ curl -s -X POST http://127.0.0.1:8000/v1/sessions/$SID/action \
    >   -H 'Content-Type: application/json' \
    >   -d '{"mode": "keyword", "keyword": "Enter Text", "params": {"element": "text=Search", "text": "Trail Runner 2"}}'
    $ curl -s -X POST http://127.0.0.1:8000/v1/sessions/$SID/action \
    >   -H 'Content-Type: application/json' \
    >   -d '{"mode": "keyword", "keyword": "Assert Presence", "params": ["text=Trail Runner 2", "20"]}'

    The first call passes a list for element, so Optics tries the XPath and then the text.

  5. Save a screenshot#

    $ curl -s http://127.0.0.1:8000/v1/sessions/$SID/screenshot \
    >   | python3 -c 'import sys,json,base64; open("screen.png","wb").write(base64.b64decode(json.load(sys.stdin)["data"]["result"]))'
  6. End the session#

    $ curl -s -X DELETE http://127.0.0.1:8000/v1/sessions/$SID/stop
    {"status":"terminated"}

    Optics writes junit_output.xml for the session to execution_output/ in the project path, or in the folder where the server runs.

Limits#

  • Sessions live in memory. Restarting the server ends them, and optics live and optics mcp can't see them.
  • There's no record or save endpoint. Write the steps you ran into project files to replay them with optics execute.
  • Error detection doesn't run over HTTP.
  • With the ble driver, the automatic launch_app at session start fails, because BLE can't launch apps.