Импорт клиентов через API

Подробный гайд по эндпоинту POST /api/v1/integration/leads/bulk — массовому импорту клиентов в вашу CRM-базу Notix из внешней системы. Эндпоинт идемпотентный по phone: повторный вызов с тем же телефоном обновит существующего клиента, а не создаст дубль.

Когда это нужно

  • Разовый перенос базы из старой CRM (amoCRM, Bitrix24, Excel) в Notix
  • Периодическая синхронизация: ночной крон сверяет клиентов и доливает новых
  • Импорт после вебинара / офлайн-мероприятия: CSV → скрипт → Notix

Эндпоинт

POST /api/v1/integration/leads/bulk

Требуется Bearer-токен со scope leads.bulk. До 100 лидов за один запрос — это ограничение защищает базу от случайного залива миллиона записей. Если у вас больше 100 клиентов — разбивайте на пачки.

Схема payload

ПолеТипОбязательноОписание
leadsarray<object>ДаМассив лидов. От 1 до 100 элементов
leads[].namestringДаИмя клиента. Если пусто или не передано — будет «Клиент»
leads[].phonestringНет*Телефон в любом формате: +7 917 000-00-00, 89170000000, 8 (917) 000-00-00. Нормализуется автоматически
leads[].emailstringНет*Email. Регистр игнорируется, обрезаются пробелы
leads[].source_ref_idstringНетВнешний ID лида (из вашей системы) — сохраняется в client.source_ref_id для аудита

* Что считается идентификатором клиента

Notix считает двух клиентов одним и тем же, если у них совпадает нормализованный телефон (последние 10 цифр с кодом страны) или email. Если передан только name без контактов — будет создан клиент с пустыми контактами, но при следующем вызове с тем же именем и контактами Notix склеит их по контактам, а не по имени.

Пример запроса

curl -X POST https://notix-hub.ru/api/v1/integration/leads/bulk \
  -H "Authorization: Bearer ntx_live_xxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxxx" \
  -H "Content-Type: application/json" \
  -d '{
    "leads": [
      {
        "name": "Иван Петров",
        "phone": "+7 917 000-00-00",
        "email": "ivan@example.com",
        "source_ref_id": "old-crm-1247"
      },
      {
        "name": "Мария Сидорова",
        "phone": "+7 (916) 555-12-34"
      },
      {
        "name": "Без контактов",
        "source_ref_id": "webinar-row-12"
      }
    ]
  }'

Ответ

{
  "processed": 3,
  "results": [
    {"ok": true, "client_id": "01a0393d-...", "created": true},
    {"ok": true, "client_id": "01a0393e-...", "created": true},
    {"ok": true, "client_id": "01a0393f-...", "created": true}
  ]
}

created: true — клиент создан. created: false — клиент с таким телефоном/email уже был в базе, мы обновили last_seen_at и (если было пусто) контактные данные.

Идемпотентность и дедупликация

Notix использует phone_hash (sha256 от нормализованного телефона) как основной ключ дедупликации. Это значит:

Обработка ошибок

Если в одном из элементов массива нет обязательного поля name, этот лид пропускается, но остальные продолжают обрабатываться:

{
  "processed": 3,
  "results": [
    {"ok": true, "client_id": "...", "created": true},
    {"ok": false, "error": "name required", "payload": {"phone": "+7 999 ..."}},
    {"ok": true, "client_id": "...", "created": false}
  ]
}

Лимит на запрос — 100 лидов. Если передадите больше, получите 422 Unprocessable:

{
  "message": "Too many leads in single request",
  "max": 100,
  "received": 150
}

Разбивайте большие CSV на батчи по 100 и отправляйте последовательно.

Пример на PHP: импорт CSV

<?php

/**
 * Скрипт импорта CSV в Notix через /leads/bulk.
 *
 * Формат CSV (заголовок):
 *   name,phone,email,source_ref_id
 *
 * Запуск:
 *   php import.php clients.csv
 */

$token = getenv('NOTIX_API_TOKEN') ?: 'ntx_live_...';
$base = 'https://notix-hub.ru/api/v1/integration/leads/bulk';
$file = $argv[1] ?? null;

if (! $file || ! is_readable($file)) {
    fwrite(STDERR, "Usage: php import.php <file.csv>\n");
    exit(1);
}

$fh = fopen($file, 'r');
$header = fgetcsv($fh);
if (! $header) {
    fwrite(STDERR, "Empty CSV\n");
    exit(1);
}

$batchSize = 100;
$batch = [];
$totalCreated = 0;
$totalUpdated = 0;
$totalErrors = 0;

while (($row = fgetcsv($fh)) !== false) {
    $row = array_combine($header, $row);
    $batch[] = array_filter([
        'name' => $row['name'] ?? null,
        'phone' => $row['phone'] ?? null,
        'email' => $row['email'] ?? null,
        'source_ref_id' => $row['source_ref_id'] ?? null,
    ], fn ($v) => $v !== null && $v !== '');

    if (count($batch) >= $batchSize) {
        [$created, $updated, $errors] = sendBatch($base, $token, $batch);
        $totalCreated += $created;
        $totalUpdated += $updated;
        $totalErrors += $errors;
        $batch = [];
    }
}

if ($batch) {
    [$created, $updated, $errors] = sendBatch($base, $token, $batch);
    $totalCreated += $created;
    $totalUpdated += $updated;
    $totalErrors += $errors;
}

fclose($fh);

echo "Created: $totalCreated, Updated: $totalUpdated, Errors: $totalErrors\n";

function sendBatch(string $url, string $token, array $batch): array
{
    $ch = curl_init($url);
    curl_setopt($ch, CURLOPT_POST, true);
    curl_setopt($ch, CURLOPT_HTTPHEADER, [
        'Authorization: Bearer ' . $token,
        'Content-Type: application/json',
    ]);
    curl_setopt($ch, CURLOPT_POSTFIELDS, json_encode(['leads' => $batch], JSON_UNESCAPED_UNICODE));
    curl_setopt($ch, CURLOPT_RETURNTRANSFER, true);

    $response = curl_exec($ch);
    $code = curl_getinfo($ch, CURLINFO_HTTP_CODE);
    curl_close($ch);

    if ($code !== 201) {
        fwrite(STDERR, "Batch failed (HTTP $code): $response\n");
        return [0, 0, count($batch)];
    }

    $data = json_decode($response, true);
    $results = $data['results'] ?? [];
    $created = count(array_filter($results, fn ($r) => $r['ok'] && ($r['created'] ?? false)));
    $updated = count(array_filter($results, fn ($r) => $r['ok'] && ! ($r['created'] ?? false)));
    $errors = count(array_filter($results, fn ($r) => ! $r['ok']));

    return [$created, $updated, $errors];
}

Что дальше