Actions API — webhooks
Actions API надсилає події CarStream у ваші власні системи в реальному
часі. Щоразу, коли CarStream відправляє push-сповіщення на ваш телефон —
початок чи завершення поїздки, спрацювання створеного вами правила, перетин
геозони, нова помилка двигуна — та сама подія також доставляється HTTP
POST-запитом на кожен зареєстрований вами webhook.
Webhook-и спрацьовують для тієї ж аудиторії, що й push: власник авто та всі, кому авто розшарене, отримують доставки на свої власні зареєстровані хуки. Телефон чи зареєстрований push-токен не потрібні — webhook-и не залежать від мобільних пристроїв.
Автентифікація
Section titled “Автентифікація”Кожен запит автентифікується вашим персональним токеном Actions API у bearer-заголовку:
Authorization: Bearer cs_live_…- Токен береться зі сторінки API — він створюється під час вашого першого візиту.
- Токен безстроковий.
- Перегенерація (на тій самій сторінці) ротує його: старий токен одразу перестає працювати, а зареєстровані webhook-и не зачіпаються — доставки автентифікуються власним секретом підпису кожного хука, а не API-токеном.
Швидкий старт
Section titled “Швидкий старт”- Підніміть HTTPS-ендпоінт, який відповідає
200наPOST(для першого експерименту підійде URL з webhook.site). - Зареєструйте його:
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"}'- 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}- Збережіть
secret— він потрібен для перевірки підписів. Відтепер кожна подіяPOST-иться на ваш URL.
Ендпоінти
Section titled “Ендпоінти”Усі ендпоінти живуть під 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-ів.
Читання стану авто
Section titled “Читання стану авто”Webhook-и — це push-сторона; GET /cars — pull-сторона: той самий об’єкт
«усе, що ми знаємо» для кожного авто, яке бачить ваш акаунт (власні та
розшарені вам). Використовуйте його, щоб ініціалізувати стан інтеграції або
перечитати його після пропущеної доставки:
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 } } ]}Примітки до полів:
| Поле | Значення |
|---|---|
access | owner для ваших авто, 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} повертає той самий об’єкт для одного авто.
Конверт події
Section titled “Конверт події”Кожна доставка — один 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): вони приходять напряму від пристрою, який спостерігав
подію.
Каталог подій
Section titled “Каталог подій”trip.started
Section titled “trip.started”Авто почало рух. 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": {}}trip.ended
Section titled “trip.ended”Поїздка завершилась і її статистика порахована.
{ "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, якщо вона відома.
violation.speed
Section titled “violation.speed”Спрацювало створене вами правило ліміту швидкості. Доставляється лише авторові правила.
{ "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 }}violation.camera_speeding
Section titled “violation.camera_speeding”Авто проїхало повз камеру швидкості з перевищенням понад ваш поріг
(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 }}geofence.entered / geofence.exited
Section titled “geofence.entered / geofence.exited”Авто перетнуло межу геозони з вашого правила. 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.
dtc.detected
Section titled “dtc.detected”З’явилися нові діагностичні коди несправностей. Коди агрегуються за одне зчитування — одна подія може нести кілька пов’язаних кодів. Кожен код має 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.test
Section titled “webhook.test”Надсилається один раз під час реєстрації або повторного ввімкнення webhook-а —
це handshake, на який ваш ендпоінт має відповісти 2xx. Єдина подія, де
car — null.
{ "id": "evt_5c2f8e0a1db64c3f9a47b0e6d92c1f58", "type": "webhook.test", "created_at": "2026-08-27T18:20:00+00:00", "car": null, "data": { "message": "CarStream webhook verification" }}Доставки
Section titled “Доставки”Кожна подія — один POST на ваш URL:
| Заголовок | Значення |
|---|---|
Content-Type | application/json |
User-Agent | CarStream-Webhooks/1.0 |
X-CarStream-Event | Тип події, напр. trip.ended |
X-CarStream-Delivery | Унікальний id доставки (відрізняється від id події) |
X-CarStream-Signature | sha256=<hex HMAC-SHA256 від сирого тіла з ключем — секретом вашого хука> |
Правила доставки:
- Таймаут: 10 секунд. Відповідайте швидко — справжню роботу робіть асинхронно.
- Успіх: будь-який статус
2xx. Усе інше — включно з таймаутами та помилками з’єднання — рахується як невдача. - Редиректи не виконуються. Реєструйте кінцевий URL.
- Повторних спроб для події немає. Події йдуть потоком; пропущена
доставка — пропущена: перечитайте актуальний стан через
GET /cars, коли потрібно ресинхронізуватися.
Невдачі й автовимкнення
Section titled “Невдачі й автовимкнення”Кожен хук має лічильник невдач поспіль:
- невдала доставка збільшує його; успішна — скидає в нуль;
- на 3 невдачах поспіль хук автоматично вимикається і перестає отримувати події;
- увімкніть його знову на сторінці API
або через
POST /api/actions/webhooks/{id}/enable— обидва шляхи спершу повторюють handshakewebhook.test.
Відповідь GET /webhooks (і сторінка API) показує consecutive_failures,
disabled_reason, last_delivery_at і last_delivery_status — точно видно,
що сталося.
Перевірка підписів
Section titled “Перевірка підписів”Завжди перевіряйте 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 позначає доставку отриманоюimport crypto from "node:crypto";import express from "express";
const WEBHOOK_SECRET = "whsec_..."; // з відповіді на реєстрацію
const app = express();// Підпис покриває СИРЕ тіло — заберіть його до будь-якого JSON-парсингу.app.post("/carstream/hook", express.raw({ type: "*/*" }), (req, res) => { const digest = crypto .createHmac("sha256", WEBHOOK_SECRET) .update(req.body) .digest("hex"); const expected = Buffer.from(`sha256=${digest}`); const got = Buffer.from(req.get("X-CarStream-Signature") ?? ""); if (got.length !== expected.length || !crypto.timingSafeEqual(got, expected)) { return res.status(401).end(); }
const event = JSON.parse(req.body); if (event.type === "dtc.detected") { console.log(event.car.name, "повідомило", event.data.codes.join(", ")); } res.status(200).end(); // будь-який 2xx позначає доставку отриманою});app.listen(3000);Поради для приймача
Section titled “Поради для приймача”- Відповідайте
2xxодразу, а справжню роботу ставте в чергу. Повільний обробник з’їдає 10-секундне вікно і накопичує невдачі. - Перевіряйте підпис кожного запиту — ваш ендпоінт публічний.
- Дедуплікуйте за
idподії. Доставка — щонайбільше один раз на хук, але ваша власна інфраструктура (балансувальники, ретраї перед вашим застосунком) може дублювати запити. - Не покладайтеся на порядок. Події диспатчаться конкурентно;
якщо порядок важливий — використовуйте
created_at/fired_at. - Стежте за статусом хука на сторінці API
під час розробки —
last_delivery_statusпоказує точний HTTP-статус чи помилку, яку видав ваш ендпоінт.