Привет, Хабр!

Каждый, кто работал с бухгалтерией, CRM или просто заводил контрагента вручную, знает эту боль. Вам присылают карточку компании в PDF, договор в DOCX или просто текстовый файл с реквизитами. Задача: достать оттуда ИНН, КПП, расчётный счёт и БИК, чтобы не схлопотать штраф за неверные данные.

Можно нанимать стажёра, который будет перепечатывать это в Excel. А можно довериться машине.

Мы в нашей компании долгое время решали эту проблему для своих внутренних задач (интеграция с 1С и автоматизация документооборота), а в итоге обкатали решение и выпилили в отдельный публичный сервис. Сегодня расскажу, как наш API извлечения реквизитов работает под капотом, покажу примеры кода на 6 языках (включая 1С, куда без него) и честно расскажу о таймаутах и подводных камнях.

Зачем это всё? (или проблема 20-й карточки)

Представьте: у вас интернет-магазин или B2B-платформа. Новый клиент регистрируется и вбивает реквизиты своей фирмы. Статистика неумолима: каждый третий пользователь ошибается при ручном вводе хотя бы в одной цифре ИНН или расчётного счёта. Дальше — сбой в 1С, невыставленный счёт, потерянная сделка.

Наш API решает эту задачу одним POST-запросом:

  1. Из карточки компании — извлекаем все юридически значимые реквизиты.

  2. Из файлов документов — договоров, счетов, актов, накладных.

Поддерживаемые форматы: PDF (только с текстовым слоем), DOCX, DOC, TXT, RTF, HTML.

⚠️ Важное уточнение: Сервис работает только с текстовыми файлами. Отсканированные PDF-изображения, картинки и фотографии не поддерживаются. Если у вас PDF без текстового слоя — перед отправкой нужно распознать его отдельным OCR-инструментом.

Как это работает (очень коротко)

Многие думают, что мы просто гоним текст через регулярные выражения. Но нет. Основная магия — в контекстном анализе и валидации.

  1. Приём файла: Вы шлёте multipart/form-data. Мы принимаем бинарник.

  2. Извлечение текста: PDF с текстовым слоем парсится напрямую. DOCX распарсивается через внутренний конвертер. TXT, RTF, HTML обрабатываются соответствующими парсерами.

  3. Нормализация: Убираем мусор, восстанавливаем разорванные слова, чиним кодировки. (Да, есть ещё люди, которые присылают документы в CP1251 без BOM).

  4. NER (Named Entity Recognition): Своя модель ищёт паттерны. ИНН — это 10 или 12 цифр, но не любая последовательность. ОГРН — тоже не просто цифры, у них своя структура.

  5. Валидация: Мы не просто выдёргиваем числа. Мы проверяем контрольные суммы ИНН, ОГРН, логику БИК. Если сумма не сходится — поле не вернётся, чтобы вы не сохранили в базу заведомо мусор.

Техническая документация по API

Всё максимально RESTful, без гимнастики.

Эндпоинт

POST https://api-k.ru/api/rekvizit_json

Заголовки

Ключ

Значение

Обязательность

X-API-Key

Ваш ключ (получаете в личном кабинете)

Да

Content-Type

multipart/form-data

Да

Параметры запроса

В теле — одно поле: file. Туда кладёте ваш PDF, DOCX, DOC, TXT, RTF или HTML.

⚠️ Важно по таймауту: Не ставьте стандартные 30 секунд. Некоторые «тяжёлые» PDF с графикой или документы большого объёма могут обрабатываться до 2 минут. Ставьте 120 секунд и спите спокойно.

Примеры кода (для ленивых и занятых)

Показывать только cURL — моветон. На Хабре любят глазами кушать код. Поэтому накидал примеры для самых популярных стеков плюс отдельный блок для тех, кто мучается с 1С.

Python (самый родной)

import requests
import os

def extract_requisites(api_key, file_path):
    url = "https://***.ru/api/rekvizit_json"
    headers = {'X-API-Key': api_key}
    
    with open(file_path, 'rb') as f:
        files = {'file': (os.path.basename(file_path), f, 'application/octet-stream')}
        response = requests.post(url, headers=headers, files=files, timeout=120)
    
    return response.json()

# Вжух
result = extract_requisites("ваш_ключ", "dogovor.pdf")
print(result['data']['inn'], result['data']['checking_account'])

cURL (для консольных магов)

curl -X POST https://***.ru/api/rekvizit_json \
  -H "X-API-Key: ваш_ключ" \
  -F "file=@/home/user/contract.docx" \
  --max-time 120

JavaScript (Node.js)

const axios = require('axios');
const FormData = require('form-data');
const fs = require('fs');

async function extract(apiKey, filePath) {
    const form = new FormData();
    form.append('file', fs.createReadStream(filePath));
    
    const response = await axios.post('https://***.ru/api/rekvizit_json', form, {
        headers: { ...form.getHeaders(), 'X-API-Key': apiKey },
        timeout: 120000
    });
    console.log(response.data);
}

PHP (без фреймворков)

$ch = curl_init();
$cFile = new CURLFile('contract.docx');

curl_setopt_array($ch, [
    CURLOPT_URL => 'https://***.ru/api/rekvizit_json',
    CURLOPT_POST => true,
    CURLOPT_POSTFIELDS => ['file' => $cFile],
    CURLOPT_HTTPHEADER => ['X-API-Key: ваш_ключ'],
    CURLOPT_TIMEOUT => 120,
    CURLOPT_RETURNTRANSFER => true
]);

$result = curl_exec($ch);
print_r(json_decode($result, true));

1С (боль и страдания)

Отдельная любовь — наши клиенты на 1С. Там нет нативной поддержки multipart/form-data "из коробки" так, как хочется. Пришлось поколдовать с формированием границы и ЗаписьДанных.

Ниже рабочий фрагмент для типовой конфигурации. Обратите внимание на ручное добавление

Процедура ОтправитьФайлНаСервере()
    // 1. Проверка - выбран ли файл
    Если ПустаяСтрока(ЭтотОбъект.ПутьКФайлу) Тогда
        Сообщить("Ошибка: Не выбран файл для отправки", СтатусСообщения.Важное);
        Возврат;
    КонецЕсли;
    
    // 2. Проверка наличия API-ключа
    Если ПустаяСтрока(ЭтотОбъект.APIКлюч) Тогда
        Сообщить("Ошибка: Не указан API-ключ", СтатусСообщения.Важное);
        Возврат;
    КонецЕсли;   
    
    // 3. Проверка существования файла
    ФайлДляПроверки = Новый Файл(ЭтотОбъект.ПутьКФайлу);
    Если Не ФайлДляПроверки.Существует() Тогда
        Сообщить("Ошибка: Файл не найден по пути " + ЭтотОбъект.ПутьКФайлу, СтатусСообщения.Важное);
        Возврат;
    КонецЕсли;
    
    // 4. Чтение файла в двоичные данные
    ДвоичныеДанныеФайла = Новый ДвоичныеДанные(ЭтотОбъект.ПутьКФайлу);
    ИмяФайла = ФайлДляПроверки.Имя;
    
	// 5. Формирование тела запроса (multipart/form-data) — ИСПРАВЛЕННЫЙ вариант
	Граница = "----WebKitFormBoundary" + СтрЗаменить(Строка(Новый УникальныйИдентификатор()), "-", "");

	Тело = Новый ПотокВПамяти;
	ЗаписьДанных = Новый ЗаписьДанных(Тело, , , Символы.ВК + Символы.ПС, "");  // Ключ: CRLF и пустой РазделительСтрок

	ЗаписьДанных.ЗаписатьСтроку("--" + Граница);
	ЗаписьДанных.ЗаписатьСтроку("Content-Disposition: form-data; name=""file""; filename=""" + ИмяФайла + """");
	ЗаписьДанных.ЗаписатьСтроку("Content-Type: application/octet-stream");
	ЗаписьДанных.ЗаписатьСтроку("");  // Пустая строка перед данными
	ЗаписьДанных.Записать(ДвоичныеДанныеФайла);  // Двоичные данные файла
	ЗаписьДанных.ЗаписатьСтроку("");  // Пустая строка после данных
	ЗаписьДанных.ЗаписатьСтроку("--" + Граница + "--");  // Закрывающий разделитель

	ЗаписьДанных.Закрыть();
	ДвоичныеДанныеТела = Тело.ЗакрытьИПолучитьДвоичныеДанные();    
    // 6. Формирование HTTP-запроса
    ИмяСервера = "***.ru";
    Порт = 443;
    ЗащищенноеСоединение = Истина;
    
    // Создаем HTTP-соединение
    Попытка
        HTTPСоединение = Новый HTTPСоединение(ИмяСервера, Порт, "", "", , ЗащищенноеСоединение);
    Исключение
        Сообщить("Ошибка при создании HTTP-соединения: " + ОписаниеОшибки(), СтатусСообщения.Важное);
        Возврат;
    КонецПопытки;
    
    // Создаем HTTP-запрос
    HTTPЗапрос = Новый HTTPЗапрос("/api/rekvizit_json");
    HTTPЗапрос.Заголовки.Вставить("X-API-Key", ЭтотОбъект.APIКлюч);
    HTTPЗапрос.Заголовки.Вставить("Content-Type", "multipart/form-data; boundary=" + Граница);  
    HTTPЗапрос.УстановитьТелоИзДвоичныхДанных(ДвоичныеДанныеТела); 
	HTTPЗапрос.Заголовки.Вставить("Content-Length", XMLСтрока(ДвоичныеДанныеТела.Размер())); 
    
    // Отправляем запрос
    Попытка
        Ответ = HTTPСоединение.ВызватьHTTPМетод("POST", HTTPЗапрос);
        
        Если Ответ.КодСостояния = 200 Тогда
            СтрокаОтвета = Ответ.ПолучитьТелоКакСтроку();
            РазобратьИОтобразитьРезультат(СтрокаОтвета);
            ЭтотОбъект.ОтветСервера = СтрокаОтвета;
            Сообщить("Успешно! Файл обработан, реквизиты получены.", СтатусСообщения.Информация);
        Иначе
            СтрокаОшибки = Ответ.ПолучитьТелоКакСтроку();
            ЭтотОбъект.ОтветСервера = СтрокаОшибки;
            Сообщить("Ошибка HTTP " + Строка(Ответ.КодСостояния) + ": " + СтрокаОшибки, СтатусСообщения.Важное);
        КонецЕсли;
        
    Исключение
        Сообщить("Ошибка запроса: " + ОписаниеОшибки(), СтатусСообщения.Важное);
    КонецПопытки;   
КонецПроцедуры

Go

package main

import (
    "bytes"
    "encoding/json"
    "io"
    "mime/multipart"
    "net/http"
    "os"
    "path/filepath"
    "time"
)

type Response struct {
    Success bool   `json:"success"`
    Error   string `json:"error,omitempty"`
    // другие поля с реквизитами
}

func ExtractRequisites(apiKey, filePath string) (*Response, error) {
    file, err := os.Open(filePath)
    if err != nil {
        return nil, err
    }
    defer file.Close()

    body := &bytes.Buffer{}
    writer := multipart.NewWriter(body)
    
    part, err := writer.CreateFormFile("file", filepath.Base(filePath))
    if err != nil {
        return nil, err
    }
    
    _, err = io.Copy(part, file)
    if err != nil {
        return nil, err
    }
    writer.Close()

    req, err := http.NewRequest("POST", "https://***.ru/api/rekvizit_json", body)
    if err != nil {
        return nil, err
    }
    
    req.Header.Set("X-API-Key", apiKey)
    req.Header.Set("Content-Type", writer.FormDataContentType())

    client := &http.Client{
        Timeout: 120 * time.Second,
    }
    
    resp, err := client.Do(req)
    if err != nil {
        return nil, err
    }
    defer resp.Body.Close()

    var result Response
    if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
        return nil, err
    }
    
    return &result, nil
}

На что похож ответ?

Вы получаете JSON. Если всё ок — "success": true и объект data. Если нет — читайте message.

Успех:

{
  "success": true,
  "filename": "act_123.pdf",
  "file_size": 20480,
  "data": {
    "organization_name": "ООО «Ромашка»",
    "inn": "7725123456",
    "kpp": "772501001",
    "ogrn": "1027700123456",
    "address": "г. Москва, ул. Тверская, д. 1",
    "phone": "+7(495) 123-45-67",
    "bank_name": "Т-Банк",
    "bik": "044525974",
    "checking_account": "40802810123456789012",
    "correspondent_account": "30101810123456789012",
    "type_doc": "договор",
    "nom_doc": "45/А",
    "signatory": "Петров А.А."
  }
}

Ошибка (например, файл слишком большой):

{
  "success": false,
  "error": "payload_too_large",
  "message": "Размер файла превышает 20 МБ"
}

Политика ошибок (чтобы не было сюрпризов)

Мы стараемся отвечать стандартными HTTP кодами, но всегда смотрите на success в теле ответа — там надёжнее.

HTTP

error

Что делать?

400

invalid_request

Вы прислали пустой файл, неверный формат или отсутствует тело запроса

401

unauthorized

Проверьте ключ X-API-Key. Он есть в заголовках?

408

timeout

Увеличьте таймаут до 120 секунд. Документ слишком сложный

413

payload_too_large

Файл весит больше 20 МБ — уменьшите или порежьте

500

internal_error

Упал наш демон. Такое бывает редко, но мы мониторим 24/7

Бенчмарки и ограничения (начистоту)

Мы прогнали через API 10 000 реальных документов из открытых бухгалтерских архивов.

  • Точность распознавания ИНН/ОГРН: 99.7% (ошибки в основном на документах с нестандартной вёрсткой).

  • Точность распознавания расчётного счёта: 98.9% (иногда путаем цифры, если шрифт "плывёт").

  • Скорость: Обычный DOCX на 2 страницы — 1.5 секунды. Тяжёлый PDF с большим количеством графики — до 45 секунд.

  • Лимиты: По умолчанию — 2 МБ на файл и 120 секунд на операцию.

  • Поддерживаемые форматы: PDF (только текстовый слой), DOCX, DOC, TXT, RTF, HTML.

Вместо заключения

Этот API уже месяц как используется в продакшне. К нам стучатся как интернет-магазины (чтобы автоматически регистрировать юрлиц), так и банки (для проверки карточек клиентов). Если вы устали парсить реквизиты регулярками или нанимать людей для перепечатки — попробуйте. Тестовый доступ к АПИ на 30 дней (100 запрсов к сервису) - с промокодом: 1MPROMO2026. Промокод действует до 30.05.2026 г.

Комментарии (2)


  1. oleg_km
    14.04.2026 19:27

    А зачем тогда на вход мультиформ? Сделайте тоже джейсон, 1С его умеет


    1. timka05
      14.04.2026 19:27

      Да не надо никаких json на входе. И multipart/form-data не надо.

      В принципе ничего в заголовках не надо (ну кроме X-API-Key для авторизации).

      Весь бинарник слать POST. Без всяких имен.

      Если совсем очень хочется правильно, то можно в заголовках:

      Content-Type: application/octet-stream

      Тогда и 1С легко такое сможет, да и на других языках немного проще станет. Да и в принципе правильнее будет, ибо нет у вас никаких multipart данных. Есть по факту один большой blob / бинарник.