Выгрузка сделок из amoCRM и Битрикс24
Скрипт раз в час забирает из CRM сделки, появившиеся после прошлой загрузки, и дописывает их на лист «Сделки». Своего сервера не нужно: скрипт сам обращается к CRM по расписанию.
Что понадобится:
- таблица Coverse на тарифе «Бизнес» или выше;
- права администратора в amoCRM или право создавать вебхуки в Битрикс24;
- 15 минут.
Как это работает
- Ключ доступа к CRM хранится в Секретах скрипта, а не в коде.
- Скрипт запоминает в Памяти номер последней загруженной сделки и при следующем запуске запрашивает только более новые.
- CRM отдаёт сделки страницами. Скрипт читает до 10 страниц за запуск, остальное заберёт следующий запуск.
- Новые строки записываются одной командой
appendRows.
Подготовьте лист
Создайте лист «Сделки» с заголовками в первой строке:
| A | B | C | D | E |
|---|---|---|---|---|
| ID | Название | Сумма | Этап | Создана |
Скрипт ничего не удаляет. Новые сделки он дописывает внизу, а сделки, которые уже есть на листе, обновляет по номеру в столбце ID.
amoCRM
1. Получите ключ
- В amoCRM в разделе amoМаркет создайте внешнюю интеграцию.
- В настройках интеграции на вкладке «Ключи» сгенерируйте долгосрочный токен и выберите срок действия, от одного дня до пяти лет.
- Скопируйте токен: amoCRM показывает его только один раз.
Подробнее — в документации amoCRM.
2. Сохраните токен в секрет
В редакторе скрипта откройте вкладку «Секреты» и добавьте секрет AMO_TOKEN со значением токена.
3. Вставьте код
Замените example на поддомен вашего аккаунта: если адрес https://shop.amocrm.ru, то SUBDOMAIN = "shop".
const SUBDOMAIN = "example";
const SHEET = "Сделки";
const MAX_PAGES = 10;
function main() {
const token = Secrets.get("AMO_TOKEN");
if (!token) throw new Error("Добавьте секрет AMO_TOKEN");
const since = Number(Properties.script.get("amoSince")) || 0;
const lastId = Number(Properties.script.get("amoLastId")) || 0;
const rows = [];
let newestCreated = since;
let newestId = lastId;
for (let page = 1; page <= MAX_PAGES; page++) {
const response = Http.fetch("https://" + SUBDOMAIN + ".amocrm.ru/api/v4/leads", {
headers: { Authorization: "Bearer " + token },
query: {
"filter[created_at][from]": since,
"order[created_at]": "asc",
limit: 100,
page: page,
},
});
if (response.status === 204) break;
if (!response.ok) throw new Error("amoCRM ответила с кодом " + response.status + ": " + response.text());
const leads = (response.json()._embedded || {}).leads || [];
for (const lead of leads) {
if (lead.created_at === since && lead.id <= lastId) continue;
rows.push([lead.id, asText(lead.name), Number(lead.price) || 0, lead.status_id, new Date(lead.created_at * 1000)]);
if (lead.created_at > newestCreated || (lead.created_at === newestCreated && lead.id > newestId)) {
newestCreated = lead.created_at;
newestId = lead.id;
}
}
if (leads.length < 100) break;
}
if (rows.length === 0) {
console.log("Новых сделок нет");
return;
}
upsertDeals(rows);
Coverse.table.flush();
Properties.script.set("amoSince", newestCreated);
Properties.script.set("amoLastId", newestId);
console.log("Добавлено сделок:", rows.length);
}
function asText(value) {
const text = String(value === undefined || value === null ? "" : value);
return text.charAt(0) === "=" ? "'" + text : text;
}
function upsertDeals(rows) {
const sheet = Coverse.table.getSheet(SHEET);
const keys = sheet.getRange(1, 1, sheet.getLastRow(), 1).getValues();
const index = new Map(keys.map((row, i) => [String(row[0]), i + 1]));
const added = new Map();
for (const row of rows) {
const at = index.get(String(row[0]));
if (at === undefined) {
added.set(String(row[0]), row);
} else {
sheet.getRange(at, 1, 1, row.length).setValues([row]);
}
}
if (added.size > 0) sheet.appendRows([...added.values()]);
}
Что важно в коде:
- Скрипт запрашивает по 100 сделок на страницу. amoCRM разрешает до 250, но ответ сервиса не может быть больше 2 МБ, а сделки с большим числом полей быстро упираются в этот предел.
- Время создания приходит в секундах Unix, поэтому дата получается через
new Date(lead.created_at * 1000). Coverse.table.flush()отправляет строки в таблицу сразу. Если запись не удалась, скрипт остановится с ошибкой до того, как сдвинет отметку в «Памяти», и следующий запуск загрузит эти сделки снова. Безflushстроки ушли бы в таблицу только в конце запуска, а отметка в «Памяти» сохраняется при любом исходе.- Текст из внешнего сервиса, который начинается с
=, таблица записала бы как формулу. ФункцияasTextставит перед таким значением апостроф, и оно записывается текстом. - Если новых сделок нет, amoCRM отвечает кодом 204 без тела, скрипт просто выходит.
- Сделки, созданные в ту же секунду, что и последняя загруженная, отсекаются по номеру, поэтому повторов не будет.
- В столбец «Этап» попадает номер этапа (
status_id). Названия этапов можно подставить по своему справочнику на отдельном листе.
Битрикс24
1. Создайте входящий вебхук
- В Битрикс24 откройте раздел «Разработчикам» и создайте входящий вебхук.
- Дайте ему доступ к CRM и сохраните.
- Скопируйте адрес вида
https://portal.bitrix24.ru/rest/1/abcd1234efgh5678/. Код в адресе даёт доступ к CRM, поэтому храните адрес как секрет.
Подробнее — в документации Битрикс24.
2. Сохраните адрес в секрет
Добавьте на вкладке «Секреты» секрет BITRIX_WEBHOOK со значением адреса вебхука, включая / в конце.
3. Вставьте код
const SHEET = "Сделки";
const MAX_PAGES = 10;
function main() {
const base = Secrets.get("BITRIX_WEBHOOK");
if (!base) throw new Error("Добавьте секрет BITRIX_WEBHOOK");
const lastId = Number(Properties.script.get("bitrixLastId")) || 0;
const rows = [];
let newestId = lastId;
let start = 0;
for (let page = 1; page <= MAX_PAGES; page++) {
const response = Http.post(base + "crm.deal.list.json", {
filter: { ">ID": lastId },
select: ["ID", "TITLE", "OPPORTUNITY", "STAGE_ID", "DATE_CREATE"],
order: { ID: "ASC" },
start: start,
});
if (!response.ok) throw new Error("Битрикс24 ответил с кодом " + response.status + ": " + response.text());
const data = response.json();
for (const deal of data.result) {
rows.push([Number(deal.ID), asText(deal.TITLE), Number(deal.OPPORTUNITY) || 0, deal.STAGE_ID, new Date(deal.DATE_CREATE)]);
}
if (data.result.length > 0) newestId = Number(data.result[data.result.length - 1].ID);
if (data.next === undefined) break;
start = data.next;
}
if (rows.length === 0) {
console.log("Новых сделок нет");
return;
}
upsertDeals(rows);
Coverse.table.flush();
Properties.script.set("bitrixLastId", newestId);
console.log("Добавлено сделок:", rows.length);
}
function asText(value) {
const text = String(value === undefined || value === null ? "" : value);
return text.charAt(0) === "=" ? "'" + text : text;
}
function upsertDeals(rows) {
const sheet = Coverse.table.getSheet(SHEET);
const keys = sheet.getRange(1, 1, sheet.getLastRow(), 1).getValues();
const index = new Map(keys.map((row, i) => [String(row[0]), i + 1]));
const added = new Map();
for (const row of rows) {
const at = index.get(String(row[0]));
if (at === undefined) {
added.set(String(row[0]), row);
} else {
sheet.getRange(at, 1, 1, row.length).setValues([row]);
}
}
if (added.size > 0) sheet.appendRows([...added.values()]);
}
Что важно в коде:
- Битрикс24 отдаёт по 50 сделок на страницу. Поле
nextв ответе указывает, с какой записи продолжать, и есть только тогда, когда записей больше. - Сделки фильтруются по номеру:
">ID"больше последнего загруженного в прошлый раз. Номера растут, поэтому повторов не будет. - Сумма приходит текстом, скрипт превращает её в число через
Number, чтобы в таблице работали формулы. Coverse.table.flush()иasTextнужны для того же, что и в коде для amoCRM.- Метод
crm.deal.listБитрикс24 больше не развивает, но он работает. Для новых проектов Битрикс24 рекомендуетcrm.item.list.
Проверьте и включите расписание
- Нажмите «Запустить». В журнале появится «Добавлено сделок: N», на листе — строки.
- Запустите ещё раз: должно быть «Новых сделок нет».
- На вкладке «Триггеры» добавьте триггер «По расписанию», каждый час, функция
main.
Чтобы загрузить всё заново, очистите лист ниже заголовков и удалите ключи на вкладке «Память»: amoSince и amoLastId или bitrixLastId.
Сразу при создании или изменении сделки
Загрузка по расписанию приходит раз в час. Если сделки нужны в таблице сразу, CRM может сама сообщать о них на вебхук скрипта. Уведомление содержит только номер сделки и событие, поэтому скрипт берёт данные сделки из API CRM. Так поддельный запрос не сможет записать в таблицу чужие данные.
Оставьте и загрузку по расписанию: она подхватит новые сделки, если уведомление о создании не дошло. Изменения уже загруженных сделок доезжают только по вебхуку.
amoCRM
- Добавьте в скрипт для amoCRM функцию ниже. Функции
upsertDealsиasTextв скрипте уже есть. - На вкладке «Триггеры» добавьте «По вебхуку» с функцией
onWebhookи скопируйте адрес видаhttps://app.coverse.team/api/hooks/scripts/whs_…. - В amoCRM откройте amoМаркет и нажмите Web hooks, вставьте адрес и отметьте события добавления сделки, изменения сделки и смены её статуса. Вебхуки в amoCRM настраивает администратор.
function onWebhook(event) {
if (!event || !event.form) return;
const ids = new Set();
for (const key of Object.keys(event.form)) {
const match = /^leads\[(add|update|status)\]\[\d+\]\[id\]$/.exec(key);
if (match) ids.add(event.form[key]);
}
if (ids.size === 0) return;
const token = Secrets.get("AMO_TOKEN");
if (!token) throw new Error("Добавьте секрет AMO_TOKEN");
if (ids.size > 40) console.warn("Сделок в уведомлении больше 40, остальные загрузит запуск по расписанию");
const rows = [];
for (const id of [...ids].slice(0, 40)) {
const response = Http.get("https://" + SUBDOMAIN + ".amocrm.ru/api/v4/leads/" + encodeURIComponent(id), {
headers: { Authorization: "Bearer " + token },
});
if (!response.ok) {
console.warn("amoCRM ответила с кодом " + response.status + " для сделки " + id);
continue;
}
if (response.status === 204) continue;
const lead = response.json();
rows.push([lead.id, asText(lead.name), Number(lead.price) || 0, lead.status_id, new Date(lead.created_at * 1000)]);
}
if (rows.length > 0) upsertDeals(rows);
}
- amoCRM отправляет уведомление как поля формы: номер сделки лежит в ключе вида
leads[add][0][id]. Скрипт собирает номера из всех таких ключей. Чтобы посмотреть, что именно прислала CRM, добавьтеconsole.log(JSON.stringify(event.form)). upsertDealsобновляет строку сделки, если она уже есть, и дописывает новую, если нет. Поэтому изменение статуса тоже доезжает в таблицу, а сделка, которую потом загрузит запуск по расписанию, не задвоится.- Удалённая сделка возвращает код 204, скрипт её пропускает.
- amoCRM ждёт ответ за 2 секунды. Coverse отвечает сразу, а скрипт выполняется после ответа.
Битрикс24
- Добавьте в скрипт для Битрикс24 функцию
onWebhookниже. ФункцииupsertDealsиasTextв скрипте уже есть. - На вкладке «Триггеры» добавьте «По вебхуку» с функцией
onWebhookи скопируйте адрес. - В Битрикс24 откройте Приложения → Разработчикам → Готовые сценарии → Другое → Исходящий вебхук, вставьте адрес и выберите события
ONCRMDEALADD(создание сделки) иONCRMDEALUPDATE(изменение сделки). - После сохранения Битрикс24 покажет токен приложения. Сохраните его в секрет
BITRIX_APP_TOKEN: по нему скрипт проверяет, что уведомление пришло от вашего Битрикс24.
function onWebhook(event) {
if (!event || !event.form) return;
if (event.form["auth[application_token]"] !== Secrets.get("BITRIX_APP_TOKEN")) {
console.warn("Неверный токен приложения, уведомление пропущено");
return;
}
const id = event.form["data[FIELDS][ID]"];
if (!id) return;
const response = Http.post(Secrets.get("BITRIX_WEBHOOK") + "crm.deal.get.json", { id: id });
if (!response.ok) throw new Error("Битрикс24 ответил с кодом " + response.status + ": " + response.text().slice(0, 300));
const deal = response.json().result;
if (!deal) throw new Error("Битрикс24 не вернул сделку " + id);
upsertDeals([[Number(deal.ID), asText(deal.TITLE), Number(deal.OPPORTUNITY) || 0, deal.STAGE_ID, new Date(deal.DATE_CREATE)]]);
}
- В уведомлении Битрикс24 только номер сделки в поле
data[FIELDS][ID], остальное скрипт берёт через входящий вебхук из секретаBITRIX_WEBHOOK. - Битрикс24 не повторяет уведомление, если доставка не удалась, поэтому загрузка по расписанию нужна обязательно.
- При массовом изменении сделок Битрикс24 отправляет по уведомлению на каждую сделку, и каждое запускает скрипт. Адрес вебхука принимает до 60 запросов в минуту, остальные Coverse отклонит с кодом 429, а Битрикс24 их не повторит. Следите и за нормой минут в панели скриптов.
Если что-то не работает
| Симптом | Причина | Что сделать |
|---|---|---|
| ошибка с кодом 401 | токен неверный, истёк или секрет называется иначе | создайте новый токен и сохраните его в секрет |
| ошибка с кодом 402 в amoCRM | аккаунт amoCRM не оплачен | продлите тариф amoCRM |
ошибка с кодом 429 в amoCRM или 503 с QUERY_LIMIT_EXCEEDED в Битрикс24 | слишком частые запросы | запускайте реже, уменьшите MAX_PAGES |
Secrets.get вернул null | секрет не добавлен или код менял не владелец скрипта | см. Секреты |
| Слишком много внешних запросов за один запуск (не больше 50). | больше 50 внешних запросов за запуск, если вы увеличили MAX_PAGES | верните MAX_PAGES не больше 40 |
| Http.fetch: ответ больше 2097152 байт. или «Внешние сервисы прислали слишком много данных за один запуск» | ответ CRM больше 2 МБ или за запуск получено больше 10 МБ | уменьшите limit или MAX_PAGES |
| вебхук CRM приходит, но строка не появляется | в журнале скрипта причина: неверный токен, ошибка API или нет номера сделки | добавьте console.log(JSON.stringify(event.form)) и сверьте ключи |
| «Превышено время» | ответы CRM приходят медленно | уменьшите MAX_PAGES, остальное заберёт следующий запуск |
Частые вопросы
Как выгружать сделки amoCRM в таблицу?
Скрипт по расписанию запрашивает новые сделки через API amoCRM с долгосрочным токеном, а вебхук amoCRM сообщает о новых сделках сразу.
Как выгружать сделки Битрикс24 в таблицу?
Через входящий вебхук Битрикс24: скрипт по расписанию вызывает crm.deal.list, а исходящий вебхук сообщает о новых и изменённых сделках.
Будут ли повторы при загрузке?
Нет. Скрипт запоминает отметку последней загрузки и обновляет строки по номеру сделки.
Что дальше
- Внешние сервисы: все параметры
Http.fetch. - Быстрые и экономные скрипты
- Сводная по менеджерам каждую неделю: сводная по загруженным сделкам.