Перейти к основному содержимому

Выгрузка сделок из amoCRM и Битрикс24

Скрипт раз в час забирает из CRM сделки, появившиеся после прошлой загрузки, и дописывает их на лист «Сделки». Своего сервера не нужно: скрипт сам обращается к CRM по расписанию.

Что понадобится:

  • таблица Coverse на тарифе «Бизнес» или выше;
  • права администратора в amoCRM или право создавать вебхуки в Битрикс24;
  • 15 минут.

Как это работает​

  1. Ключ доступа к CRM хранится в Секретах скрипта, а не в коде.
  2. Скрипт запоминает в Памяти номер последней загруженной сделки и при следующем запуске запрашивает только более новые.
  3. CRM отдаёт сделки страницами. Скрипт читает до 10 страниц за запуск, остальное заберёт следующий запуск.
  4. Новые строки записываются одной командой appendRows.

Подготовьте лист​

Создайте лист «Сделки» с заголовками в первой строке:

ABCDE
IDНазваниеСуммаЭтапСоздана

Скрипт ничего не удаляет. Новые сделки он дописывает внизу, а сделки, которые уже есть на листе, обновляет по номеру в столбце ID.

amoCRM​

1. Получите ключ​

  1. В amoCRM в разделе amoМаркет создайте внешнюю интеграцию.
  2. В настройках интеграции на вкладке «Ключи» сгенерируйте долгосрочный токен и выберите срок действия, от одного дня до пяти лет.
  3. Скопируйте токен: 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. Создайте входящий вебхук​

  1. В Битрикс24 откройте раздел «Разработчикам» и создайте входящий вебхук.
  2. Дайте ему доступ к CRM и сохраните.
  3. Скопируйте адрес вида 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.

Проверьте и включите расписание​

  1. Нажмите «Запустить». В журнале появится «Добавлено сделок: N», на листе — строки.
  2. Запустите ещё раз: должно быть «Новых сделок нет».
  3. На вкладке «Триггеры» добавьте триггер «По расписанию», каждый час, функция main.

Чтобы загрузить всё заново, очистите лист ниже заголовков и удалите ключи на вкладке «Память»: amoSince и amoLastId или bitrixLastId.

Сразу при создании или изменении сделки​

Загрузка по расписанию приходит раз в час. Если сделки нужны в таблице сразу, CRM может сама сообщать о них на вебхук скрипта. Уведомление содержит только номер сделки и событие, поэтому скрипт берёт данные сделки из API CRM. Так поддельный запрос не сможет записать в таблицу чужие данные.

Оставьте и загрузку по расписанию: она подхватит новые сделки, если уведомление о создании не дошло. Изменения уже загруженных сделок доезжают только по вебхуку.

amoCRM​

  1. Добавьте в скрипт для amoCRM функцию ниже. Функции upsertDeals и asText в скрипте уже есть.
  2. На вкладке «Триггеры» добавьте «По вебхуку» с функцией onWebhook и скопируйте адрес вида https://app.coverse.team/api/hooks/scripts/whs_….
  3. В 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​

  1. Добавьте в скрипт для Битрикс24 функцию onWebhook ниже. Функции upsertDeals и asText в скрипте уже есть.
  2. На вкладке «Триггеры» добавьте «По вебхуку» с функцией onWebhook и скопируйте адрес.
  3. В Битрикс24 откройте Приложения → Разработчикам → Готовые сценарии → Другое → Исходящий вебхук, вставьте адрес и выберите события ONCRMDEALADD (создание сделки) и ONCRMDEALUPDATE (изменение сделки).
  4. После сохранения Битрикс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, а исходящий вебхук сообщает о новых и изменённых сделках.

Будут ли повторы при загрузке?​

Нет. Скрипт запоминает отметку последней загрузки и обновляет строки по номеру сделки.

Что дальше​