Интеграция с 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:
{
"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": {}
}Что дальше
- «Приём вебхуков» — полное описание заголовков, повторов и проверки подписи.
- «Интеграция с Make» — тот же сценарий в Make.