ДокументацияАвтоматизация
Автоматизация скриптами
Скрипт на том же компьютере может открыть профиль и взять на себя его браузер. Он получает тот же профиль, который вы открываете руками, — с тем же отпечатком, подключением и cookie.
Что такое локальный API
Локальный API — небольшая служба внутри приложения. Она отвечает только на этом компьютере и только на запросы с вашим ключом. Через неё скрипт может получить список профилей, запускать и останавливать их и узнать адрес, по которому библиотека автоматизации подключается к запущенному браузеру.
- Любой тариф, бесплатный тоже.
- Только этот компьютер: служба слушает
127.0.0.1, и открыть её в сеть нельзя. - Выключена, пока вы её не включите.
- Профили создаются в приложении. API запускает те профили, что у вас есть; создавать и менять их он не умеет.
Включить
- Откройте Настройки › API и автоматизация и включите Локальный API. Строка рядом меняется на Работает только на этом компьютере.
- Оставьте Порт —
48361, если вы его не меняли, — или нажмите Случайный свободный порт. - Сразу скопируйте Ключ API кнопкой Копировать: сам ключ не показывается, а после ухода со страницы скопировать его уже нельзя — только заменить. Его длина — 64 символа.

- Адрес
- Где отвечает служба, с кнопкой Копировать, — по умолчанию
http://127.0.0.1:48361. - Пересоздать…
- Создаёт новый ключ. Старый сразу перестаёт работать, как и адреса браузеров, выданные с ним.
- Документация API
- Открывает справочник.
Заметка. Ключ хранится в системном хранилище учётных данных, а не в файле. Адрес скрипту лучше читать из файла, который пишет приложение, — Local API/status.json в папке данных Liminal, — а не зашивать порт; где он лежит, объясняет справочник.
Первый запрос
Каждый запрос несёт ключ в заголовке. Спросите службу, работает ли она:
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/status{"status":"running","version":"<version>","signedIn":true}signedIn говорит, вошёл ли кто-нибудь в приложение. Операциям с профилями это нужно: без входа они отвечают not_signed_in.
Найти профиль и запустить его
Получите список профилей — ответ приходит страницами, и его можно отфильтровать по имени, пространству, папке и статусу:
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profilesВозьмите id нужного профиля и запустите его:
curl -X POST -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles/<profile-id>/start{
"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- Адрес, к которому подключаются Puppeteer и Playwright.
seleniumDebuggerUrl,chromedriverPath- Что нужно Selenium: адрес отладчика и драйвер, который подходит браузеру и поставляется вместе с приложением.
externalIp,countryCode- Как выглядело подключение профиля в момент запуска.
Приложение запускает профиль так, как если бы вы нажали Открыть. Профиль, который уже открыт, дважды не запускается — как подключиться к работающему, описано в справочнике.
Подключить библиотеку
Каждый пример цельный: он читает файл с адресом, находит профиль по имени, запускает его и открывает страницу.
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()import { readFileSync } from 'node:fs'
import { chromium } from 'playwright'
// 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 chromium.connectOverCDP(started.cdpBrowserUrl)
const page = await browser.contexts()[0].newPage()
await page.goto('https://example.com')
console.log(await page.title())
await browser.close()import json
import os
import urllib.parse
import urllib.request
from playwright.sync_api import sync_playwright
# 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.
with open(os.environ["LIMINAL_ADDRESS_FILE"], encoding="utf-8") as handle:
address = json.load(handle)["address"]
def call(path, method="GET"):
request = urllib.request.Request(address + path, method=method)
request.add_header("Authorization", "Bearer " + os.environ["LIMINAL_API_KEY"])
with urllib.request.urlopen(request) as response:
return json.load(response)
profile = call("/v1/profiles?name=" + urllib.parse.quote("Shop EU"))["items"][0]
started = call(f"/v1/profiles/{profile['id']}/start", method="POST")
with sync_playwright() as playwright:
browser = playwright.chromium.connect_over_cdp(started["cdpBrowserUrl"])
page = browser.contexts[0].new_page()
page.goto("https://example.com")
print(page.title())import json
import os
import urllib.parse
import urllib.request
from selenium import webdriver
from selenium.webdriver.chrome.service import Service
# 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.
with open(os.environ["LIMINAL_ADDRESS_FILE"], encoding="utf-8") as handle:
address = json.load(handle)["address"]
def call(path, method="GET"):
request = urllib.request.Request(address + path, method=method)
request.add_header("Authorization", "Bearer " + os.environ["LIMINAL_API_KEY"])
with urllib.request.urlopen(request) as response:
return json.load(response)
profile = call("/v1/profiles?name=" + urllib.parse.quote("Shop EU"))["items"][0]
started = call(f"/v1/profiles/{profile['id']}/start", method="POST")
options = webdriver.ChromeOptions()
options.debugger_address = started["seleniumDebuggerUrl"]
driver = webdriver.Chrome(service=Service(started["chromedriverPath"]), options=options)
driver.get("https://example.com")
print(driver.title)
driver.quit()import { readFileSync } from 'node:fs'
import { Builder } from 'selenium-webdriver'
import chrome from 'selenium-webdriver/chrome.js'
// 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 driver = await new Builder()
.forBrowser('chrome')
.setChromeOptions(new chrome.Options().debuggerAddress(started.seleniumDebuggerUrl))
.setChromeService(new chrome.ServiceBuilder(started.chromedriverPath))
.build()
await driver.get('https://example.com')
console.log(await driver.getTitle())
await driver.quit()Совет. Подключайтесь к браузеру, который у профиля уже есть. Не давайте библиотеке запускать собственный браузер — у того не будет ни отпечатка профиля, ни его cookie.
Остановить профиль
curl -X POST -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/profiles/<profile-id>/stopОстановка закрывает профиль так же, как Закрыть. Есть и принудительная остановка — для профиля, который не закрывается.
Полезно знать
| Вопрос | Ответ |
|---|---|
| Скрипт видит не тот браузер, что я? | Тот же. Отпечаток и подключение применяются точно так же, как при открытии профиля руками, и действуют те же защиты. |
| Сайты видят, что браузер автоматизирован? | Обычные признаки автоматизации у профиля, запущенного через API, скрыты. Отключить это нельзя. |
| Есть ли режим без окна? | Нет. API запускает профиль так же, как приложение. |
| Есть ли лимит на запросы? | Квоты, привязанной к тарифу, нет. |
| Есть ли инструмент командной строки или записанные сценарии? | Нет. Способ автоматизации — локальный API. |
| Что я получу, если что-то не так? | Статус и короткий code, по которому можно ветвиться, — invalid_key, not_signed_in, profile_already_running и другие. В справочнике они перечислены по операциям. |
Сверено с выпущенным приложением: октябрь 2026. Если приложение ведёт себя не так, как здесь написано, — ошибается страница: напишите на [email protected]. Что менялось в каждой версии — в разделе «Что нового».