Необходимые cookie-файлы нужны для работы сайта. Аналитические cookie (Яндекс Метрика) — только с вашего согласия. Политика cookie-файлов

Тема оформления
Инструкции

Интеграция с n8n

Как вызывать Typizer из n8n узлом HTTP Request и принимать результат узлом Webhook.

Typizer не публикует отдельный узел (node) для n8n — интеграция идёт через встроенные узлы HTTP Request и Webhook, как с любым REST API. Ниже — готовый сценарий (workflow) с обоими узлами, который можно импортировать целиком.

Примечание:

Возможности узлов Code и переменные окружения зависят от версии и настроек вашего инстанса n8n (self-hosted или n8n Cloud) — сверяйтесь с актуальной документацией n8n, если что-то из описанного ниже недоступно в вашей установке.

Узел HTTP Request: мультизагрузка файла

Узел HTTP Request с Content-Type: multipart-form-data отправляет файл и схему тем же способом, что и curl-пример в «Первый вызов API»:

  • метод POST, адрес https://api.typizer.ru/v1/extract;
  • заголовок Authorization: Bearer <ключ> — ключ стоит хранить в переменной окружения n8n ({{ $env.TYPIZER_API_KEY }}), а не в самом узле;
  • заголовок Idempotency-Key — свой на каждый запуск workflow, например {{ $execution.id }} (идентификатор текущего выполнения n8n — гарантированно разный у двух разных запусков);
  • тело — formBinaryData для самого файла и formData для schema (JSON-схема как строка);
  • необязательно: webhook_url, указывающий на узел Webhook этого же сценария (см. ниже) — тогда результат придёт туда, а не придётся дожидаться синхронного ответа.

Узел Webhook: приём результата

Узел Webhook (режим onReceived, чтобы сразу ответить 200 и не задерживать повторные попытки доставки) принимает вебхук на свой уникальный адрес. Включите у него опцию Response → Raw Body — без неё n8n отдаёт уже распарсенное тело, а пересобранный заново JSON (JSON.stringify от разобранного объекта) почти наверняка не совпадёт побайтово с тем, что подписал Typizer (другой порядок ключей, другие пробелы), и подпись не сойдётся ни у одного настоящего события. С этой опцией сырые байты тела приходят как бинарные данные — точное имя свойства и формат зависят от версии n8n, сверяйтесь с документацией вашей установки.

Прежде чем использовать тело запроса, проверьте подпись — тем же алгоритмом, что описан в «Приём вебхуков»: HMAC-SHA256 от {webhook-id}.{webhook-timestamp}.{сырое тело} на секрете конечной точки, с допуском 5 минут по времени. В n8n это делает узел Code (JavaScript) со встроенным crypto — на self-hosted-инстансе может понадобиться переменная окружения NODE_FUNCTION_ALLOW_BUILTIN=crypto, чтобы require('crypto') был разрешён в песочнице узла; в облачном n8n доступность таких модулей определяется тарифом и настройками — сверяйтесь с документацией n8n. Код узла ниже протестирован (test/n8n-webhook-verify.test.ts) на том же векторе, что и сниппеты Node/Python/PHP, включая отклонённые варианты — поддельный секрет, изменённое тело, просроченную метку времени, нечисловую метку времени и отсутствие сырого тела.

Пример сценария целиком

Готовый экспорт сценария (JSON, импортируется в n8n через «Import from File») с обоими узлами — HTTP Request с мультизагрузкой и Webhook с проверкой подписи в узле Code:

JSON
{
  "name": "Typizer: extract + webhook",
  "nodes": [
    {
      "id": "manual-trigger",
      "name": "When clicking ‘Execute workflow’",
      "type": "n8n-nodes-base.manualTrigger",
      "typeVersion": 1,
      "position": [-640, 180],
      "parameters": {}
    },
    {
      "id": "read-file",
      "name": "Read invoice from disk",
      "type": "n8n-nodes-base.readWriteFile",
      "typeVersion": 1,
      "position": [-420, 180],
      "parameters": {
        "operation": "read",
        "fileSelector": "/data/invoice.pdf",
        "options": {
          "dataPropertyName": "data"
        }
      }
    },
    {
      "id": "http-request-extract",
      "name": "POST /v1/extract",
      "type": "n8n-nodes-base.httpRequest",
      "typeVersion": 4.2,
      "position": [-200, 180],
      "parameters": {
        "method": "POST",
        "url": "https://api.typizer.ru/v1/extract",
        "sendHeaders": true,
        "headerParameters": {
          "parameters": [
            {
              "name": "Authorization",
              "value": "=Bearer {{ $env.TYPIZER_API_KEY }}"
            },
            {
              "name": "Idempotency-Key",
              "value": "={{ $execution.id }}"
            }
          ]
        },
        "sendBody": true,
        "contentType": "multipart-form-data",
        "bodyParameters": {
          "parameters": [
            {
              "parameterType": "formBinaryData",
              "name": "file",
              "inputDataFieldName": "data"
            },
            {
              "parameterType": "formData",
              "name": "schema",
              "value": "{\"type\":\"object\",\"properties\":{\"invoice_number\":{\"type\":\"string\"},\"total\":{\"type\":\"number\"},\"inn\":{\"type\":\"string\",\"x-typizer-check\":\"inn\"}},\"required\":[\"invoice_number\",\"total\"]}"
            },
            {
              "parameterType": "formData",
              "name": "webhook_url",
              "value": "={{ $env.N8N_WEBHOOK_URL }}/webhook/typizer-webhook"
            }
          ]
        },
        "options": {}
      },
      "notes": "Idempotency-Key = id выполнения workflow: свой на каждый запуск, как и для ручных примеров в «Первый вызов API». webhook_url указывает на ноду Webhook этого же workflow (см. ниже) — результат придёт туда, а не в ответ на этот запрос."
    },
    {
      "id": "webhook-receive",
      "name": "Webhook: приём результата",
      "type": "n8n-nodes-base.webhook",
      "typeVersion": 2,
      "position": [-640, 420],
      "parameters": {
        "httpMethod": "POST",
        "path": "typizer-webhook",
        "responseMode": "onReceived",
        "responseCode": 200,
        "options": {}
      },
      "webhookId": "typizer-webhook-demo"
    },
    {
      "id": "verify-signature",
      "name": "Проверить подпись (Code)",
      "type": "n8n-nodes-base.code",
      "typeVersion": 2,
      "position": [-420, 420],
      "parameters": {
        "language": "javaScript",
        "jsCode": "// Тот же алгоритм, что в snippets/webhooks/verify.ts — см. «Приём вебхуков». Проверено тестом\n// против того же вектора, что и Node/Python/PHP-сниппеты (test/n8n-webhook-verify.test.ts).\n// Доступность node:crypto в Code-ноде зависит от инстанса n8n (self-hosted: переменная\n// NODE_FUNCTION_ALLOW_BUILTIN=crypto); сверяйтесь с документацией вашей установки n8n.\nconst crypto = require('crypto');\n\nconst item = $input.first();\nconst headers = item.json && item.json.headers;\nconst secret = $env.TYPIZER_WEBHOOK_SECRET;\n\n// Нода Webhook выше ДОЛЖНА быть настроена с опцией Response > Raw Body — иначе n8n отдаёт уже\n// распарсенное тело, и переподписанное заново (JSON.stringify) тело почти наверняка разойдётся с\n// исходными байтами, по которым Typizer считал подпись (другой порядок ключей, другие пробелы).\n// С этой опцией сырые байты лежат в бинарных данных под свойством data — имя свойства и точный\n// формат зависят от версии n8n, сверяйтесь с документацией вашей установки.\nconst rawBodyBase64 = item.binary && item.binary.data && item.binary.data.data;\nif (!rawBodyBase64) {\n  throw new Error(\n    'raw body missing \\u2014 enable \"Raw Body\" on the Webhook node (see /how-to/webhooks on docs.typizer.ru)',\n  );\n}\nconst rawBody = Buffer.from(rawBodyBase64, 'base64').toString('utf8');\n\nconst msgId = headers && headers['webhook-id'];\nconst timestamp = headers && headers['webhook-timestamp'];\nconst signatureHeader = headers && headers['webhook-signature'];\nif (!msgId || !timestamp || !signatureHeader) {\n  throw new Error('missing webhook headers');\n}\nif (!/^\\d{1,15}$/.test(timestamp)) {\n  throw new Error('malformed webhook-timestamp');\n}\n\nconst nowSec = Math.floor(Date.now() / 1000);\nif (Math.abs(nowSec - Number(timestamp)) > 300) {\n  throw new Error('stale webhook-timestamp');\n}\n\nconst key = Buffer.from(secret.replace(/^whsec_/, ''), 'base64');\nconst expected = crypto\n  .createHmac('sha256', key)\n  .update(`${msgId}.${timestamp}.${rawBody}`, 'utf8')\n  .digest();\n\nlet ok = false;\nfor (const entry of signatureHeader.split(' ')) {\n  const comma = entry.indexOf(',');\n  if (comma < 0 || entry.slice(0, comma) !== 'v1') continue;\n  const given = Buffer.from(entry.slice(comma + 1), 'base64');\n  if (given.length === expected.length && crypto.timingSafeEqual(given, expected)) ok = true;\n}\nif (!ok) throw new Error('invalid webhook signature');\n\nreturn $input.all();"
      }
    }
  ],
  "connections": {
    "When clicking ‘Execute workflow’": {
      "main": [[{ "node": "Read invoice from disk", "type": "main", "index": 0 }]]
    },
    "Read invoice from disk": {
      "main": [[{ "node": "POST /v1/extract", "type": "main", "index": 0 }]]
    },
    "Webhook: приём результата": {
      "main": [[{ "node": "Проверить подпись (Code)", "type": "main", "index": 0 }]]
    }
  },
  "active": false,
  "settings": { "executionOrder": "v1" },
  "pinData": {}
}

Что дальше