Ir al contenido

DocumentaciónAutomatización

API local

Última comprobación con el contrato: septiembre de 2026

Publicada, y ya en la compilación que puedes descargar hoy, tanto en macOS como en Windows. El interruptor está en los ajustes de la app y la API permanece apagada hasta que la enciendes. A qué llamadas responde esa compilación se indica en cada operación y en cada ejemplo de abajo, en lugar de escribirlo aquí en una frase que la siguiente versión dejaría falsa.

Un script en el mismo ordenador convierte el navegador de un perfil en algo que puede manejar. La interfaz es un servicio, una clave y un archivo de dirección —descritos primero, porque todo se apoya en ellos— y un puñado de rutas por encima: encontrar un perfil, abrirlo, conectar Puppeteer, Playwright o Selenium a la dirección con la que responde y volver a cerrarlo.

La interfaz de la app está solo en inglés y en ruso. Los botones y ajustes se nombran aquí como los muestra la interfaz en inglés.

Cómo activarla

Apagada por defecto y accesible solo desde este ordenador.

El interruptor
Abre Settings → API & Automation y activa Local API. Hasta que lo hagas, nada escucha: el puerto no está abierto y un script recibe una conexión rechazada, no una respuesta vacía.
Solo este ordenador
El servicio acepta conexiones en 127.0.0.1 y en ningún otro sitio. Otro equipo de tu red no puede alcanzarlo y no hay ningún ajuste que lo abra: un script se ejecuta junto a Liminal, no contra él a través de la red.
El puerto
El valor por defecto es 48361, un puerto propio de Liminal para no chocar con otro navegador que ya esté escuchando en el ordenador. Escribe otro distinto o pulsa Random free port y deja que la app elija. El cambio se aplica al instante, sin reiniciar Liminal.
Cuando el puerto está ocupado
Si no se puede abrir el puerto, la pantalla muestra Port in use y el archivo de dirección indica port_in_use. Nada escucha hasta que eliges otro puerto, así que un script que lee el archivo distingue «apagada» de «no pudo arrancar».

La clave

Una sola clave, mostrada una vez y guardada donde tu sistema operativo guarda los secretos.

Cópiala cuando aparezca
Al activar la API se emite una clave de 64 caracteres hexadecimales en minúscula y se muestra una sola vez. Si sales de la pantalla, queda oculta: la app no puede volver a mostrarla, solo emitir una nueva.
Dónde se guarda
En Keychain en macOS y en Credential Manager en Windows; nunca en un archivo de ajustes, en una instantánea de sincronización ni en un registro, y nunca en el archivo de dirección de abajo. Guarda tu copia como guardas cualquier otra credencial que use tu script.
Cómo revocarla
Regenerate key emite una clave nueva y deja de aceptar la anterior de inmediato, sin reiniciar la app y sin cambiar el puerto. Un script que conserve la clave anterior es rechazado desde la siguiente petición.
Cómo enviarla
Cada petición lleva la clave en una cabecera: Authorization: Bearer seguido de la propia clave. No hay parámetro de consulta ni cookie, y una petición sin la cabecera se rechaza igual que una con una clave incorrecta.

Dónde encuentra la dirección un script

La app escribe su dirección en un archivo, para que no haya que teclear nada dos veces.

El archivo
La pantalla de ajustes muestra la ruta absoluta de Local API/status.json dentro del directorio de datos de usuario de Liminal. Léelo en lugar de fijar un puerto en el código: sigue al puerto que tú pones y al que eligió Random free port.
Qué contiene
Cuatro campos —enabled, status, address, port— y ningún secreto de ningún tipo.
{"enabled":true,"status":"running","address":"http://127.0.0.1:48361","port":48361}
Cuando la API está apagada
El archivo sigue en su sitio y lo dice: off, con la dirección vacía y sin puerto. La misma forma, otro estado: un script lee un solo archivo para saberlo todo.
{"enabled":false,"status":"off","address":"","port":0}
Se sustituye entero, nunca queda a medio escribir
Cada cambio —encender o apagar la API, un puerto nuevo, un puerto que no se pudo abrir— reescribe el archivo de forma atómica. Un script que lo lee durante un cambio ve el contenido anterior o el nuevo, nunca una línea truncada.

La petición de estado

La llamada por la que empezar: si Liminal está en marcha y si alguien ha iniciado sesión.

La llamada
Un GET a /v1/status en la dirección del archivo, con la clave en la cabecera.
curl -H 'Authorization: Bearer <key>' http://127.0.0.1:48361/v1/status
La respuesta
HTTP 200, con la versión de la compilación en marcha y si alguien ha iniciado sesión en la app. Un signedIn falso no es un error: la API responde; son las operaciones que leen el catálogo o abren un perfil las que necesitan una cuenta.
{"status":"running","version":"<version>","signedIn":true}
Una clave ausente o revocada
HTTP 401 y el código invalid_key: es permanente y es por lo que conviene ramificar. El mensaje que lo acompaña es la redacción de la app para una persona que lee un registro; trátalo como texto que puede cambiar, no como parte del contrato.
{"error":{"code":"invalid_key","message":"Неверный ключ"}}
Llámala exactamente como lo hace el archivo
Usa 127.0.0.1 y el puerto del archivo. Una petición que llega nombrando cualquier otro host no recibe respuesta, aunque alcance el mismo puerto: el servicio solo contesta a su propia dirección.
Qué hace un Liminal anterior
Nada: no se abre ningún puerto, así que un script que se conecta es rechazado. Ese rechazo es lo que le permite a un script distinguir una compilación sin la API local de una que la tiene apagada: en el segundo caso el archivo de dirección existe y dice off.

Qué hace esta versión y qué no

Dicho sin rodeos, para que nada aquí se lea como una promesa que no es.

Una operación llega a esta página antes que a tu compilación
La interfaz crece versión a versión, y una llamada ya escrita se documenta aquí antes que la compilación que la responde: marcada, nunca oculta. Así que lee la marca, no la promesa: está en cada operación y en cada ejemplo de abajo, y es lo que te dice si lo que vas a automatizar funciona en la app que tienes.
Para qué planes
Todos los planes, incluido el gratuito. La API local no es un nivel de pago: un script en tu propio ordenador es la forma en que mucha gente usa un navegador antidetección, y ponerlo detrás de un plan sería cobrar un peaje por ello.Planes
Con qué frecuencia puede preguntar un script
No hay ninguna cuota ligada a tu plan. El servicio se protege de las avalanchas de peticiones, algo que un script escrito para él no notará.
Un perfil automatizado es el mismo perfil
Conserva su huella, su proxy y sus cookies, y sale a la red a través de su proxy exactamente igual que cuando lo abres a mano. La API es una vía de entrada, no un segundo modo del navegador.Datos y seguridad

Cada operación, una por una

Para qué sirve cada ruta, qué recibe, qué responde y cada rechazo que puede dar. La insignia junto a una operación indica si la compilación que puedes descargar hoy la responde.

GET /v1/status

En la compilación publicada

Si Liminal está en marcha y si alguien ha iniciado sesión

La llamada más barata que existe y la que conviene repetir mientras esperas a la app. Responde sin cuenta: un signedIn falso es un estado, no un error.

Parámetros

Ninguno.

Respuesta

{
  "status": "running",
  "version": "<version>",
  "signedIn": true
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.

GET /v1/profiles

En la compilación publicada

Los perfiles del catálogo, filtrados y paginados

Lee el mismo catálogo que muestra la app. Los filtros se combinan y todos son exactos salvo name, que coincide con cualquier perfil cuyo nombre contenga lo que escribiste, sin distinguir mayúsculas.

Parámetros

pagequery · integer · opcional · de 1 a 1000000 · por defecto, 1
Qué página leer, contando desde la primera.
pageSizequery · integer · opcional · de 1 a 100 · por defecto, 100
Cuántos perfiles contiene una página. El máximo es el valor por defecto.
workspaceIdquery · string · opcional
Solo los perfiles de ese espacio de trabajo.
folderIdquery · string · opcional
Solo los perfiles de esa carpeta.
statusquery · string · opcional
Solo los perfiles en ese estado; running es el estado de un perfil abierto.
namequery · string · opcional
Solo los perfiles cuyo nombre contenga esto, sin distinguir mayúsculas.

Respuesta

{
  "page": 1,
  "pageSize": 100,
  "total": 1,
  "items": [
    "<profile>"
  ]
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.
  • 403 not_signed_in — Nadie ha iniciado sesión en la app; las operaciones del catálogo necesitan una cuenta.
  • 403 team_closed — El equipo activo está cerrado: renueva su plan o cambia a otro equipo.
  • 503 catalog_loading — El catálogo de perfiles aún se está cargando. Vuelve a preguntar.
  • 503 catalog_unavailable — La app no pudo abrir el catálogo de perfiles.
  • 400 invalid_pagination — page o pageSize está fuera del intervalo aceptado.

GET /v1/profiles/{id}

En la compilación publicada

Un perfil, con su proxy y su huella

El mismo objeto que devuelve la lista, para un solo identificador. La dirección del proxy se muestra; su contraseña, nunca.

Parámetros

idpath · string · obligatorio
El identificador del perfil, tal como lo devuelven las operaciones del catálogo.

Respuesta

{
  "id": "<profile-id>",
  "name": "Shop EU",
  "workspaceId": "<workspace-id>",
  "folderId": "<folder-id>",
  "status": "running",
  "health": "ok",
  "proxy": {
    "id": "<proxy-id>",
    "name": "Residential DE",
    "type": "socks5",
    "host": "proxy.example.net",
    "port": 1080,
    "working": true,
    "lastCheckedExternalIp": "203.0.113.10",
    "lastCheckedCountryCode": "DE"
  },
  "fingerprint": {
    "platform": "MacIntel",
    "language": "de-DE",
    "timezone": "Europe/Berlin"
  },
  "lastOpenedAt": "2026-09-30T08:15:00.000Z"
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.
  • 403 not_signed_in — Nadie ha iniciado sesión en la app; las operaciones del catálogo necesitan una cuenta.
  • 403 team_closed — El equipo activo está cerrado: renueva su plan o cambia a otro equipo.
  • 503 catalog_loading — El catálogo de perfiles aún se está cargando. Vuelve a preguntar.
  • 503 catalog_unavailable — La app no pudo abrir el catálogo de perfiles.
  • 404 profile_not_found — No hay ningún perfil con ese identificador en el catálogo.

POST /v1/profiles/{id}/start

En la compilación publicada

Abrir un perfil y obtener la dirección a la que se conecta un driver

Inicia el perfil como lo hace la app —su huella, su proxy, sus cookies— y responde con la dirección CDP cuando el navegador está listo. La llamada espera; no devuelve una tarea que haya que consultar.

Parámetros

idpath · string · obligatorio
El identificador del perfil, tal como lo devuelven las operaciones del catálogo.
debugPortbody · integer · opcional · de 1024 a 65534 · por defecto, 0
El puerto al que se enlaza el endpoint CDP. Con 0 la app elige uno libre; cualquier otro valor tiene que ser un puerto que tu usuario pueda abrir.
timeoutSecondsbody · integer · opcional · de 1 a 180 · por defecto, 60
Cuánto espera la llamada a que el perfil arranque antes de rendirse.
maskAutomationbody · boolean · opcional · por defecto, true
Solo se acepta true. El enmascaramiento de la automatización no se puede desactivar: un perfil manejado por un script es el mismo perfil que ve un sitio cuando lo abres a mano.

Respuesta

{
  "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"
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.
  • 403 not_signed_in — Nadie ha iniciado sesión en la app; las operaciones del catálogo necesitan una cuenta.
  • 503 runtime_unavailable — La app no puede iniciar ni detener un perfil en este momento.
  • 403 team_closed — El equipo activo está cerrado: renueva su plan o cambia a otro equipo.
  • 503 catalog_loading — El catálogo de perfiles aún se está cargando. Vuelve a preguntar.
  • 503 catalog_unavailable — La app no pudo abrir el catálogo de perfiles.
  • 404 profile_not_found — No hay ningún perfil con ese identificador en el catálogo.
  • 409 profile_already_running — El perfil ya está abierto, su inicio sigue en curso o una dirección de driver entregada antes sigue siendo válida.
  • 400 invalid_start_options — El cuerpo no es un objeto JSON.
  • 400 invalid_port — debugPort no es 0 ni un puerto del intervalo aceptado.
  • 400 invalid_start_timeout — timeoutSeconds está fuera del intervalo aceptado.
  • 400 masking_unavailable — maskAutomation se envió como false, algo que la app no ofrece.
  • 409 port_in_use — El puerto de depuración que pediste está ocupado.
  • 409 listen_failed — El puerto de depuración no se pudo abrir por otro motivo.
  • 409 start_failed — El perfil no arrancó. Un detail seguro nombra la etapa cuando la app puede decirla.
  • 409 proxy_unavailable — El perfil exige su proxy y el proxy no respondió.
  • 502 start_failed — La preparación del perfil falló antes de pedirle al navegador que arrancara.
  • 503 cdp_unavailable — El perfil arrancó, pero su endpoint CDP no llegó a estar disponible.
  • 504 start_timeout — El perfil no arrancó dentro de timeoutSeconds.

POST /v1/profiles/{id}/attach-automation

En la compilación publicada

Obtener una dirección de driver para un perfil que ya está abierto

Para un perfil que abriste a mano y ahora quieres manejar. No reinicia nada: la sesión, sus pestañas y sus cookies se quedan como están.

Parámetros

idpath · string · obligatorio
El identificador del perfil, tal como lo devuelven las operaciones del catálogo.
debugPortbody · integer · opcional · de 1024 a 65534 · por defecto, 0
El puerto al que se enlaza el endpoint CDP. Con 0 la app elige uno libre; cualquier otro valor tiene que ser un puerto que tu usuario pueda abrir.
timeoutSecondsbody · integer · opcional · de 1 a 180 · por defecto, 60
Cuánto espera la llamada a que el perfil arranque antes de rendirse.
maskAutomationbody · boolean · opcional · por defecto, true
Solo se acepta true. El enmascaramiento de la automatización no se puede desactivar: un perfil manejado por un script es el mismo perfil que ve un sitio cuando lo abres a mano.

Respuesta

{
  "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"
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.
  • 403 not_signed_in — Nadie ha iniciado sesión en la app; las operaciones del catálogo necesitan una cuenta.
  • 503 runtime_unavailable — La app no puede iniciar ni detener un perfil en este momento.
  • 403 team_closed — El equipo activo está cerrado: renueva su plan o cambia a otro equipo.
  • 503 catalog_loading — El catálogo de perfiles aún se está cargando. Vuelve a preguntar.
  • 503 catalog_unavailable — La app no pudo abrir el catálogo de perfiles.
  • 404 profile_not_found — No hay ningún perfil con ese identificador en el catálogo.
  • 409 profile_already_running — El perfil ya está abierto, su inicio sigue en curso o una dirección de driver entregada antes sigue siendo válida.
  • 400 invalid_start_options — El cuerpo no es un objeto JSON.
  • 400 invalid_port — debugPort no es 0 ni un puerto del intervalo aceptado.
  • 400 invalid_start_timeout — timeoutSeconds está fuera del intervalo aceptado.
  • 400 masking_unavailable — maskAutomation se envió como false, algo que la app no ofrece.
  • 409 port_in_use — El puerto de depuración que pediste está ocupado.
  • 409 listen_failed — El puerto de depuración no se pudo abrir por otro motivo.
  • 409 start_failed — El perfil no arrancó. Un detail seguro nombra la etapa cuando la app puede decirla.
  • 409 proxy_unavailable — El perfil exige su proxy y el proxy no respondió.
  • 502 start_failed — La preparación del perfil falló antes de pedirle al navegador que arrancara.
  • 503 cdp_unavailable — El perfil arrancó, pero su endpoint CDP no llegó a estar disponible.
  • 504 start_timeout — El perfil no arrancó dentro de timeoutSeconds.
  • 409 profile_not_running — El perfil no está abierto, así que no hay nada a lo que conectarse.

POST /v1/profiles/{id}/stop

En la compilación publicada

Cerrar un perfil como lo haría su ventana

Deja que la sesión termine de escribir su estado. Es la llamada que conviene hacer entre ejecuciones automatizadas sobre el mismo perfil.

Parámetros

idpath · string · obligatorio
El identificador del perfil, tal como lo devuelven las operaciones del catálogo.

Respuesta

{
  "id": "<profile-id>",
  "status": "closed"
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.
  • 403 not_signed_in — Nadie ha iniciado sesión en la app; las operaciones del catálogo necesitan una cuenta.
  • 503 runtime_unavailable — La app no puede iniciar ni detener un perfil en este momento.
  • 403 team_closed — El equipo activo está cerrado: renueva su plan o cambia a otro equipo.
  • 503 catalog_loading — El catálogo de perfiles aún se está cargando. Vuelve a preguntar.
  • 503 catalog_unavailable — La app no pudo abrir el catálogo de perfiles.
  • 404 profile_not_found — No hay ningún perfil con ese identificador en el catálogo.
  • 409 profile_not_running — El perfil no está abierto, así que no hay nada que cerrar.
  • 409 stop_failed — La app no pudo cerrar el perfil.

POST /v1/profiles/{id}/force-stop

En la compilación publicada

Cerrar un perfil que no se cierra

La misma respuesta, sin esperar a que la sesión se asiente. Recurre a ella cuando una parada no ha vuelto, no por costumbre.

Parámetros

idpath · string · obligatorio
El identificador del perfil, tal como lo devuelven las operaciones del catálogo.

Respuesta

{
  "id": "<profile-id>",
  "status": "closed"
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.
  • 403 not_signed_in — Nadie ha iniciado sesión en la app; las operaciones del catálogo necesitan una cuenta.
  • 503 runtime_unavailable — La app no puede iniciar ni detener un perfil en este momento.
  • 403 team_closed — El equipo activo está cerrado: renueva su plan o cambia a otro equipo.
  • 503 catalog_loading — El catálogo de perfiles aún se está cargando. Vuelve a preguntar.
  • 503 catalog_unavailable — La app no pudo abrir el catálogo de perfiles.
  • 404 profile_not_found — No hay ningún perfil con ese identificador en el catálogo.
  • 409 profile_not_running — El perfil no está abierto, así que no hay nada que cerrar.
  • 409 stop_failed — La app no pudo cerrar el perfil.

GET /v1/running-profiles

En la compilación publicada

Qué está abierto ahora mismo y qué se puede manejar

Cada perfil que está abierto, lo haya abierto una ventana o una llamada —startedBy dice cuál—, con su dirección de driver cuando hay una disponible. Reconéctate desde aquí después de que tu script se reinicie.

Parámetros

Ninguno.

Respuesta

{
  "items": [
    {
      "id": "<profile-id>",
      "workspaceId": "<workspace-id>",
      "status": "running",
      "automationAvailable": true,
      "startedBy": "api",
      "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"
    }
  ]
}

Rechazos

  • 401 invalid_key — La clave falta, está mal formada o se ha regenerado desde entonces.
  • 403 not_signed_in — Nadie ha iniciado sesión en la app; las operaciones del catálogo necesitan una cuenta.
  • 403 team_closed — El equipo activo está cerrado: renueva su plan o cambia a otro equipo.
  • 503 catalog_loading — El catálogo de perfiles aún se está cargando. Vuelve a preguntar.
  • 503 catalog_unavailable — La app no pudo abrir el catálogo de perfiles.

Manejar un perfil desde tu propio script

Cada bloque está completo: lee el archivo de dirección, llama a la API y conecta el driver. Copia uno, pon el nombre de tu propio perfil y funciona. La insignia indica si la compilación que puedes descargar hoy responde a todo lo que ese bloque necesita.

Puppeteer (JavaScript)

En la compilación publicada

Se conecta por CDP a la dirección con la que respondió el inicio, igual que un script de Puppeteer llega a un navegador que ya está en marcha.

Llama a /v1/profiles · /v1/profiles/{id}/start

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()

Playwright (JavaScript)

En la compilación publicada

La misma conexión mediante la entrada CDP de Playwright, al contexto que el perfil ya tiene y no a uno nuevo.

Llama a /v1/profiles · /v1/profiles/{id}/start

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()

Playwright (Python)

En la compilación publicada

El lado Python de lo mismo: lee el archivo de dirección sin nada ajeno a la biblioteca estándar.

Llama a /v1/profiles · /v1/profiles/{id}/start

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())

Selenium (Python)

En la compilación publicada

Selenium se acopla a un navegador que ya está en marcha, por la dirección del depurador, y toma el chromedriver al que apunta la respuesta: no se descarga nada ni se adivina nada.

Llama a /v1/profiles · /v1/profiles/{id}/start

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()

Selenium (JavaScript)

En la compilación publicada

Lo mismo para Node: tanto la dirección del depurador como la ruta a un chromedriver compatible salen de la respuesta del inicio.

Llama a /v1/profiles · /v1/profiles/{id}/start

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()

La descripción que una herramienta puede importar

El mismo contrato como archivo, en una dirección que no cambia: OpenAPI, en JSON, generado a partir de los datos que muestra esta página. Impórtalo en Postman o Insomnia, o genera un cliente a partir de él. Cada operación lleva en él la misma disponibilidad que la insignia de arriba.

openapi.json

La versión que la estrenó, y cada versión que la amplía, están en las novedades. Una pregunta sobre el contrato, o un valor de aquí que no coincide con lo que hace tu compilación: [email protected].