Перейти до вмісту

Actions API — webhooks

Actions API надсилає події CarStream у ваші власні системи в реальному часі. Щоразу, коли CarStream відправляє push-сповіщення на ваш телефон — початок чи завершення поїздки, спрацювання створеного вами правила, перетин геозони, нова помилка двигуна — та сама подія також доставляється HTTP POST-запитом на кожен зареєстрований вами webhook.

Webhook-и спрацьовують для тієї ж аудиторії, що й push: власник авто та всі, кому авто розшарене, отримують доставки на свої власні зареєстровані хуки. Телефон чи зареєстрований push-токен не потрібні — webhook-и не залежать від мобільних пристроїв.

Кожен запит автентифікується вашим персональним токеном Actions API у bearer-заголовку:

Authorization: Bearer cs_live_…
  • Токен береться зі сторінки API — він створюється під час вашого першого візиту.
  • Токен безстроковий.
  • Перегенерація (на тій самій сторінці) ротує його: старий токен одразу перестає працювати, а зареєстровані webhook-и не зачіпаються — доставки автентифікуються власним секретом підпису кожного хука, а не API-токеном.
  1. Підніміть HTTPS-ендпоінт, який відповідає 200 на POST (для першого експерименту підійде URL з webhook.site).
  2. Зареєструйте його:
Terminal window
curl -X POST https://carstream.live/api/actions/webhooks \
-H "Authorization: Bearer cs_live_…" \
-H "Content-Type: application/json" \
-d '{"url": "https://example.com/carstream/hook"}'
  1. CarStream одразу надішле на URL підписану подію webhook.test. Якщо він відповість 2xx протягом 10 секунд, хук зареєстровано, а відповідь містить його секрет підпису:
{
"id": "3f8a1c2e-6b4d-4f0a-9c21-7d5e8a9b0c34",
"url": "https://example.com/carstream/hook",
"secret": "whsec_…",
"active": true,
"created_at": 1724769730.4
}
  1. Збережіть secret — він потрібен для перевірки підписів. Відтепер кожна подія POST-иться на ваш URL.

Усі ендпоінти живуть під https://carstream.live/api/actions і приймають заголовок Authorization: Bearer cs_live_….

МетодШляхЩо робить
POST/webhooksРеєструє URL. Тіло: {"url": "https://…"}. Проганяє handshake webhook.test; 422, якщо URL не проходить перевірку або не відповідає 2xx за 10 с. Повертає хук із його secret.
GET/webhooksСписок ваших хуків зі статусом: active, consecutive_failures, disabled_reason, last_delivery_at, last_delivery_status.
POST/webhooks/{id}/enableПовторно вмикає вимкнений хук. Спершу проганяє той самий handshake, тож досі зламаний приймач не повернеться в пул доставок.
DELETE/webhooks/{id}Видаляє хук. Доставки припиняються одразу.
GET/carsУсі видимі вам авто — все, що CarStream знає про кожне (див. Читання стану авто).
GET/cars/{id}Одне авто за id. 404 покриває і «не існує», і «не ваше».

URL webhook-а має задовольняти два правила:

  • схема http:// або https:// (для чогось справжнього — HTTPS);
  • ім’я хоста має резолвитися в публічну адресу — loopback, приватні (RFC 1918), link-local та інші зарезервовані діапазони відхиляються під час реєстрації.

Можна зареєструвати до 10 webhook-ів.

Webhook-и — це push-сторона; GET /cars — pull-сторона: той самий об’єкт «усе, що ми знаємо» для кожного авто, яке бачить ваш акаунт (власні та розшарені вам). Використовуйте його, щоб ініціалізувати стан інтеграції або перечитати його після пропущеної доставки:

Terminal window
curl https://carstream.live/api/actions/cars \
-H "Authorization: Bearer cs_live_…"
{
"cars": [
{
"id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55",
"name": "BMW 530d",
"access": "owner",
"created_at": 1712841600.0,
"vin": "WBAJC51090B330218",
"fuel_type": "diesel",
"obd_protocol": "ISO 15765-4 (CAN 11/500)",
"tank_size_liters": 66.0,
"vehicle_info": {
"make": "BMW",
"model": "530d",
"year": "2019",
"engine": "3.0L L6 DIESEL"
},
"device": {
"serial": "cs-a1b2c3d4",
"online": true,
"last_seen": 1724769730.4
},
"status": {
"fuel_percent": 62.5,
"position": {
"lat": 50.4501,
"lon": 30.5234,
"speed_kmh": 47.0,
"updated_at": 1724769728.9
},
"active_dtc": 1,
"active_dtc_codes": ["P0420"],
"mil": true
}
}
]
}

Примітки до полів:

ПолеЗначення
accessowner для ваших авто, viewer для розшарених вам.
vin, fuel_type, obd_protocolЗчитано з авто пристроєм; null до першої успішної ідентифікації.
vehicle_infoРозшифрований VIN (марка, модель, рік, двигун, …); null, якщо VIN не розшифровувався. Набір ключів залежить від того, що знає декодер.
deviceСпарений CarStream Unit; null, якщо авто зараз без пристрою. online — те саме 30-секундне правило свіжості, що й у застосунку.
status.fuel_percentОстаннє валідне значення пального (null, якщо авто ніколи його не передавало).
status.positionОстанній GPS-фікс зі швидкістю в той момент; null до першого фікса.
status.active_dtc_codesВідкриті зараз коди несправностей; mil — лампа check engine.

Часові позначки тут — Unix epoch у секундах, як усюди в Actions API поза created_at конверта події.

GET /cars/{id} повертає той самий об’єкт для одного авто.

Кожна доставка — один JSON-об’єкт:

{
"id": "evt_9f2c41d0a8b34c6d9e21f7a3b5d80c14",
"type": "trip.ended",
"created_at": "2026-08-27T17:42:10+00:00",
"car": { "id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55", "name": "BMW 530d" },
"data": { "…": "поля конкретної події" }
}
ПолеЗначення
idУнікальний id події (evt_ + 32 hex-символи). Використовуйте для ідемпотентності — той самий id обробляйте один раз.
typeТип події (каталог нижче). Дублюється в заголовку X-CarStream-Event, тож маршрутизувати можна без парсингу тіла.
created_atКоли подію випущено, ISO 8601, UTC.
carАвто, якого стосується подія — {id, name}. null лише для webhook.test.
dataПоля конкретної події, задокументовані нижче за типами.

Часові позначки всередині data (fired_at, detected_at) — це Unix epoch у секундах (float): вони приходять напряму від пристрою, який спостерігав подію.

Авто почало рух. data порожня — уся інформація в об’єкті car.

{
"id": "evt_9f2c41d0a8b34c6d9e21f7a3b5d80c14",
"type": "trip.started",
"created_at": "2026-08-27T17:10:22+00:00",
"car": { "id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55", "name": "BMW 530d" },
"data": {}
}

Поїздка завершилась і її статистика порахована.

{
"id": "evt_2c1e7a90bb614f0d8332ac54fd0e91aa",
"type": "trip.ended",
"created_at": "2026-08-27T17:42:10+00:00",
"car": { "id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55", "name": "BMW 530d" },
"data": {
"trip_id": 8412,
"distance_km": 23.4,
"max_speed_kmh": 92,
"avg_speed_kmh": 47,
"duration_min": 31.5,
"start_time": "2026-08-27T17:10:20+00:00",
"end_time": "2026-08-27T17:41:50+00:00"
}
}

trip_id (та будь-яка статистика) може бути null у рідкісному випадку, коли поїздку не вдалося зіставити з фіналізованим записом — подія все одно прийде, з duration_min, якщо вона відома.

Спрацювало створене вами правило ліміту швидкості. Доставляється лише авторові правила.

{
"id": "evt_77d0b2c94aa14b7f8e02cd13ef559b21",
"type": "violation.speed",
"created_at": "2026-08-27T17:20:31+00:00",
"car": { "id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55", "name": "BMW 530d" },
"data": {
"violation_id": 5121,
"rule_id": 18,
"rule_name": "Понад 90 у місті",
"violation_type": "speed",
"value": {
"speed": 96.4,
"threshold": 90,
"lat": 50.4501,
"lon": 30.5234
},
"fired_at": 1724769730.4
}
}

Авто проїхало повз камеру швидкості з перевищенням понад ваш поріг (speed_limit + заданий у правилі offset). Швидкість — у точці найближчого проходження, тобто та, яку зафіксував би радар.

{
"id": "evt_b8f4e1a2cd374d569910aa72c3e8f0d5",
"type": "violation.camera_speeding",
"created_at": "2026-08-27T17:25:03+00:00",
"car": { "id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55", "name": "BMW 530d" },
"data": {
"violation_id": 5122,
"rule_id": 21,
"rule_name": "Камери швидкості",
"violation_type": "camera_speeding",
"value": {
"speed": 84.0,
"threshold": 70.0,
"speed_limit": 50,
"offset": 20,
"cam_id": "UA-2214",
"cam_lat": 50.4477,
"cam_lon": 30.452,
"highway_class": "secondary",
"address": "просп. Перемоги, 57",
"lat": 50.4476,
"lon": 30.4514,
"dist_m": 42.5
},
"fired_at": 1724769730.4
}
}

Авто перетнуло межу геозони з вашого правила. value містить позицію авто, центр і радіус зони та відстань від центру в момент перетину.

{
"id": "evt_4a6c0d2e8bb14f77a3c9e51d20f6b843",
"type": "geofence.entered",
"created_at": "2026-08-27T18:02:44+00:00",
"car": { "id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55", "name": "BMW 530d" },
"data": {
"violation_id": 5123,
"rule_id": 12,
"rule_name": "Дім",
"violation_type": "geofence_enter",
"value": {
"lat": 50.4021,
"lon": 30.6521,
"fence_lat": 50.4025,
"fence_lon": 30.6512,
"radius_m": 250,
"distance_m": 180.3
},
"fired_at": 1724769730.4
}
}

geofence.exited ідентична, окрім type, violation_type: "geofence_exit" і distance_m, більшої за radius_m.

З’явилися нові діагностичні коди несправностей. Коди агрегуються за одне зчитування — одна подія може нести кілька пов’язаних кодів. Кожен код має 24-годинний кулдаун сповіщень, як і push.

{
"id": "evt_d1e9a4b7f2c94e0286f3ab15c07d8e46",
"type": "dtc.detected",
"created_at": "2026-08-27T18:15:09+00:00",
"car": { "id": "1b2f7c1e-93d4-4c88-b1a0-2f6e8d9a4c55", "name": "BMW 530d" },
"data": {
"codes": ["P0301", "P0420"],
"detected_at": 1724769730.4
}
}

Надсилається один раз під час реєстрації або повторного ввімкнення webhook-а — це handshake, на який ваш ендпоінт має відповісти 2xx. Єдина подія, де carnull.

{
"id": "evt_5c2f8e0a1db64c3f9a47b0e6d92c1f58",
"type": "webhook.test",
"created_at": "2026-08-27T18:20:00+00:00",
"car": null,
"data": { "message": "CarStream webhook verification" }
}

Кожна подія — один POST на ваш URL:

ЗаголовокЗначення
Content-Typeapplication/json
User-AgentCarStream-Webhooks/1.0
X-CarStream-EventТип події, напр. trip.ended
X-CarStream-DeliveryУнікальний id доставки (відрізняється від id події)
X-CarStream-Signaturesha256=<hex HMAC-SHA256 від сирого тіла з ключем — секретом вашого хука>

Правила доставки:

  • Таймаут: 10 секунд. Відповідайте швидко — справжню роботу робіть асинхронно.
  • Успіх: будь-який статус 2xx. Усе інше — включно з таймаутами та помилками з’єднання — рахується як невдача.
  • Редиректи не виконуються. Реєструйте кінцевий URL.
  • Повторних спроб для події немає. Події йдуть потоком; пропущена доставка — пропущена: перечитайте актуальний стан через GET /cars, коли потрібно ресинхронізуватися.

Невдачі й автовимкнення

Section titled “Невдачі й автовимкнення”

Кожен хук має лічильник невдач поспіль:

  • невдала доставка збільшує його; успішна — скидає в нуль;
  • на 3 невдачах поспіль хук автоматично вимикається і перестає отримувати події;
  • увімкніть його знову на сторінці API або через POST /api/actions/webhooks/{id}/enable — обидва шляхи спершу повторюють handshake webhook.test.

Відповідь GET /webhooks (і сторінка API) показує consecutive_failures, disabled_reason, last_delivery_at і last_delivery_status — точно видно, що сталося.

Завжди перевіряйте X-CarStream-Signature, перш ніж довіряти доставці: він доводить, що запит прийшов від CarStream і тіло не було підмінене. Обчисліть HMAC-SHA256 від сирих байтів запиту з ключем — вашим секретом whsec_ — і порівняйте порівнянням за сталий час.

import hashlib, hmac
from fastapi import FastAPI, Header, HTTPException, Request
WEBHOOK_SECRET = "whsec_..." # з відповіді на реєстрацію
app = FastAPI()
@app.post("/carstream/hook")
async def carstream_hook(
request: Request,
x_carstream_signature: str = Header(""),
x_carstream_event: str = Header(""),
):
body = await request.body()
digest = hmac.new(WEBHOOK_SECRET.encode(), body, hashlib.sha256).hexdigest()
if not hmac.compare_digest(x_carstream_signature, f"sha256={digest}"):
raise HTTPException(401, "bad signature")
event = await request.json()
if x_carstream_event == "trip.ended":
print(event["car"]["name"], "проїхало", event["data"]["distance_km"], "км")
return {"ok": True} # будь-який 2xx позначає доставку отриманою
  • Відповідайте 2xx одразу, а справжню роботу ставте в чергу. Повільний обробник з’їдає 10-секундне вікно і накопичує невдачі.
  • Перевіряйте підпис кожного запиту — ваш ендпоінт публічний.
  • Дедуплікуйте за id події. Доставка — щонайбільше один раз на хук, але ваша власна інфраструктура (балансувальники, ретраї перед вашим застосунком) може дублювати запити.
  • Не покладайтеся на порядок. Події диспатчаться конкурентно; якщо порядок важливий — використовуйте created_at / fired_at.
  • Стежте за статусом хука на сторінці API під час розробки — last_delivery_status показує точний HTTP-статус чи помилку, яку видав ваш ендпоінт.