Skip to content

DocumentationAutomation

Automate with scripts

A script on the same computer can open a profile and take over its browser. The profile it gets is the same one you open by hand — same fingerprint, same connection, same cookies.

What the Local API is

The Local API is a small service inside the app. It answers on this computer only, to requests that carry your key. With it a script can list profiles, start and stop them, and receive the address at which an automation library connects to the started browser.

  • Every plan, the free one included.
  • This computer only: it listens on 127.0.0.1 and cannot be opened to the network.
  • Off until you turn it on.
  • Profiles are made in the app. The API runs the profiles you have; it does not create or edit them.

Turn it on

  1. Open Settings › API & Automation and turn on Local API. The line beside it changes to Running on this computer only.
  2. Keep the Port — 48361 unless you change it — or press Random free port.
  3. Copy the API key now with Copy: the key itself is never displayed, and once you leave the page it cannot be copied again — only replaced. It is 64 characters long.
Settings › API & Automation: the Local API switch, the port, the address and the key
Address
Where the service answers, with a Copy button — by default http://127.0.0.1:48361.
Regenerate…
Makes a new key. The old one stops working at once, and so do the browser addresses given out with it.
API documentation
Opens the reference.

Note. The key is kept in the system’s credential store, not in a file. A script should read the address from the file the app writes — Local API/status.json in Liminal’s data folder — instead of hardcoding a port; the reference explains where it is.

The first request

Every request carries the key in a header. Ask the service whether it is up:

curl
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/status
JSON
{"status":"running","version":"<version>","signedIn":true}

signedIn tells you whether somebody is signed in to the app. The operations that touch profiles need that: without it they answer not_signed_in.

Find a profile and start it

List the profiles — the answer comes in pages and can be filtered by name, workspace, folder and status:

curl
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles

Take the id of the profile you want and start it:

curl
curl -X POST -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles/<profile-id>/start
JSON
{
  "id": "<profile-id>",
  "status": "running",
  "cdpDiscoveryUrl": "http://127.0.0.1:48361/devtools/<token>/json/version",
  "cdpBrowserUrl": "ws://127.0.0.1:9222/devtools/browser/<token>",
  "seleniumDebuggerUrl": "127.0.0.1:9222",
  "chromedriverPath": "/Applications/Liminal.app/.../chromedriver",
  "debugPort": 9222,
  "debugAddress": "127.0.0.1:9222",
  "externalIp": "203.0.113.10",
  "countryCode": "DE"
}
cdpBrowserUrl
The address Puppeteer and Playwright connect to.
seleniumDebuggerUrl, chromedriverPath
What Selenium needs: the debugger address and the driver that matches the browser, shipped with the app.
externalIp, countryCode
What the profile’s connection looked like when it started.

The app starts the profile as if you had pressed Open. A profile that is already open is not started twice — the reference describes how to attach to a running one.

Connect your library

Each example is whole: it reads the address file, finds a profile by name, starts it and opens a page.

import { readFileSync } from 'node:fs'
import puppeteer from 'puppeteer-core'

// Local API/status.json, inside Liminal's user-data directory. The settings
// screen shows the absolute path; that path and the key are your script’s to
// keep — the app puts neither into the environment for you.
const { address } = JSON.parse(readFileSync(process.env.LIMINAL_ADDRESS_FILE, 'utf8'))

async function call(path, init) {
  const response = await fetch(address + path, {
    ...init,
    headers: { Authorization: `Bearer ${process.env.LIMINAL_API_KEY}` },
  })
  const body = await response.json()
  if (!response.ok) throw new Error(body.error.code)
  return body
}

const { items } = await call('/v1/profiles?name=' + encodeURIComponent('Shop EU'))
const started = await call(`/v1/profiles/${items[0].id}/start`, { method: 'POST' })

const browser = await puppeteer.connect({ browserWSEndpoint: started.cdpBrowserUrl })
const page = await browser.newPage()
await page.goto('https://example.com')
console.log(await page.title())
await browser.disconnect()

Tip. Connect to the browser the profile already has. Do not let the library launch a browser of its own — that one would have none of the profile’s fingerprint or cookies.

Stop the profile

curl
curl -X POST -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles/<profile-id>/stop

Stopping closes the profile the way Close does. There is also a forced stop for a profile that does not close.

Good to know

QuestionAnswer
Does a script see a different browser than I do?No. The fingerprint and the connection are applied exactly as when you open the profile by hand, and the same protections hold.
Do sites see that the browser is automated?The usual signs of automation are masked for a profile started through the API. This cannot be turned off.
Is there a headless mode?No. The API starts a profile the same way the app does.
Is there a limit on requests?No quota is tied to a plan.
Is there a command-line tool, or recorded scenarios?No. The Local API is the way to automate.
What do I get when something is wrong?A status and a short code to branch on — invalid_key, not_signed_in, profile_already_running and others. They are listed per operation in the reference.

Checked against the released app: October 2026. If the app does something else than this page says, the page is wrong — write to [email protected]. What changed in each version is in the changelog.