Вебхуки
Получайте HTTPS POST, когда запуск браузерного агента начинает ждать человека или завершается, с подписью, по которой можно убедиться, что запрос пришёл от Lovely Router.
Вместо опроса передайте запуску URL в webhook. Lovely Router отправляет на него POST, когда запуск начинает ждать человека и когда он завершается. Каждый запрос подписан секретом, уникальным для этого запуска.
Вебхук для запуска#
При старте запуска передайте URL https в поле webhook. Ответ содержит webhook_secret. Он возвращается только здесь, поэтому сохраните его вместе с id запуска.
curl https://lovelyrouter.com/v1/browser/runs \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{
"goal": "Get a car insurance quote for the details below and tell me the quote reference and monthly premium. ...",
"start_url": "https://insure.example.com/quote",
"webhook": "https://api.example.com/hooks/browser"
}'Ответ в сокращённом виде:
{
"id": "brun_9c2e4a6b8d0f1a3c5e7b9d1f",
"object": "browser_run",
"status": "running",
"webhook_secret": "4f1d0c9b8a7e6d5c4b3a29180f7e6d5c4b3a2918a7b6c5d4",
"poll": "/v1/browser/runs/brun_9c2e4a6b8d0f1a3c5e7b9d1f?wait=30",
"charge_usd": null,
"reserved_usd": 0.445
}Если webhook не является URL https, возвращается 400 с "webhook" must be an https URL. Примеры на Python и Node.js используют настройку API, H и headers из раздела Запуск первой задачи.
События#
| Событие | Когда отправляется |
|---|---|
run.waiting | Запуск начинает ждать человека: вопрос, одобрение или передача управления. Отправляется один раз на каждую паузу. |
run.finished | Запуск завершается с любым итоговым статусом: succeeded, failed, timed_out или stopped. |
Содержимое запроса#
Каждое событие — это POST с JSON-телом, где указаны имя события и объект запуска на этот момент:
POST /hooks/browser HTTP/1.1
Host: api.example.com
content-type: application/json
x-lovely-event: run.waiting
x-lovely-signature: sha256=8d6f0c3b2a1e9f8d7c6b5a4f3e2d1c0b9a8f7e6d5c4b3a2f1e0d9c8b7a6f5e4d{
"event": "run.waiting",
"run": {
"id": "brun_9c2e4a6b8d0f1a3c5e7b9d1f",
"object": "browser_run",
"status": "waiting_for_human",
"goal": "Get a car insurance quote for the details below and tell me the quote reference and monthly premium. ...",
"start_url": "https://insure.example.com/quote",
"engine": "chromium",
"mode": "fast",
"model": null,
"steps": 14,
"max_steps": 50,
"billable_time_s": 0,
"time_usd": 0,
"current_url": "https://insure.example.com/quote/review",
"answer": null,
"data": null,
"error": null,
"needs": {
"kind": "approval",
"prompt": "Approve: click button \"Get my quote\" on Review your details?",
"fields": ["Full name: \"John Smith\"", "Coverage: \"Standard\""],
"action": "Get my quote",
"screenshot": "/v1/browser/runs/brun_9c2e4a6b8d0f1a3c5e7b9d1f/screenshot",
"since": "2026-10-11T09:41:03.000Z",
"reply": "/v1/browser/runs/brun_9c2e4a6b8d0f1a3c5e7b9d1f/reply"
},
"profile_saved": null,
"usage": { "decision_calls": 16, "llm_calls": 3 },
"created_at": "2026-10-11T09:40:21.000Z",
"finished_at": null
}
}В объекте запуска внутри вебхука нет charge_usd и reserved_usd: списание рассчитывается через несколько секунд после завершения запуска. Чтобы его узнать, запросите запуск через GET /v1/browser/runs/{id}.
Проверка подписи#
x-lovely-signature — это sha256=, за которым следует HMAC-SHA256 в шестнадцатеричном виде от сырого тела запроса, вычисленный с ключом webhook_secret запуска (как строкой UTF-8). Чтобы проверить запрос:
- Прочитайте сырое тело до разбора. Повторно сериализованный JSON не совпадёт.
- Найдите id запуска в теле (
run.id) и возьмитеwebhook_secret, который вы для него сохранили. Отклоняйте неизвестные id. - Вычислите HMAC и сравните его с заголовком сравнением за постоянное время.
import hashlib
import hmac
import json
from flask import Flask, abort, request
app = Flask(__name__)
@app.post("/hooks/browser")
def browser_hook():
body = request.get_data() # raw bytes
event = json.loads(body)
secret = load_secret(event["run"]["id"]) # what you stored at run creation
if secret is None:
abort(404)
expected = "sha256=" + hmac.new(secret.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(expected, request.headers.get("x-lovely-signature", "")):
abort(401)
run = event["run"]
if event["event"] == "run.waiting":
notify_person(run["id"], run["needs"]) # e.g. send needs.prompt to the right person
elif event["event"] == "run.finished":
save_result(run["id"], run["status"], run["answer"], run["data"])
return "", 204Доставка#
- Каждое событие отправляется один раз. Повторных попыток нет, и при ответе не из диапазона
2xxсобытие не отправляется заново. - Lovely Router ждёт вашего ответа до 5 секунд. Сразу отвечайте
2xx, а медленную работу, например уведомление человека, выполняйте в фоне. - События одного запуска отправляются по порядку, но относитесь к вебхуку как к сигналу, а не к источнику истины. Если событие потерялось, запуск никуда не делся: запросите его через
GET /v1/browser/runs/{id}.
Для задач, которые нельзя потерять, держите запасной вариант: периодически проверяйте запуски, которые спустя какое-то время всё ещё в статусе running или waiting_for_human. GET /v1/browser/runs возвращает 50 ваших последних запусков.
Ответ из вебхука#
Событие run.waiting содержит всё, что нужно, чтобы спросить человека: needs.prompt, needs.fields для одобрений, needs.live_view_url для передачи управления и путь needs.screenshot. Когда человек ответит, отправьте POST на needs.reply:
curl https://lovelyrouter.com/v1/browser/runs/brun_9c2e4a6b8d0f1a3c5e7b9d1f/reply \
-H "Authorization: Bearer $LOVELY_API_KEY" \
-H "content-type: application/json" \
-d '{"approve": true}'Запуск продолжится и по завершении отправит run.finished. Все виды ответов описаны в разделе Человек в контуре.