Інтеграція CRMMECH з вашою системою

Коли камера на СТО фіксує заїзд авто, CRMMECH надсилає подію на ваш веб-сервіс — щоб у вашій CRM чи 1С автоматично з'явилось замовлення-наряд. Ця сторінка — все, що потрібно, щоб підготувати сервіс, який прийматиме такі події.

Камера на СТОрозпізнає номер авто
→
CRMMECHстворює запис заїзду
→
CRMMECH Webhooksпідписує подію і доставляє, з повторами
→
Ваш веб-сервісPOST /events → документ у CRM / 1С

Базова адреса цього API:

Швидкий старт

  1. Реалізуйте дві точки на своєму сервері: GET /health і POST /events — див. Ваш веб-сервіс. Сервер має бути доступний з інтернету по HTTPS.
  2. Власник СТО в CRMMECH відкриває налаштування інтеграції, вказує базовий URL вашого сервісу (напр. https://crm.example.com/crmmech) і отримує API-ключ mk_…. Ключ показується один раз — збережіть його у своїй системі.
  3. Перевірте ключ і з'єднання — у тест-консолі нижче або кнопкою «Перевірити з'єднання» в CRMMECH (вона викликає ваш /health).
  4. Надішліть тестову подію з консолі й переконайтесь, що у вашій системі з'явився документ, а повторне vehicle.updated з тим самим record_id оновлює його, а не створює новий.
  5. Готово. Далі події йдуть автоматично при кожному заїзді авто на камеру.

Ваш веб-сервіс

Ви надаєте базовий 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"}.
1С: опублікуйте HTTP-сервіс на веб-сервері (Apache / IIS) з шаблонами /health (GET) та /events (POST). Кореневий URL сервісу, напр. https://1c.example.com/base/hs/crmmech, і буде вашим базовим URL.

Заголовки запиту

Кожен POST /events містить:

ЗаголовокПрикладОпис
X-API-Keymk_CZm6sE…PJ6Ваш API-ключ. Мінімальна перевірка — порівняти з ключем, збереженим у вашій системі.
X-Mech-Signaturesha256=5d41…9a2fHMAC-SHA256 підпис тіла — див. перевірку підпису.
X-Mech-Timestamp1790662803Unix-час відправки (секунди). Входить у підпис; відкидайте запити старші за 5 хв.
X-Mech-Eventvehicle.arrivedТип події — те саме, що поле type у тілі.
X-Mech-Event-Id6f1c0c7e-7a0c-…UUID події — те саме, що id у тілі. Однаковий для всіх повторів однієї події.
Content-Typeapplication/json; charset=utf-8Тіло завжди JSON у UTF-8.
User-AgentCRMMECH-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.

ПолеТипОпис
idstring (UUID)Унікальний ідентифікатор події. Той самий при повторній доставці — використовуйте для ідемпотентності.
typestringvehicle.arrived або vehicle.updated. Невідомі типи просто підтверджуйте 200 — список може розширюватись.
created_atstring (ISO 8601)Коли подія сталася на СТО, з часовим поясом, напр. 2026-09-29T10:15:02+03:00.
workshop_idintegerID СТО в CRMMECH. Корисно, якщо одна ваша система обслуговує кілька СТО.
dataobjectДані події — див. нижче.
test опц.booleanЄ лише в тестових подіях із тестових точок (true). У бойових подіях поля немає.

vehicle.arrived — новий заїзд

Камера розпізнала авто, і в CRMMECH створено новий запис заїзду. Створіть у своїй системі замовлення-наряд.

Поле dataТипОпис
record_idintegerКлюч зв'язку. ID запису заїзду в CRMMECH. Збережіть у своєму документі — за ним прийдуть оновлення.
car_idintegerКороткий номер картки заїзду (4 цифри), який бачать працівники СТО.
statusstringСтатус запису: new для нового заїзду. Див. статуси.
typestringТип запису, зараз завжди visit.
customer_idinteger | nullID клієнта в CRMMECH, якщо авто вже закріплене за клієнтом; інакше null.
photo_urlstring | nullФото номерного знака з камери (публічне посилання, JPEG).
vehicleobjectАвто:
idintegerID авто в CRMMECH.
platestringДержномер латиницею без пробілів, напр. AA1234BB. Кириличні літери номера передаються латинськими двійниками.
makestring | nullМарка (з держреєстру або бази СТО).
modelstring | nullМодель.
yearinteger | nullРік випуску.
vinstring | nullVIN-код, якщо відомий.
colorstring | nullКолір (з держреєстру).
is_newbooleantrue — авто на цій СТО вперше.
plate_recognizedstringНомер, як його прочитала камера. Може відрізнятись від vehicle.plate на 1–2 символи — довіряйте vehicle.plate.
countrystringФормат номера: UA, EU, PL… або інший код.
ocr_confidencenumberВпевненість розпізнавання, % (0–100).
camera_indexstringНомер камери / поста на СТО.
session_idintegerID сесії перебування авто в кадрі (службове).
Завантаження прикладу…

vehicle.updated — авто знову в кадрі

Авто, запис якого ще відкритий, знову потрапило в кадр (стоїть на посту, переїхало на інший пост тощо). Оновіть наявний документ за record_id — новий не створюйте.

Надсилається не частіше ніж раз на 5 хвилин на один запис. Якщо документа з таким record_id у вас ще немає (авто заїхало до підключення інтеграції) — створіть його.

Поле dataТипОпис
record_idintegerКлюч пошуку вашого документа.
car_idintegerКороткий номер картки заїзду.
statusstringПоточний статус запису — може змінитися з моменту заїзду (new → diagnostic → repair).
customer_idinteger | nullID клієнта в CRMMECH.
entered_atstring | nullКоли авто з'явилося в кадрі в поточному перебуванні (ISO 8601).
last_seen_atstringКоли авто бачили востаннє (ISO 8601). Оновлюйте, лише якщо новіше за збережене — події можуть прийти не по порядку.
on_site_mininteger | nullСкільки хвилин авто на СТО (від entered_at).
photo_urlstring | nullСвіже фото номерного знака.
vehicleobjectid, 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 секунд, подію буде надіслано повторно:

Спроба123456
Колиодразу+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.