Інтеграція CRMMECH з вашою системою
Коли камера на СТО фіксує заїзд авто, CRMMECH надсилає подію на ваш веб-сервіс — щоб у вашій CRM чи 1С автоматично з'явилось замовлення-наряд. Ця сторінка — все, що потрібно, щоб підготувати сервіс, який прийматиме такі події.
POST /events → документ у CRM / 1СБазова адреса цього API:
Швидкий старт
- Реалізуйте дві точки на своєму сервері:
GET /healthіPOST /events— див. Ваш веб-сервіс. Сервер має бути доступний з інтернету по HTTPS. - Власник СТО в CRMMECH відкриває налаштування інтеграції, вказує базовий URL вашого сервісу (напр.
https://crm.example.com/crmmech) і отримує API-ключmk_…. Ключ показується один раз — збережіть його у своїй системі. - Перевірте ключ і з'єднання — у тест-консолі нижче або кнопкою «Перевірити з'єднання» в CRMMECH (вона викликає ваш
/health). - Надішліть тестову подію з консолі й переконайтесь, що у вашій системі з'явився документ, а повторне
vehicle.updatedз тим самимrecord_idоновлює його, а не створює новий. - Готово. Далі події йдуть автоматично при кожному заїзді авто на камеру.
Ваш веб-сервіс
Ви надаєте базовий URL, а ми звертаємось до двох шляхів відносно нього:
| Метод | URL | Для чого | Що відповісти |
|---|---|---|---|
| GET | {url}/health | Перевірка з'єднання (кнопка в CRMMECH, тест-консоль). Приходить із заголовком X-API-Key. | Будь-який 2xx, якщо ключ правильний; 401 — якщо ні. |
| POST | {url}/events | Доставка подій. JSON-тіло + заголовки з підписом. | 2xx — подію прийнято. Будь-що інше або відсутність відповіді за 10 с — ми повторимо пізніше. |
Вимоги
- HTTPS і публічна адреса. Адреси в локальних / приватних мережах (
localhost,10.x,192.168.xтощо) не приймаються. - Відповідь за 10 секунд. Якщо обробка довга — збережіть подію у свою чергу, одразу відповідайте
200і обробляйте фоново. - Без редіректів. Ми не переходимо за
301/302— вказуйте кінцеву адресу. - Ідемпотентність. Одна подія може прийти більше одного разу (повтори після збою). Ключ унікальності — поле
id(він же заголовокX-Mech-Event-Id). - Тіло відповіді ми не аналізуємо, лише зберігаємо перші 2000 символів у журналі — зручно повертати там свій номер документа, напр.
{"ok":true,"document":"ЗН-000123"}.
/health (GET) та /events (POST). Кореневий URL сервісу, напр. https://1c.example.com/base/hs/crmmech, і буде вашим базовим URL.Заголовки запиту
Кожен POST /events містить:
| Заголовок | Приклад | Опис |
|---|---|---|
X-API-Key | mk_CZm6sE…PJ6 | Ваш API-ключ. Мінімальна перевірка — порівняти з ключем, збереженим у вашій системі. |
X-Mech-Signature | sha256=5d41…9a2f | HMAC-SHA256 підпис тіла — див. перевірку підпису. |
X-Mech-Timestamp | 1790662803 | Unix-час відправки (секунди). Входить у підпис; відкидайте запити старші за 5 хв. |
X-Mech-Event | vehicle.arrived | Тип події — те саме, що поле type у тілі. |
X-Mech-Event-Id | 6f1c0c7e-7a0c-… | UUID події — те саме, що id у тілі. Однаковий для всіх повторів однієї події. |
Content-Type | application/json; charset=utf-8 | Тіло завжди JSON у UTF-8. |
User-Agent | CRMMECH-Webhooks/1.0 |
GET /health приходить лише з X-API-Key і User-Agent. Деякі сервери перетворюють назви заголовків у нижній регістр — порівнюйте без урахування регістру.
Перевірка підпису
Підпис гарантує, що запит надіслав саме CRMMECH і тіло не змінено дорогою:
X-Mech-Signature = "sha256=" + hex( HMAC_SHA256( key = API-ключ, message = X-Mech-Timestamp + "." + сире_тіло ) )
- Підписуйте сире тіло байт-у-байт, як воно прийшло, — не перезібраний JSON (порядок полів і пробіли зміняться, і підпис не зійдеться).
- Порівнюйте рядки функцією постійного часу (
hmac.compare_digest,hash_equals,timingSafeEqual). - Відкидайте запити, де
X-Mech-Timestampвідрізняється від поточного часу більше ніж на 300 с — захист від повторного відтворення. - Не виходить — перевірте свій код точкою
POST /api/v1/test/verify-signature.
X-API-Key через HTTPS достатньо, щоб почати. Перевірку підпису радимо додати, щойно інтеграція запрацює.# FastAPI. pip install fastapi uvicorn
import hashlib, hmac, time
from fastapi import FastAPI, HTTPException, Request
API_KEY = "mk_..." # ключ з CRMMECH — тримайте в конфігурації, не в коді
app = FastAPI()
def verified(request: Request, raw: bytes) -> bool:
h = request.headers
if not hmac.compare_digest(h.get("x-api-key", ""), API_KEY):
return False
ts = h.get("x-mech-timestamp", "")
if not ts.isdigit() or abs(time.time() - int(ts)) > 300:
return False
expected = "sha256=" + hmac.new(API_KEY.encode(), f"{ts}.".encode() + raw, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, h.get("x-mech-signature", ""))
@app.get("/health")
def health(request: Request):
if not hmac.compare_digest(request.headers.get("x-api-key", ""), API_KEY):
raise HTTPException(401)
return {"status": "ok"}
@app.post("/events")
async def events(request: Request):
raw = await request.body() # сире тіло — ДО парсингу JSON
if not verified(request, raw):
raise HTTPException(401, "invalid signature")
event = await request.json()
if already_processed(event["id"]): # ідемпотентність
return {"ok": True, "duplicate": True}
doc = upsert_order(event["type"], event["data"]) # див. розділ «Як обробляти»
mark_processed(event["id"])
return {"ok": True, "document": doc}<?php
// events.php (GET /health — аналогічно, лише перевірка X-API-Key)
$apiKey = getenv('CRMMECH_API_KEY'); // mk_...
$raw = file_get_contents('php://input'); // сире тіло
$key = $_SERVER['HTTP_X_API_KEY'] ?? '';
$ts = $_SERVER['HTTP_X_MECH_TIMESTAMP'] ?? '';
$sig = $_SERVER['HTTP_X_MECH_SIGNATURE'] ?? '';
$expected = 'sha256=' . hash_hmac('sha256', $ts . '.' . $raw, $apiKey);
$fresh = ctype_digit($ts) && abs(time() - (int)$ts) <= 300;
if (!hash_equals($apiKey, $key) || !$fresh || !hash_equals($expected, $sig)) {
http_response_code(401);
exit;
}
$event = json_decode($raw, true);
if (alreadyProcessed($event['id'])) { // ідемпотентність
echo json_encode(['ok' => true, 'duplicate' => true]);
exit;
}
$doc = upsertOrder($event['type'], $event['data']);
markProcessed($event['id']);
header('Content-Type: application/json');
echo json_encode(['ok' => true, 'document' => $doc]);// Express. npm i express
const express = require('express');
const crypto = require('crypto');
const API_KEY = process.env.CRMMECH_API_KEY; // mk_...
const app = express();
const safeEqual = (a, b) =>
a.length === b.length && crypto.timingSafeEqual(Buffer.from(a), Buffer.from(b));
app.get('/health', (req, res) =>
safeEqual(req.get('X-API-Key') || '', API_KEY) ? res.json({ status: 'ok' }) : res.sendStatus(401));
// express.raw — щоб отримати СИРЕ тіло, а не вже розпарсений JSON
app.post('/events', express.raw({ type: 'application/json' }), (req, res) => {
const ts = req.get('X-Mech-Timestamp') || '';
const expected = 'sha256=' + crypto.createHmac('sha256', API_KEY)
.update(ts + '.').update(req.body).digest('hex');
const fresh = /^\d+$/.test(ts) && Math.abs(Date.now() / 1000 - Number(ts)) <= 300;
if (!safeEqual(req.get('X-API-Key') || '', API_KEY) || !fresh ||
!safeEqual(req.get('X-Mech-Signature') || '', expected)) {
return res.sendStatus(401);
}
const event = JSON.parse(req.body.toString('utf8'));
if (alreadyProcessed(event.id)) return res.json({ ok: true, duplicate: true });
const doc = upsertOrder(event.type, event.data);
markProcessed(event.id);
res.json({ ok: true, document: doc });
});
app.listen(8080);// ASP.NET Core (Minimal API), .NET 6+
using System.Security.Cryptography;
using System.Text;
using System.Text.Json;
var apiKey = Environment.GetEnvironmentVariable("CRMMECH_API_KEY")!; // mk_...
var app = WebApplication.Create(args);
bool SafeEq(string a, string b) =>
CryptographicOperations.FixedTimeEquals(Encoding.UTF8.GetBytes(a), Encoding.UTF8.GetBytes(b));
app.MapGet("/health", (HttpRequest r) =>
SafeEq(r.Headers["X-API-Key"].ToString(), apiKey) ? Results.Ok(new { status = "ok" }) : Results.Unauthorized());
app.MapPost("/events", async (HttpRequest r) =>
{
using var ms = new MemoryStream();
await r.Body.CopyToAsync(ms);
var raw = ms.ToArray(); // сире тіло
var ts = r.Headers["X-Mech-Timestamp"].ToString();
using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(apiKey));
var mac = hmac.ComputeHash(Encoding.UTF8.GetBytes(ts + ".").Concat(raw).ToArray());
var expected = "sha256=" + Convert.ToHexString(mac).ToLowerInvariant();
var fresh = long.TryParse(ts, out var t) && Math.Abs(DateTimeOffset.UtcNow.ToUnixTimeSeconds() - t) <= 300;
if (!SafeEq(r.Headers["X-API-Key"].ToString(), apiKey) || !fresh ||
!SafeEq(r.Headers["X-Mech-Signature"].ToString(), expected))
return Results.Unauthorized();
var ev = JsonDocument.Parse(raw).RootElement;
var id = ev.GetProperty("id").GetString();
// ідемпотентність по id → upsert документа по data.record_id
return Results.Ok(new { ok = true });
});
app.Run();// Модуль HTTP-сервісу 1С. Шаблони: /health (GET → HealthGET), /events (POST → EventsPOST).
// Тут перевіряється X-API-Key (достатньо через HTTPS). HMAC-підпис у 1С можна додати пізніше.
Функция HealthGET(Запрос)
Если Заголовок(Запрос, "X-API-Key") <> КлючCRMMECH() Тогда
Возврат Новый HTTPСервисОтвет(401);
КонецЕсли;
Возврат ОтветJSON(200, "{""status"":""ok""}");
КонецФункции
Функция EventsPOST(Запрос)
Если Заголовок(Запрос, "X-API-Key") <> КлючCRMMECH() Тогда
Возврат Новый HTTPСервисОтвет(401);
КонецЕсли;
Чтение = Новый ЧтениеJSON;
Чтение.УстановитьСтроку(Запрос.ПолучитьТелоКакСтроку(КодировкаТекста.UTF8));
Событие = ПрочитатьJSON(Чтение, Истина); // Соответствие
Чтение.Закрыть();
ИдСобытия = Событие.Получить("id");
ТипСобытия = Событие.Получить("type");
Данные = Событие.Получить("data");
Если СобытиеУжеОбработано(ИдСобытия) Тогда // ідемпотентність
Возврат ОтветJSON(200, "{""ok"":true,""duplicate"":true}");
КонецЕсли;
Если ТипСобытия = "vehicle.arrived" Или ТипСобытия = "vehicle.updated" Тогда
// Шукаємо замовлення-наряд за Данные.Получить("record_id"):
// немає — створюємо, є — оновлюємо (статус, час останньої появи, фото).
ЗаписатьЗаездCRMMECH(ТипСобытия, Данные);
КонецЕсли;
ОтметитьСобытиеОбработанным(ИдСобытия);
Возврат ОтветJSON(200, "{""ok"":true}");
КонецФункции
Функция Заголовок(Запрос, Имя)
// Назви заголовків можуть прийти в іншому регістрі
Для Каждого КЗ Из Запрос.Заголовки Цикл
Если НРег(КЗ.Ключ) = НРег(Имя) Тогда
Возврат КЗ.Значение;
КонецЕсли;
КонецЦикла;
Возврат "";
КонецФункции
Функция ОтветJSON(Код, Текст)
Ответ = Новый HTTPСервисОтвет(Код);
Ответ.Заголовки.Вставить("Content-Type", "application/json; charset=utf-8");
Ответ.УстановитьТелоИзСтроки(Текст, КодировкаТекста.UTF8);
Возврат Ответ;
КонецФункцииФормат події
Усі події мають однакову «обгортку»; зміст залежить від type і лежить у data.
| Поле | Тип | Опис |
|---|---|---|
id | string (UUID) | Унікальний ідентифікатор події. Той самий при повторній доставці — використовуйте для ідемпотентності. |
type | string | vehicle.arrived або vehicle.updated. Невідомі типи просто підтверджуйте 200 — список може розширюватись. |
created_at | string (ISO 8601) | Коли подія сталася на СТО, з часовим поясом, напр. 2026-09-29T10:15:02+03:00. |
workshop_id | integer | ID СТО в CRMMECH. Корисно, якщо одна ваша система обслуговує кілька СТО. |
data | object | Дані події — див. нижче. |
test опц. | boolean | Є лише в тестових подіях із тестових точок (true). У бойових подіях поля немає. |
vehicle.arrived — новий заїзд
Камера розпізнала авто, і в CRMMECH створено новий запис заїзду. Створіть у своїй системі замовлення-наряд.
Поле data | Тип | Опис |
|---|---|---|
record_id | integer | Ключ зв'язку. ID запису заїзду в CRMMECH. Збережіть у своєму документі — за ним прийдуть оновлення. |
car_id | integer | Короткий номер картки заїзду (4 цифри), який бачать працівники СТО. |
status | string | Статус запису: new для нового заїзду. Див. статуси. |
type | string | Тип запису, зараз завжди visit. |
customer_id | integer | null | ID клієнта в CRMMECH, якщо авто вже закріплене за клієнтом; інакше null. |
photo_url | string | null | Фото номерного знака з камери (публічне посилання, JPEG). |
vehicle | object | Авто: |
id | integer | ID авто в CRMMECH. |
plate | string | Держномер латиницею без пробілів, напр. AA1234BB. Кириличні літери номера передаються латинськими двійниками. |
make | string | null | Марка (з держреєстру або бази СТО). |
model | string | null | Модель. |
year | integer | null | Рік випуску. |
vin | string | null | VIN-код, якщо відомий. |
color | string | null | Колір (з держреєстру). |
is_new | boolean | true — авто на цій СТО вперше. |
plate_recognized | string | Номер, як його прочитала камера. Може відрізнятись від vehicle.plate на 1–2 символи — довіряйте vehicle.plate. |
country | string | Формат номера: UA, EU, PL… або інший код. |
ocr_confidence | number | Впевненість розпізнавання, % (0–100). |
camera_index | string | Номер камери / поста на СТО. |
session_id | integer | ID сесії перебування авто в кадрі (службове). |
Завантаження прикладу…
vehicle.updated — авто знову в кадрі
Авто, запис якого ще відкритий, знову потрапило в кадр (стоїть на посту, переїхало на інший пост тощо). Оновіть наявний документ за record_id — новий не створюйте.
Надсилається не частіше ніж раз на 5 хвилин на один запис. Якщо документа з таким record_id у вас ще немає (авто заїхало до підключення інтеграції) — створіть його.
Поле data | Тип | Опис |
|---|---|---|
record_id | integer | Ключ пошуку вашого документа. |
car_id | integer | Короткий номер картки заїзду. |
status | string | Поточний статус запису — може змінитися з моменту заїзду (new → diagnostic → repair). |
customer_id | integer | null | ID клієнта в CRMMECH. |
entered_at | string | null | Коли авто з'явилося в кадрі в поточному перебуванні (ISO 8601). |
last_seen_at | string | Коли авто бачили востаннє (ISO 8601). Оновлюйте, лише якщо новіше за збережене — події можуть прийти не по порядку. |
on_site_min | integer | null | Скільки хвилин авто на СТО (від entered_at). |
photo_url | string | null | Свіже фото номерного знака. |
vehicle | object | id, plate, make, model, year — як у vehicle.arrived. |
plate_recognized, country, ocr_confidence, camera_index, session_id | Як у vehicle.arrived; camera_index — камера, яка бачила авто цього разу. |
Завантаження прикладу…
Статуси запису
| status | Значення |
|---|---|
new | Новий заїзд, роботи ще не почато. |
diagnostic | Діагностика. |
repair | У ремонті. |
complete / completed | Завершено. Після цього vehicle.updated по запису не надсилається; наступний заїзд того ж авто прийде новим vehicle.arrived з іншим record_id. |
Список статусів може розширюватись — невідомі значення зберігайте як є.
Як обробляти
Надійна схема — один документ на один record_id, «upsert» для обох типів:
при отриманні події:
якщо id вже оброблено → відповісти 200 (дубль), нічого не робити
документ = знайти за data.record_id
якщо документа немає → створити (номер, авто, клієнт, фото, дата = entered_at або created_at)
інакше (vehicle.updated) → оновити status, camera_index, photo_url, on_site_min;
last_seen_at — лише якщо новіше за збережене
запам'ятати id як оброблений
відповісти 200 (бажано з номером документа в тілі)record_id і workshop_id — за цією парою документ однозначно знаходиться, навіть якщо у вас кілька СТО.Повтори та таймаути
Якщо ваш сервіс не відповів 2xx за 10 секунд, подію буде надіслано повторно:
| Спроба | 1 | 2 | 3 | 4 | 5 | 6 |
|---|---|---|---|---|---|---|
| Коли | одразу | +1 хв | +5 хв | +15 хв | +1 год | +3 год |
Після 6 невдалих спроб подія отримує статус failed. Власник СТО бачить журнал доставки в CRMMECH і може надіслати подію повторно, а ви — забрати пропущені події через Pull API.
Порядок доставки не гарантується: після збою vehicle.updated може прийти раніше за vehicle.arrived того ж запису. Схема з розділу «Як обробляти» це враховує.
Pull API
Окрім вебхуків, історію подій можна забрати самостійно — наприклад, після простою вашого сервера. Авторизація — той самий X-API-Key.
| Метод | URL | Опис |
|---|---|---|
| GET | /api/v1/auth/check | Перевірити ключ. Повертає workshop_id і чи увімкнені вебхуки. |
| GET | /api/v1/events | Події від старих до нових. Параметри: after_id (id останньої обробленої події), type, status (pending / delivered / failed / cancelled), limit (1–200, за замовчуванням 50). У відповіді items і has_more. |
| GET | /api/v1/events/{id} | Одна подія за її id. |
curl "{BASE}/api/v1/events?after_id=6f1c0c7e-7a0c-4a8e-9a55-2d7c1f0b1e2a&limit=100" \
-H "X-API-Key: mk_..."Кожен елемент items містить payload — рівно те тіло, що приходить у вебхуку, — плюс delivery_status, delivery_attempts, delivered_at. Доставлені події зберігаються 90 днів.
Тестування
Тестові точки нічого не пишуть у журнал подій і не повторюються — одна спроба, у відповіді видно точний запит, який пішов на ваш сервіс, і що він відповів. Працюють, навіть якщо інтеграцію в CRMMECH вимкнено. Ліміт — 20 запитів на хвилину.
| Метод | URL | Тіло | Що робить |
|---|---|---|---|
| POST | /api/v1/test/webhook | {"type": "vehicle.arrived", "record_id": 777} | Надсилає тестову подію на {url}/events з реальними заголовками й підписом. record_id необов'язковий — передайте той самий в arrived і updated, щоб перевірити оновлення документа. |
| POST | /api/v1/test/health | — | Викликає ваш GET {url}/health. |
| POST | /api/v1/test/verify-signature | {"timestamp": "…", "body": "…", "signature": "sha256=…"} | Рахує підпис вашим ключем і порівнює з вашим. Повертає правильне значення і підказку, що не так. |
| GET | /api/v1/test/samples | — | Приклади тіл усіх подій. Без авторизації. |
curl -X POST "{BASE}/api/v1/test/webhook" \
-H "X-API-Key: mk_..." -H "Content-Type: application/json" \
-d '{"type": "vehicle.arrived", "record_id": 777}'Тест-консоль
Вставте свій API-ключ — запити підуть прямо звідси. Ключ нікуди не зберігається й живе лише в цій вкладці.
Типові помилки
| Симптом | Причина / що робити |
|---|---|
401 Invalid X-API-Key у Pull API / тест-консолі | Ключ скинули або відкликали в CRMMECH — старий більше не діє. Отримайте новий у власника СТО. |
| Підпис не сходиться | Ви підписуєте перезібраний JSON замість сирого тіла, або ваш фреймворк уже змінив тіло (middleware для JSON). Перевірте через /api/v1/test/verify-signature. |
| «URL вказує на внутрішню / приватну адресу» | Сервіс має бути доступний з інтернету за публічною адресою. |
| Таймаут (10 с) | Відповідайте одразу після збереження події, довгу обробку робіть фоново. |
HTTP 404 на /events або /health | У CRMMECH вказано не той базовий URL: ми самі дописуємо /events і /health — вказуйте URL без них. |
HTTP 301 / 302 | Редіректи не виконуються — вкажіть кінцеву адресу (часто http → https або слеш у кінці). |
| Дублікати документів | Не перевіряється id події, або документ шукається не за record_id. |
Питання щодо інтеграції — до вашого контакту в CRMMECH.