Проблемы начинаются в тот момент, когда система сталкивается с реальностью.
CRM временно недоступна. 1С отвечает 12 секунд вместо одной. Пользователь дважды нажимает кнопку оформления заказа. Один webhook приходит повторно. Второй приходит раньше первого. HTTP-запрос завершается по timeout, хотя удалённая система успела обработать операцию. Менеджер меняет телефон клиента в CRM одновременно с тем, как пользователь исправляет его в личном кабинете.
И тогда интеграция перестаёт быть задачей «соединить два API». Она становится задачей проектирования распределённой системы.
Разберём, как действительно происходит интеграция сайта с 1С, CRM и внешними API, какие архитектурные решения используются на практике и что необходимо предусмотреть, чтобы обмен данными продолжал работать после первых тысяч заказов, обновлений и сетевых ошибок.
Сначала нужно определить не API, а владельца данных
Одна из самых частых ошибок при разработке интеграции — сразу переходить к документации API.
Есть сайт. Есть CRM. Есть 1С. Значит, нужно просто настроить обмен.
Но сначала необходимо ответить на более важный вопрос:
какая система является источником истины для каждого типа данных?
Например:
| Данные | Основная система |
|---|---|
| карточка товара | 1С |
| остаток товара | 1С |
| базовая цена | 1С |
| пользователь сайта | сайт |
| лид | CRM |
| история общения с клиентом | CRM |
| заказ | сайт или CRM — зависит от архитектуры |
| бухгалтерский документ | 1С |
| платёж | платёжная система |
| статус доставки | служба доставки |
| маркетинговые согласия | сайт или CRM |
Это кажется формальностью, пока одно и то же поле не начинают редактировать сразу две системы.
Представим, что название организации клиента можно изменить и в CRM, и в личном кабинете.
В 12:00 менеджер изменил:
ООО Альфана:
ООО Альфа ТрейдА в 12:00:03 пользователь сохранил старое название из открытой ранее формы.
Если архитектура определяет синхронизацию как «последняя запись побеждает», CRM неожиданно получит старые данные.
Поэтому качественная интеграция начинается с определения ответственности систем, а уже затем — с выбора REST API, webhook, очереди или другого транспорта.
Типичная архитектура: сайт, CRM и 1С отвечают за разные задачи
Рассмотрим интернет-магазин или B2B-сервис.
Сайт отвечает за пользовательский интерфейс: регистрацию, корзину, оформление заказа, личный кабинет.
CRM отвечает за отношения с клиентом: лиды, сделки, менеджеров, коммуникации, этапы продаж.
1С хранит товарный учёт, бухгалтерские и складские данные.
Внешние сервисы могут отвечать за платежи, доставку, SMS, email или проверку реквизитов.
Получается уже не линейная цепочка, а сеть:
┌──────────────┐
│ Платёжный API│
└───────┬──────┘
│
▼
┌──────────┐ ┌─────────────────────┐ ┌────────────┐
│ Сайт │◄─────►│ Интеграционный слой│◄─────►│ CRM │
└────┬─────┘ └─────────┬───────────┘ └────────────┘
│ │
│ ▼
│ ┌───────────┐
└────────────────►│ 1С │
└───────────┘Интеграционный слой здесь необязателен для маленького проекта. Но по мере роста количества систем он сильно упрощает архитектуру.
Без него сайт постепенно начинает содержать код вроде:
sendToCRM()
sendTo1C()
createPayment()
updateDelivery()
notifyTelegram()
sendEmail()А затем каждая из этих операций обзаводится собственными retry, timeout, авторизацией, логированием и обработкой ошибок.
Через несколько лет изменение одной интеграции начинает затрагивать половину приложения.
Как сайт может взаимодействовать с 1С
У платформы 1С есть несколько механизмов интеграции.
В частности, платформа позволяет публиковать автоматический REST-интерфейс на основе OData, через который внешние приложения могут читать, создавать и изменять данные. Также можно создавать собственные HTTP-сервисы и самостоятельно определять маршруты, структуру запросов и формат ответов.
Это даёт два принципиально разных подхода.
В первом случае сайт работает с достаточно универсальным интерфейсом объектов 1С.
Например:
GET /odata/.../Catalog_Товары
POST /odata/.../Document_ЗаказПокупателяВо втором в 1С создаётся специализированный HTTP API:
GET /api/v1/products/123
POST /api/v1/orders
POST /api/v1/orders/984/cancelДля сложного продукта второй вариант нередко оказывается удобнее.
Причина проста: внешний сайт обычно не должен знать внутреннее устройство конфигурации 1С.
Сегодня заказ внутри 1С хранится одним способом, а завтра конфигурацию обновили или переработали. Если внешний сайт напрямую зависит от структуры внутренних справочников и документов, изменения начинают распространяться наружу.
Специализированный API позволяет создать стабильный контракт.
Сайт знает:
{
"order_id": "WR-10452",
"customer": {
"name": "ООО Альфа",
"inn": "7700000000"
},
"items": [
{
"sku": "A-135",
"quantity": 3,
"price": 1490
}
]
}А каким образом этот объект превращается в документы и регистры внутри 1С — ответственность самой интеграции.
Платформа 1С также поддерживает JSON при работе с HTTP-интерфейсами, поэтому такой формат обмена является вполне естественным для современных веб-приложений.
Как происходит интеграция сайта с CRM
С CRM ситуация похожая.
Большинство современных CRM предоставляют REST API, webhooks или оба механизма.
REST API используется, когда одна система сама инициирует операцию:
сайт → CRMНапример:
создать контакт
создать лид
создать сделку
изменить стадию
добавить комментарийWebhook работает в противоположном направлении:
CRM → сайтНапример, менеджер перевёл сделку в статус «Согласовано», после чего CRM отправила событие сайту.
Получается двусторонняя интеграция:
REST API
┌────────────────────────►
┌───────┴─────┐ ┌──────────┐
│ Сайт │ │ CRM │
└───────▲─────┘ └────┬─────┘
│ │
◄─────────────────────────┘
WebhookИменно здесь появляется один из главных принципов интеграционной архитектуры:
команда и событие — не одно и то же.
Команда:
Создай сделку.Событие:
Сделка создана.Если смешивать эти понятия, интеграция быстро становится трудно управляемой.
Разберём жизненный цикл одного заказа
Представим, что пользователь оформляет заказ №10452.
Сайт сначала сохраняет заказ в собственной базе:
order_id = 10452
status = created
integration_status = pendingЭто важный момент.
Плохая архитектура выглядит так:
Пользователь нажал «Оформить»
↓
сайт вызывает CRM
↓
сайт вызывает 1С
↓
если всё успешно — сохраняет заказЕсли 1С в этот момент недоступна, пользователь может получить ошибку, хотя сам заказ вполне мог быть принят CRM.
Гораздо устойчивее другой вариант:
Пользователь
↓
Сайт
↓
Локальная транзакция
├── сохранить заказ
└── создать событие интеграции
↓
очередь
┌─────┴─────┐
↓ ↓
CRM 1СПользовательский запрос заканчивается после того, как приложение надёжно сохранило заказ.
Передача во внешние системы может выполняться отдельно.
Так недоступность CRM не превращается автоматически в недоступность интернет-магазина.
Зачем интеграции очередь
Для небольшого сайта допустимо сразу вызывать внешний API:
await crm.createDeal(order);Но у такого решения появляется неприятное свойство: производительность сайта теперь зависит от чужой системы.
Если CRM отвечает 300 миллисекунд — всё хорошо.
Если CRM отвечает восемь секунд — пользователь ждёт восемь секунд.
Если CRM не отвечает — пользователь получает ошибку.
Если система использует очередь, сценарий меняется:
const order = await db.createOrder(data);
await queue.publish({
type: "order.created",
orderId: order.id
});
return order;А отдельный worker выполняет интеграцию:
async function processOrderCreated(event) {
const order = await getOrder(event.orderId);
await sendOrderToCRM(order);
await sendOrderTo1C(order);
}Появляется буфер между системами.
Даже если 1С временно недоступна, события остаются в очереди и могут быть обработаны позже.
Для небольшой системы вместо отдельного RabbitMQ или Kafka вовсе не обязательно сразу вводить сложную инфраструктуру. В качестве очереди иногда достаточно PostgreSQL и отдельной таблицы задач.
Например:
CREATE TABLE integration_jobs (
id UUID PRIMARY KEY,
type TEXT NOT NULL,
payload JSONB NOT NULL,
status TEXT NOT NULL,
attempts INTEGER NOT NULL DEFAULT 0,
next_attempt_at TIMESTAMPTZ,
created_at TIMESTAMPTZ NOT NULL DEFAULT NOW()
);Главное здесь не конкретная технология.
Главное — разделить пользовательскую операцию и потенциально ненадёжное взаимодействие с внешней системой.
Timeout не означает, что операция не выполнилась
Это одна из самых неприятных особенностей API-интеграций.
Представим запрос:
POST /crm/dealsСайт отправляет его в CRM.
CRM создаёт сделку.
Но ответ по сети не успевает вернуться за установленный timeout.
Для сайта результат выглядит так:
TimeoutErrorНо в CRM сделка уже существует.
Worker считает операцию неудачной и повторяет запрос.
В результате появляется вторая сделка.
Поэтому нельзя строить retry по принципу:
try {
await createDeal();
} catch {
await createDeal();
}Повторная отправка должна быть безопасной.
И здесь появляется идемпотентность.
Идемпотентность защищает систему от дублей
Каждой бизнес-операции можно присвоить уникальный идентификатор:
operation_id = order:create:10452И передавать его вместе с запросом:
POST /api/orders
Idempotency-Key: order:create:10452Получатель сохраняет обработанный ключ.
Первый запрос:
order:create:10452
→ заказ созданПовторный запрос:
order:create:10452
→ операция уже выполнялась
→ вернуть прежний результатТогда повтор после timeout перестаёт создавать дубли.
Если внешнее API не поддерживает Idempotency-Key, аналогичный механизм иногда приходится реализовывать на уровне интеграционного слоя.
Например, хранить соответствие:
website_order_id = 10452
crm_deal_id = 83951
1c_document_id = ...Перед созданием новой сущности система проверяет, не была ли она уже создана ранее.
Это особенно важно для заказов, платежей, счетов, документов и любых операций с финансовыми последствиями.
Webhook тоже может прийти дважды
Распространённая ошибка — считать webhook гарантированно уникальным.
На практике отправитель может повторить событие, если не получил подтверждение.
Поэтому обработчик вида:
app.post("/webhooks/crm", async (req, res) => {
await updateOrder(req.body);
res.sendStatus(200);
});слишком наивен.
Более устойчивый подход:
app.post("/webhooks/crm", async (req, res) => {
verifySignature(req);
const event = req.body;
const exists = await db.integrationEvents.find(event.id);
if (exists) {
return res.sendStatus(200);
}
await db.transaction(async tx => {
await tx.integrationEvents.insert({
id: event.id,
payload: event
});
await tx.jobs.insert({
type: "crm.webhook",
payload: event
});
});
res.sendStatus(200);
});HTTP-обработчик выполняет минимум работы: проверяет запрос, сохраняет событие и быстро отвечает.
Тяжёлая бизнес-логика выполняется отдельно.
Это уменьшает риск, что webhook будет отправляться повторно просто потому, что обработчик слишком долго работал.
А если события пришли в неправильном порядке?
Такое тоже возможно.
Например:
12:00:05 — заказ оплачен
12:00:06 — заказ созданС точки зрения бизнеса это невозможно.
С точки зрения распределённой сети — вполне.
Первое событие могло пройти более быстрым маршрутом, а второе задержалось в очереди.
Поэтому состояние объекта нельзя всегда менять без проверки.
Вместо:
order.status = event.status;лучше иметь правила переходов:
created
↓
awaiting_payment
↓
paid
↓
processing
↓
shipped
↓
completedИ отдельно разрешённые исключения:
awaiting_payment → cancelled
paid → refund_pending
refund_pending → refundedТогда событие:
completed → createdне откатит заказ назад только потому, что пришло позже.
По сути здесь начинает работать state machine — конечный автомат состояний.
Что делать, если сайт и CRM изменили данные одновременно
Синхронизация становится ещё сложнее, когда обмен двусторонний.
Предположим:
сайт → CRM
CRM → сайтПользователь меняет телефон на сайте.
Сайт отправляет изменение CRM.
CRM сохраняет телефон и отправляет webhook:
contact.updatedСайт получает его и снова обновляет пользователя.
Если после каждого изменения сайт опять отправляет событие CRM, может возникнуть цикл:
сайт
↓
CRM
↓
сайт
↓
CRM
↓
...Чтобы такого не происходило, изменения должны иметь источник.
Например:
{
"customer_id": 7281,
"phone": "+7...",
"source": "website",
"operation_id": "01J..."
}При получении события система видит:
source = websiteи понимает, что это отражение собственного изменения, которое не нужно отправлять назад.
Для более сложных систем дополнительно используют версии:
{
"customer_id": 7281,
"version": 17
}Если уже сохранена версия 18, событие версии 17 считается устаревшим.
Не все данные нужно синхронизировать мгновенно
Иногда в требованиях появляется фраза:
Все данные между сайтом и 1С должны синхронизироваться в реальном времени.
Звучит хорошо, но далеко не всегда действительно необходимо.
Статус оплаты желательно получить почти мгновенно.
Изменение заказа — тоже.
Но каталог из 150 000 товаров совсем не обязательно каждый раз запрашивать непосредственно из 1С при открытии страницы пользователем.
Схема:
Пользователь → сайт → 1С → сайт → пользовательсоздаёт сильную связанность.
Если 1С недоступна, вместе с ней оказывается недоступен каталог сайта.
Гораздо практичнее:
1С
↓
синхронизация
↓
локальный каталог сайта
↓
пользовательСайт получает данные периодически или по событиям и обслуживает пользователей из собственной оптимизированной базы.
Например, полная синхронизация выполняется ночью, а изменения цен и остатков передаются инкрементально в течение дня.
Полная и инкрементальная синхронизация
Полная синхронизация означает:
выгрузить все товары
сравнить все товары
обновить всё состояниеПри небольшом каталоге это допустимо.
Но при сотнях тысяч записей такая операция становится дорогой.
Поэтому обычно появляется инкрементальная синхронизация:
дай все объекты, изменённые после 14:35:00или:
дай события начиная с cursor=839245Например:
GET /products/changes?after=2026-10-04T14:35:00ZНо работа только со временем тоже имеет недостатки: часы систем могут различаться, записи могут появиться с задержкой.
Надёжнее использовать последовательный cursor или версию:
10091
10092
10093
10094Клиент запоминает:
last_cursor = 10094и при следующем обмене запрашивает:
cursor > 10094Так значительно проще понять, что ничего не пропущено.
Сопоставление данных — отдельная часть интеграции
Сайт может называть сущность:
customerCRM:
contact1С:
КонтрагентЭто вовсе не означает, что структуры этих объектов совпадают.
На сайте:
{
"name": "Иван Петров",
"email": "user@example.com"
}В CRM:
{
"first_name": "Иван",
"last_name": "Петров",
"email": [
{
"value": "user@example.com",
"type": "WORK"
}
]
}Поэтому между системами обычно существует mapping layer:
WebsiteCustomer
↓
CustomerMapper
↓
CRMContactХорошая архитектура не позволяет форматам внешних API распространяться по всему приложению.
Плохой вариант:
const email =
crmContact.custom_fields_values[4].values[0].value;и такой код находится в двадцати файлах.
Лучше:
const customer = crmMapper.toCustomer(crmContact);Если CRM изменит структуру API, потребуется исправить один адаптер, а не весь продукт.
Интеграционный слой защищает бизнес-логику
По мере роста проекта полезно отделять внешние системы адаптерами:
┌── CRM Adapter
│
Business Logic ─────┼── 1C Adapter
│
├── Payment Adapter
│
└── Delivery AdapterБизнес-логика говорит:
await crm.createDeal(order);а не:
await axios.post(
`${config.crmHost}/rest/...`,
createVerySpecificPayload(...)
);То же относится к 1С.
Приложение должно оперировать понятиями:
Order
Customer
Product
Invoice
Paymentа не внутренними HTTP-маршрутами конкретного поставщика.
Это становится особенно полезно, когда через два года компания меняет CRM.
Если интеграция изолирована, меняется один адаптер.
Если API CRM проникло во всю кодовую базу — фактически приходится переписывать продукт.
Retry должен быть управляемым
При временной ошибке повтор запроса необходим.
Но бесконечный цикл:
ошибка
↓
повтор
↓
ошибка
↓
повторможет окончательно положить восстанавливающийся сервис.
Поэтому применяется задержка между попытками.
Например:
1-я ошибка → повтор через 5 секунд
2-я ошибка → через 30 секунд
3-я ошибка → через 2 минуты
4-я ошибка → через 10 минут
5-я ошибка → через часЭто называется exponential backoff.
Если система вернула:
400 Bad Requestповторять тот же запрос обычно бессмысленно: данные необходимо исправить.
При:
429 Too Many Requestsнеобходимо учитывать ограничение API.
При:
502 Bad Gateway
503 Service Unavailable
504 Gateway Timeoutповтор, как правило, вполне разумен.
Поэтому retry — не просто attempts++.
У системы должна быть классификация ошибок.
Нужна очередь «неразобранных» событий
Представим событие, которое не удалось обработать и после десяти попыток.
Удалять его нельзя.
Бесконечно повторять тоже нельзя.
Для этого используется Dead Letter Queue, или логически аналогичный механизм.
Получается:
обычная очередь
↓
несколько попыток
↓
не удалось обработать
↓
Dead Letter QueueАдминистратор может увидеть:
order.created
order_id: 10452
attempts: 10
error: CRM returned 422Исправить причину и запустить обработку повторно.
Без такого механизма проблемы интеграции часто обнаруживаются иначе:
Почему заказ клиента от прошлой среды так и не появился в 1С?
После чего разработчик начинает искать его среди гигабайтов логов.
Логи должны отвечать на бизнес-вопрос
Обычный лог:
Request failed with status 500почти бесполезен.
Хороший лог:
{
"timestamp": "2026-10-04T10:18:41Z",
"service": "integration-worker",
"operation": "crm.create_deal",
"order_id": "10452",
"correlation_id": "01K...",
"attempt": 3,
"duration_ms": 1842,
"status": "failed",
"http_status": 503
}Теперь можно ответить на вопрос:
Что происходило с заказом 10452?
По correlation_id можно проследить весь путь:
сайт
↓
order.created
↓
CRM
↓
1С
↓
платёж
↓
доставкаЭто называется distributed tracing, хотя даже без полноценной tracing-платформы единый correlation ID значительно упрощает поддержку.
Мониторить нужно не только ошибки
Интеграция может работать без ошибок и при этом быть неисправной.
Например:
очередь: 38 426 событий
ошибок: 0Worker работает.
Но обрабатывает 10 событий в минуту, а приходит 100.
Формально всё «зелёное».
Фактически задержка синхронизации уже составляет несколько часов.
Поэтому для интеграций полезно измерять не только количество ошибок, но и:
размер очереди
возраст самого старого события
среднее время обработки
процент успешных операций
количество retry
число событий в DLQ
время ответа 1С
время ответа CRMОсобенно показательный показатель:
oldest_pending_event_ageЕсли самому старому необработанному заказу 43 секунды — система, скорее всего, работает нормально.
Если ему 6 часов — проблема очевидна даже при нулевом количестве HTTP 500.
Outbox решает ещё одну неприятную проблему
Рассмотрим код:
await db.createOrder(order);
await queue.publish("order.created", order.id);Между этими двумя строками процесс может аварийно завершиться.
Тогда заказ сохранён:
orders: естьа событие:
queue: отсутствуетCRM никогда о нём не узнает.
Если поменять операции местами:
await queue.publish(...);
await db.createOrder(...);возникает обратная ситуация: событие отправлено, а заказ сохранить не удалось.
Один из способов решения — Transactional Outbox.
В той же транзакции базы данных сохраняются и заказ, и событие:
BEGIN
INSERT INTO orders ...
INSERT INTO outbox_events ...
COMMITОтдельный publisher читает outbox_events и отправляет записи в очередь.
Теперь невозможно получить ситуацию, когда бизнес-операция зафиксирована, а информация о необходимости интеграции потеряна из-за падения процесса между двумя независимыми действиями.
Для критических интеграций это гораздо надёжнее простого вызова очереди после COMMIT.
Exactly once обычно превращается в at least once
В интеграционных системах часто хочется гарантировать:
каждое событие будет выполнено ровно один раз.
На практике добиться настоящего exactly once между независимыми системами сложно и дорого.
Поэтому архитектура обычно строится вокруг другой модели:
at least once delivery
+
idempotent processingТо есть событие гарантированно доставляется как минимум один раз и иногда может прийти повторно.
Но его повторная обработка безопасна.
Это намного реалистичнее, чем пытаться сделать сеть, несколько баз данных и несколько внешних API одной огромной транзакцией.
Безопасность интеграции
Интеграционный API часто имеет значительно больше полномочий, чем обычный пользователь сайта.
Поэтому ключ API нельзя просто добавить во frontend:
const API_KEY = "...";Всё взаимодействие с 1С, CRM и внутренними сервисами должно выполняться сервером.
Секреты хранятся в защищённой конфигурации окружения или специализированном хранилище секретов.
Для webhook желательно проверять подпись запроса.
Например:
X-Signature: ...Сервер самостоятельно вычисляет HMAC от тела запроса и сравнивает результат.
Необходимо также ограничивать права интеграционной учётной записи. Если сайту требуется только создание заказов, ему не нужен административный доступ ко всей 1С или CRM.
Принцип простой:
интеграция должна получать минимально необходимые полномочия.
Дополнительно нужны HTTPS, ротация ключей, журналирование административных операций и защита от повторного воспроизведения запросов там, где это критично.
Версионирование API тоже необходимо продумать заранее
Сегодня сайт отправляет:
{
"name": "Иван",
"phone": "+7..."
}Через два года потребуется:
{
"name": {
"first": "Иван",
"last": "Петров"
},
"phones": [
{
"value": "+7...",
"type": "mobile"
}
]
}Нельзя просто поменять контракт и надеяться, что все подключённые системы обновятся одновременно.
Поэтому публичный или межсервисный API желательно проектировать с учётом совместимости.
Например:
/api/v1/orders
/api/v2/ordersили использовать эволюцию схемы без разрушения старых полей.
Особенно опасны незаметные изменения смысла поля.
Если вчера:
price = цена за единицуа завтра:
price = стоимость всей позицииформально JSON остаётся валидным.
Но результат оказывается гораздо опаснее обычной ошибки 500.
Как тестировать интеграцию
Проверки вида:
нажал кнопку → запись появилась в CRMнедостаточно.
Нужно проверять не только happy path.
Что произойдёт, если CRM недоступна 20 минут?
Если запрос завершился timeout?
Если webhook пришёл три раза?
Если 1С вернула некорректный JSON?
Если события поменялись местами?
Если worker упал после выполнения операции, но до подтверждения сообщения?
Если товар удалён в 1С?
Если пользователь изменил заказ одновременно с менеджером?
Если API начал возвращать 429?
Если access token истёк?
Если была развёрнута новая версия сайта во время обработки очереди?
Именно такие сценарии определяют качество интеграции.
В production пользователи довольно быстро найдут состояния, которых не было в демонстрационном сценарии разработчика.
Контрактные тесты защищают от неожиданного изменения API
Интеграция может сломаться даже без единого изменения на сайте.
Например, сторонняя система изменила:
{
"id": 128
}на:
{
"id": "128"
}или перестала возвращать необязательное поле, которое разработчики сайта фактически считали обязательным.
Поэтому полезны contract tests.
Они проверяют не весь бизнес-процесс, а соглашение между системами:
какие поля обязательны
какие типы данных допустимы
какие HTTP-коды возможны
какие события существуют
какие версии API поддерживаютсяЕсли одна сторона меняет контракт, проблема обнаруживается до production.
Нужна ли отдельная интеграционная платформа
Не всегда.
Для простого сайта с одной CRM вполне может быть достаточно:
backend
+
REST API
+
webhook
+
таблица integration_jobsСтроить Kafka-кластер только ради передачи пяти лидов в день бессмысленно.
Но если система соединяет:
сайт
CRM
1С
платёжный сервис
склад
службу доставки
email
SMS
аналитику
партнёрский APIвыделение интеграционного слоя становится всё более оправданным.
Главный критерий — не количество модных технологий.
Главный критерий — можно ли ответить на четыре вопроса:
Где сейчас находится конкретный заказ?
Какая система владеет его текущим состоянием?
Что произойдёт, если одна из систем станет недоступна?
Можно ли безопасно повторить операцию?
Если на эти вопросы нет точного ответа, интеграция уже начинает превращаться в набор неявных связей.
Как выглядит здоровая интеграция
Возьмём тот же заказ №10452.
Пользователь оформляет его на сайте.
Сайт открывает транзакцию и сохраняет:
order = 10452
status = createdОдновременно создаётся outbox-событие:
order.createdТранзакция завершается.
Publisher передаёт событие worker.
Worker создаёт сделку в CRM с идемпотентным идентификатором:
website-order-10452CRM отвечает:
deal_id = 83951Связь сохраняется:
order 10452 ↔ crm deal 83951Следующий обработчик передаёт заказ в 1С.
Если 1С временно недоступна, заказ остаётся в очереди.
Через несколько минут retry выполняется успешно.
1С возвращает идентификатор документа.
Система сохраняет:
order 10452
crm 83951
1c 65d8...Когда оплата проходит, платёжный сервис отправляет webhook.
Webhook проверяется, сохраняется и передаётся в очередь.
Статус заказа становится:
paidCRM и 1С получают соответствующие события.
Каждая операция имеет correlation ID.
Если через месяц возникнет спор по заказу, можно восстановить полный маршрут его обработки.
Это уже не просто «связали сайт с CRM».
Это управляемая интеграционная архитектура.
Главная ошибка — считать интеграцию передачей JSON
Сам JSON обычно является самой простой частью задачи.
Сложность появляется в других вопросах:
кто владеет данными;
как определять дубли;
что делать при timeout;
как повторять операции;
как восстанавливать обмен после сбоя;
что делать с устаревшими событиями;
как изменять контракт API;
как найти потерянную операцию;
как синхронизировать несколько источников;
как не сделать внешний сервис единой точкой отказа.Именно ответы на эти вопросы отличают демонстрационную интеграцию от production-системы.
Вместо заключения
Интеграция сайта с 1С, CRM или внешним API начинается не с HTTP-запроса.
Она начинается с проектирования границ систем.
Нужно определить владельцев данных, контракты API, направление синхронизации, правила разрешения конфликтов и допустимые состояния объектов.
После этого появляются технические механизмы: REST API, HTTP-сервисы, webhooks, очереди, retry, идемпотентность, Transactional Outbox, Dead Letter Queue, версионирование и мониторинг.
Для небольшого проекта часть этих механизмов действительно будет избыточна.
Но три свойства стоит закладывать почти всегда:
операция должна быть безопасна при повторе; ошибка внешней системы не должна автоматически ломать основной продукт; любую важную интеграционную операцию должно быть возможно найти и восстановить.
Если эти принципы соблюдены, временная недоступность CRM превращается в задержку синхронизации, а не в потерянный заказ.
Timeout перестаёт автоматически означать повторное создание сущности.
Повторный webhook перестаёт создавать дубли.
А замена CRM или изменение внутренней структуры 1С перестают требовать переписывания всего сайта.
Именно в этот момент интеграция становится не набором связанных API-вызовов, а полноценной частью архитектуры веб-продукта.