API Numera Docs
Версия v1. Базовый адрес https://docs.numeraai.kz/api/v1
Начало работы
Через API ваша CRM, 1С или сайт загружает документ, отправляет его контрагенту на подпись и узнаёт результат. Подписывают люди, как и в кабинете: своей ЭЦП НУЦ РК через NCALayer, по ссылке, без регистрации. API берёт на себя всё остальное, от создания процесса до архива с подписями.
Обычная интеграция выглядит так:
- Загрузить файл:
POST /documents, или собрать документ из шаблона. - Отправить на подпись:
POST /documents/{id}/send. В ответе будут ссылки для подписантов. - Передать ссылку контрагенту. Если указать email, письмо отправим сами.
- Получить вебхук
document.completedи скачать архив с подписями.
Все запросы и ответы в JSON и UTF-8, даты в ISO 8601 (UTC). Загрузка файла принимает и multipart/form-data.
Ключи и авторизация
Ключ выпускает владелец или администратор организации в кабинете: «API и интеграции». Ключ показывается один раз, у нас хранится только его хеш. Передавайте его в заголовке:
curl https://docs.numeraai.kz/api/v1/me \
-H "Authorization: Bearer nd_live_ваш_ключ"Ключи бывают двух видов. nd_live_… работает с настоящими документами. nd_test_… работает в тестовой среде, о ней ниже. Запросы по ключу выполняются от имени сотрудника, который его выпустил, с его ролью. Если отключить сотрудника в команде, его ключи перестанут работать сразу.
Держите ключ на сервере. В коде сайта или мобильного приложения его увидит любой посетитель.
GET /me возвращает организацию, тариф, остаток лимитов за месяц и лимит запросов. Удобно для проверки ключа.
Тестовая среда
Тестовый ключ nd_test_… ходит в тот же адрес и в те же методы, но документы попадают в песочницу:
- они бесплатны и не тратят лимит тарифа (в сутки можно создать до 100);
- их не видно в кабинете и по боевому ключу, а боевые документы не видно по тестовому;
- письма подписантам не уходят;
- подписать можно только методом «подпись в песочнице»: подпись ставит тестовый удостоверяющий центр Numera, а не НУЦ РК. Ссылку из песочницы можно открыть, но настоящей ЭЦП такой документ не подписать, на странице будет пометка «Тестовый документ»;
- вебхуки тестовых документов приходят только на вебхуки тестовой среды, в теле стоит
"sandbox": true.
В остальном песочница ведёт себя как боевая среда: те же статусы, ошибки и события. Отладьте интеграцию на тестовом ключе, потом замените его на боевой.
Лимиты
API включён во все тарифы. Цены те же, что в кабинете, а лимиты зависят от тарифа:
| Start | Team | Business | |
|---|---|---|---|
| Цена в месяц | 5 000 ₸ | 10 000 ₸ | 50 000 ₸ |
| Запросов в минуту | 60 | 300 | 1 200 |
| Новых документов в месяц | 20 | 150 | 1 000 |
| Активных ключей | 2 | 5 | 20 |
| Вебхуков | 1 | 3 | 10 |
| SMS в месяц | 50 | 200 | 1 000 |
| Параллельное подписание | нет | да | да |
Документы из API и из кабинета считаются вместе. Лимит запросов общий на организацию. Ключи считаются вместе, тестовые и боевые. Вебхуков можно столько, сколько указано, отдельно для каждой среды. В каждом ответе есть заголовки X-RateLimit-Limit, X-RateLimit-Remaining и X-RateLimit-Reset (секунд до сброса окна). Если лимит превышен, придёт 429 с заголовком Retry-After: подождите указанное число секунд и повторите запрос.
Ошибки
Ошибка приходит с HTTP-кодом и телом одного формата. error стабилен, по нему удобно ветвиться в коде. message написан по-русски для человека. Если запрос не прошёл проверку схемы (validation_error), в fields перечислены поля с ошибкой.
{
"error": "documents_limit_reached",
"message": "Достигнут лимит тарифа Start: 20 новых документов в месяц"
}| Код | Когда |
|---|---|
401 unauthorized | Нет ключа, ключ неверный или отозван |
402 | Подписка неактивна или исчерпан лимит тарифа: documents_limit_reached, plan_feature_unavailable, webhooks_limit_reached, sms_limit_reached |
403 forbidden | Роли владельца ключа не хватает прав |
404 not_found | Объекта нет, или он принадлежит другой организации |
409 | Действие не подходит к текущему состоянию: active_workflow, not_your_turn, idempotency_key_reused, sandbox_document, sms_disabled, parent_not_signed, document_terminated, termination_in_progress, uploaded_signed |
413 file_too_large | Файл больше 20 МБ |
415 unsupported_file_type | Тип файла не поддерживается или в нём есть активное содержимое |
422 | Неверные данные: validation_error, invalid_base64, invalid_status, template_fields_missing, invalid_webhook_url, invalid_phone, signer_phone_missing, contract_number_required, contract_fields_required, invalid_field |
429 | rate_limited (смотрите Retry-After), sandbox_limit_reached, remind_too_often |
Документы
| POST | /documents | Загрузить файл, multipart/form-data: file, title, doc_type (CONTRACT, ACT или HR) |
| POST | /documents/base64 | То же одним JSON: title, filename, content_base64 |
| POST | /documents/from-template | Собрать документ из шаблона: template_id, title, values |
| GET | /documents | Список: ?status=SIGNED&contract_number=15/2026&relation=ADDENDUM&parent_id=…&terminated=true&limit=20&offset=0 |
| GET | /documents/{id} | Документ, процесс подписания и подписанты |
Форматы: PDF, DOCX и TXT, до 20 МБ. Тип определяется по содержимому файла, а не по расширению. PDF с JavaScript и DOCX с макросами или внешними связями не принимаются. В base64 файл занимает на треть больше места, поэтому через /documents/base64 передавайте файлы до 18 МБ, а крупнее загружайте через multipart.
Повтор без дублей. Передайте заголовок Idempotency-Key (например, номер заказа в вашей системе, от 8 символов). Если запрос оборвался и вы отправили его снова с тем же ключом, вернётся уже созданный документ, второй не появится и лимит не потратится.
curl https://docs.numeraai.kz/api/v1/documents \
-H "Authorization: Bearer $NUMERA_KEY" \
-F title="Договор поставки № 15/2026" \
-F doc_type=CONTRACT \
-F file=@dogovor.pdf{
"id": "3f6c1e2a-…",
"title": "Договор поставки № 15/2026",
"doc_type": "CONTRACT",
"status": "DRAFT",
"sandbox": false,
"version": 1,
"created_at": "2026-10-20T09:12:44Z",
"template_id": null,
"url": "https://docs.numeraai.kz/documents/3f6c1e2a-…",
"workflow": null
}Отправка на подпись
| POST | /documents/{id}/send | Создать и запустить подписание |
| POST | /documents/{id}/signers/{signer_id}/link | Новая ссылка подписанту (старая перестаёт работать) |
| POST | /documents/{id}/remind | Напомнить тем, чья очередь подошла (Team и выше) |
| POST | /documents/{id}/cancel | Отменить подписание: {"reason": "…"} |
В recipients перечислите получателей. Если указать iin, подпись примем только от сертификата с этим ИИН. Без ИИН подписать может любой, у кого есть ссылка, а в документ запишутся ФИО и ИИН из его ЭЦП. bin требует, чтобы ЭЦП была выдана на эту организацию. view_only: true отправляет документ на ознакомление без подписи.
Повтор без второго процесса. Передайте Idempotency-Key и в /send. Если ответ потерялся и вы повторили запрос с тем же ключом, придёт уже запущенный процесс с "replayed": true. Ссылки в повторном ответе не показываются, новую выдаёт /signers/{signer_id}/link.
Если вы тоже подписываете, передайте i_sign: true. Подписать можно в кабинете (сотрудник, выпустивший ключ, должен указать ИИН в профиле), а в песочнице методом из раздела ниже. При последовательном подписании получатели подписывают в порядке списка, а вы после них. Другой порядок задают order у получателей и my_order у вас. Одинаковый номер означает «одновременно», а это параллельное подписание, оно есть на тарифах Team и Business.
curl https://docs.numeraai.kz/api/v1/documents/$DOC_ID/send \
-H "Authorization: Bearer $NUMERA_KEY" \
-H "Content-Type: application/json" \
-d '{
"recipients": [
{"full_name": "ТОО «Бета Логистик»", "email": "director@beta.kz", "iin": "900101300111", "order": 1}
],
"i_sign": true,
"my_order": 2,
"expires_in_days": 14
}'{
"document": { "id": "3f6c1e2a-…", "status": "PENDING_SIGNATURE", "workflow": { … } },
"links": [
{ "signer_id": "a1b2…", "url": "https://docs.numeraai.kz/invite/Xk2…" }
]
}Если указать phone, ссылку отправим по SMS. Способ подписи задаёт sign_method, об этом в следующем разделе.
Ссылки в ответе есть только у тех, чья очередь уже подошла. Когда очередь дойдёт до следующего подписанта, придёт вебхук signer.invited. Если у подписанта есть email, письмо со ссылкой он получит сам, больше ничего делать не нужно. Если email нет, запросите ссылку методом /signers/{signer_id}/link и отправьте её в WhatsApp или Telegram. Этот метод выпускает новую ссылку, а прежнюю (в том числе ту, что ушла письмом) отключает. Ссылка показывается один раз: в базе хранится только её хеш.
/remind возвращает {"reminded": число, "invitation_links": {signer_id: url}}: внешним подписантам выпускаются свежие ссылки. /link возвращает {"signer_id", "url"}.
Подпись по SMS
Получатель с "sign_method": "sms" подписывает кодом из SMS, без ЭЦП. Для него обязательны full_name и phone (мобильный номер Казахстана). Значение any оставляет выбор за подписантом: SMS или ЭЦП, а ecp, значение по умолчанию, допускает только ЭЦП.
curl https://docs.numeraai.kz/api/v1/documents/$DOC_ID/send \
-H "Authorization: Bearer $NUMERA_KEY" -H "Content-Type: application/json" \
-d '{"recipients": [{"full_name": "Иванов Иван", "phone": "+77011234567", "sign_method": "sms"}]}'Мы отправим ссылку на этот номер. Подписант откроет документ, подтвердит согласие и введёт код. Код приходит только на указанный вами номер, изменить его подписант не может. Дальше всё как обычно: те же статусы, вебхуки, квитанция и архив.
Чем это отличается от ЭЦП. Код из SMS не является ЭЦП НУЦ РК. По статье 47 Цифрового кодекса РК это цифровое подтверждение, которое применяется по соглашению сторон: оно показывает, что код ввели на этом номере. Личность по ИИН сервис не проверяет. В signature такого подписанта стоит "method": "sms" и маска номера, а cert_iin и cert_bin содержат то, что указал отправитель. Если закон требует ЭЦП, используйте ЭЦП.
Ссылка и код считаются как отдельные SMS в лимите тарифа. Если лимит закончился, процесс не прерывается: вы получите links и событие signer.sms_failed, а запрос кода вернёт 402 sms_limit_reached. То же событие приходит, если оператор не доставил ссылку. Пока SMS не подключены, такие получатели отклоняются с 409 sms_disabled, проверить можно в sms_enabled из GET /me. В песочнице SMS не уходят, подпись ставит метод из раздела «Подпись в песочнице».
Номер договора и абонентская плата
Любой документ может хранить номер договора. Компаниям с пометкой «особая компания» (contract_fields_enabled в GET /me; пометку ставит поддержка) при создании договора обязательны номер и абонентская плата. Поля передаются в meta в JSON, а при загрузке multipart строкой с JSON в поле meta. Эти данные служат для учёта и не меняют подписанный файл.
curl https://docs.numeraai.kz/api/v1/documents/base64 \
-H "Authorization: Bearer $NUMERA_KEY" -H "Content-Type: application/json" \
-d '{"title": "Договор", "filename": "d.pdf", "content_base64": "…",
"meta": {"contract_number": "15/2026", "fields": {"monthly_fee": 150000}}}'| PATCH | /documents/{id}/fields | Заполнить или изменить: {"contract_number": "…", "fields": {"monthly_fee": 150000}}. Подходит и для документов, созданных раньше. Изменение попадает в журнал и событие document.fields_updated |
В ответе fields содержит значения самого документа, effective_fields текущие значения договора с учётом подписанных допсоглашений, а fields_history историю изменений. Для расчётов берите effective_fields. Суммы приходят строкой с двумя знаками: "150000.00".
Допсоглашения и расторжение
Соглашение заключается к договору, который подписан всеми сторонами или загружен подписанным. Это обычный документ со связью: создаёте и отправляете через /send.
# Допсоглашение: новая абонплата начнёт действовать, когда его подпишут все стороны
curl https://docs.numeraai.kz/api/v1/documents/base64 \
-H "Authorization: Bearer $NUMERA_KEY" -H "Content-Type: application/json" \
-d '{"title": "ДС № 1", "filename": "ds.pdf", "content_base64": "…",
"meta": {"relation": "ADDENDUM", "parent_document_id": "'$DOC_ID'", "fields": {"monthly_fee": 180000}}}'
# Расторжение: текст соберём сами, либо передайте filename и content_base64
curl https://docs.numeraai.kz/api/v1/documents/$DOC_ID/termination \
-H "Authorization: Bearer $NUMERA_KEY" -H "Content-Type: application/json" \
-d '{"reason": "По соглашению сторон", "effective_on": "2026-12-31"}'Номер договора наследуется, если не указан другой. Когда все стороны подпишут расторжение, у договора станет "lifecycle": "TERMINATED" и появится terminated_at; его статус подписания остаётся SIGNED. Незавершённые допсоглашения при этом отменяются. Если соглашение отклонили, договор продолжает действовать. В GET /documents/{id} есть parent_id и children.
Для соглашений приходят события с данными договора: document.addendum_signed с полем addendum_id и document.terminated с termination_id, reason и effective_on. В обоих data.document содержит сам договор и актуальные effective_fields.
Подписанные документы
| POST | /documents/signed | Загрузить документ, подписанный вне сервиса: title, filename, content_base64, meta с signed_on, counterparty_name, counterparty_bin, contract_number |
Такой документ хранится для учёта и поиска со статусом UPLOADED_SIGNED и не тратит лимит новых документов. Мы не проверяем подписи в файле, поэтому квитанции и публичной проверки для него нет, а на подпись его не отправить. Допсоглашения и расторжение к нему работают. Загруженное подписанным соглашение (relation и parent_document_id в meta) вступает в силу сразу.
Статусы
Статус документа (status) равен статусу его последнего процесса подписания:
DRAFT | Загружен, на подпись не отправлялся |
PENDING_SIGNATURE | Отправлен, подписей пока нет |
PARTIALLY_SIGNED | Подписали не все |
SIGNED | Подписан всеми обязательными подписантами |
SHARED | Отправлен только на ознакомление |
REJECTED | Обязательный подписант отклонил, причина в closed_reason |
CANCELLED | Отменён отправителем |
EXPIRED | Срок подписания истёк |
VALIDATION_FAILED | Повторная проверка обнаружила, что подпись или файл повреждены |
UPLOADED_SIGNED | Загружен уже подписанным, для учёта |
Статус подписанта: WAITING (очередь не подошла), PENDING (может подписать), SIGNED, REJECTED, SKIPPED (документ завершён без его подписи). У подписавшего в signature лежат способ подписи method и данные сертификата: ФИО, ИИН, БИН и серийный номер. Расторгнут ли договор, показывает отдельное поле lifecycle: ACTIVE или TERMINATED.
Скачивание
| GET | /documents/{id}/download | Исходный файл текущей версии |
| GET | /documents/{id}/download?kind=archive | ZIP: документ, подписи CMS, сертификаты, протокол проверки и скрипт независимой проверки |
| GET | /documents/{id}/download?kind=stamped | PDF с DOC ID и QR-кодом проверки на каждой странице и квитанцией в конце (для PDF-оригиналов) |
| GET | /documents/{id}/download?kind=receipt | Квитанция о подписании (PDF): кто подписал, когда, тип ЭЦП, хеши файлов |
| GET | /documents/{id}/download?kind=files | Оригинал и приложения без подписей (несколько файлов — ZIP) |
Архив удобно класть в вашу систему как доказательство подписания: его можно проверить OpenSSL или NCANode без нашего участия.
Шаблоны
| GET | /templates | Системные шаблоны и шаблоны вашей организации |
| GET | /templates/{id} | Шаблон с текстом и списком полей |
Шаблоны создаются в кабинете, раздел «Шаблоны». Поле в тексте выглядит как {{summa_dogovora}}. Через API передайте значения по ключам полей:
curl https://docs.numeraai.kz/api/v1/documents/from-template \
-H "Authorization: Bearer $NUMERA_KEY" -H "Content-Type: application/json" \
-d '{"template_id": "…", "title": "Акт № 7", "values": {"summa_dogovora": "1 500 000 тенге"}}'Если не заполнить обязательное поле, придёт 422 template_fields_missing со списком полей.
Вебхуки
Вебхук сообщает вашему серверу о событии по документу, и опрашивать API не нужно. Адрес должен быть публичным, работать по https на порту 443 или 8443 и не содержать логина и пароля. Редиректы мы не выполняем.
| GET | /webhooks | Вебхуки текущей среды и список событий |
| POST | /webhooks | Добавить: url и необязательный events (пусто значит все). В ответе секрет whsec_…, он показывается один раз |
| DELETE | /webhooks/{id} | Удалить |
| POST | /webhooks/{id}/test | Отправить событие ping (до 10 в минуту) |
| GET | /webhooks/{id}/deliveries | Журнал доставок: статус, попытки, ответ вашего сервера. Хранится 30 дней |
| POST | /webhooks/{id}/deliveries/{delivery_id}/redeliver | Отправить событие ещё раз (тот же id события) |
Вебхуки настраивает роль с правом управлять настройками (владелец или администратор). Тестовый ключ видит только вебхуки песочницы, боевой только боевые.
document.created | Документ загружен |
document.sent | Отправлен на подпись |
signer.invited | Очередь дошла до подписанта, ссылка выпущена |
signer.opened | Подписант открыл ссылку |
signer.signed | Подписант подписал |
signer.signature_invalid | Подпись не прошла проверку (неверный ключ, чужой ИИН, отозванный сертификат) |
document.completed | Документ подписан всеми |
document.addendum_signed | Допсоглашение подписано всеми, абонплата могла измениться |
document.terminated | Договор расторгнут |
document.uploaded_signed | Загружен уже подписанный документ |
document.fields_updated | Изменены номер или учётные поля |
signer.sms_failed | SMS со ссылкой не доставлено или закончился лимит: отправьте ссылку другим способом |
document.rejected | Подписант отклонил документ |
document.cancelled | Вы отменили подписание |
document.expired | Срок подписания истёк |
ping | Тестовое событие из кабинета или POST /webhooks/{id}/test |
POST https://crm.example.kz/numera/webhook
X-Numera-Event: signer.signed
X-Numera-Delivery: 5b1e…
X-Numera-Signature: t=1760951564,v1=8f3c…
{
"id": "evt_9c2f…",
"seq": 18342,
"type": "signer.signed",
"created_at": "2026-10-20T09:32:44Z",
"sandbox": false,
"data": {
"document": { "id": "3f6c1e2a-…", "title": "Договор поставки № 15/2026", "status": "PARTIALLY_SIGNED" },
"workflow_id": "c0de…",
"signer": { "id": "a1b2…", "full_name": "Нурланова Динара", "email": "dinara@beta.kz", "status": "SIGNED", "signed_at": "2026-10-20T09:32:43Z" }
}
}У document.rejected и document.cancelled в data.reason лежит причина. Ответьте кодом 2xx в течение 10 секунд, а долгую обработку делайте после ответа. Если ответ другой или его нет, повторим через 1 минуту, 5 минут, 30 минут, 2, 6, 12 часов и затем раз в сутки трижды, всего около 4 суток. Одно событие может прийти дважды, поэтому сверяйте id. seq — сквозной номер события: по нему удобно упорядочивать события и дочитывать пропущенные из ленты. Порядок доставки не гарантирован: актуальный статус всегда есть в data.document.status и в GET /documents/{id}. Журнал доставок виден в кабинете.
Проверяйте подпись, иначе кто угодно сможет прислать вашему серверу поддельное событие. Секрет whsec_… выдаётся при создании вебхука. Подпись считается так: HMAC-SHA256 от строки {t}.{тело запроса}, где t взят из заголовка. Сравнивайте с v1 и отклоняйте запросы старше 5 минут.
import hashlib, hmac, time
def verify(secret: str, header: str, body: bytes) -> bool:
parts = dict(x.split("=", 1) for x in header.split(","))
t, sig = parts.get("t", ""), parts.get("v1", "")
if not t.isdigit() or abs(time.time() - int(t)) > 300:
return False
expected = hmac.new(secret.encode(), f"{t}.".encode() + body, hashlib.sha256).hexdigest()
return hmac.compare_digest(expected, sig)
# Flask:
# ok = verify(WHSEC, request.headers["X-Numera-Signature"], request.get_data())Считайте подпись по сырому телу запроса, а не по JSON, который ваш фреймворк уже разобрал и собрал заново.
Лента событий
| GET | /events?after={seq}&limit=100 | События по порядку seq: те же объекты, что приходят вебхуком. Хранятся 90 дней |
Лента пишется всегда, даже если вебхук не настроен. Сохраните seq последнего обработанного события и запрашивайте after=<seq>: придут только новые. Пока has_more равен true, повторяйте запрос с after=next_after. Фильтр по документу: document_id. Тестовый ключ видит события песочницы, боевой только боевые.
{
"data": [ { "id": "evt_…", "seq": 18342, "type": "signer.signed", "created_at": "…", "sandbox": false, "data": { … } } ],
"has_more": false,
"next_after": 18342
}Надёжная интеграция
- Создание и отправка с
Idempotency-Key. Используйте свой стабильный идентификатор (номер заказа или сделки). Повтор после таймаута или 5xx не создаст дубль. - Вебхук: быстрый ответ и дедупликация. Проверьте подпись, сохраните событие по
id(уникальный ключ), ответьте 2xx, обработку делайте отдельно. - Догоняющее чтение. При старте сервиса и раз в 5–15 минут читайте
/events?after=<последний seq>. Так вы получите всё, что пропустили, даже если сервер лежал дольше 4 суток повторов (до 90 дней). - Источник истины —
GET /documents/{id}. Решения «подписан / отклонён» принимайте по статусу документа, а не по порядку пришедших событий. - Повторы ваших запросов. На
429ждитеRetry-After, на5xxи сетевые ошибки повторяйте с паузой 1, 2, 4… секунды;4xx(кроме 429) не повторяйте.
Подпись в песочнице
| POST | /sandbox/documents/{id}/signers/{signer_id}/sign | Подписать за подписанта тестовой ЭЦП, а если выбран sms — без отправки и проверки кода |
| POST | /sandbox/documents/{id}/signers/{signer_id}/reject | Отклонить за подписанта: {"reason": "…"} |
Методы работают только с тестовым ключом. Подпись проходит обычную проверку: статусы меняются, вебхуки приходят, архив собирается. В подписи стоит "test": true, а в архиве пометка, что это не ЭЦП НУЦ РК. Юридической силы такая подпись не имеет.
Пример целиком
Скрипт на Python загружает договор в песочницу, отправляет его контрагенту, подписывает за обе стороны и скачивает архив. Замените ключ на свой тестовый.
import requests
API = "https://docs.numeraai.kz/api/v1"
H = {"Authorization": "Bearer nd_test_ваш_ключ"}
doc = requests.post(f"{API}/documents", headers=H,
data={"title": "Тестовый договор"},
files={"file": open("dogovor.pdf", "rb")}).json()
sent = requests.post(f"{API}/documents/{doc['id']}/send", headers=H, json={
"recipients": [{"full_name": "ТОО «Бета»", "iin": "900101300111", "order": 1}],
"i_sign": True, "my_order": 2,
}).json()
for s in sent["document"]["workflow"]["signers"]:
r = requests.post(f"{API}/sandbox/documents/{doc['id']}/signers/{s['id']}/sign", headers=H)
print(s["full_name"], "→", r.json()["status"]) # PARTIALLY_SIGNED, затем SIGNED
zip_ = requests.get(f"{API}/documents/{doc['id']}/download", headers=H, params={"kind": "archive"})
open("archive.zip", "wb").write(zip_.content)Вопросы по интеграции присылайте на info@numeraai.kz или в чат поддержки в кабинете. Клиентам тарифа Business помогаем с подключением.