Коротко
При создании счёта есть два поля, в которые вы кладёте данные своей системы:
externalOrderId— ваш номер заказа. Короткая строка, видна в кабинете, по ней работает поиск, возвращается в вебхуке.metadata— произвольный JSON-объект. Покупателю не виден, в поиск не попадает, но возвращается и в вебхуке, и при запросе статуса — ровно в том виде, в каком вы его отправили.
Правило одно на оба поля: не кладите туда секреты. metadata не шифруется, и её видит любой сотрудник, у которого есть доступ в кабинет.
externalOrderId
Это номер заказа в вашем магазине или CRM. В Kaspi он не уходит, покупатель его не видит.
{
"amount": 12500,
"description": "Заказ №4471",
"externalOrderId": "4471"
}
Что он даёт:
| Возможность | Как работает |
|---|---|
| Возврат в вебхуке | Приходит в поле invoice.externalOrderId — сразу понятно, какой заказ закрывать |
| Поиск | GET /api/v1/invoices?externalOrderId=4471 вернёт счета по этому заказу |
| Видимость в кабинете | Показывается в списке счетов и в карточке счёта |
| Экспорт | Попадает в колонку CSV — удобно сверять с бухгалтерией |
На один заказ может приходиться несколько счетов: первый истёк, покупатель оплатил со второй попытки. Поэтому externalOrderId — не уникальный ключ. От дублей защищает отдельный заголовок Idempotency-Key: Идемпотентность.
Что писать: номер заказа, код брони, номер счёта-фактуры. Что не писать: телефон, ИИН, e-mail покупателя — для них есть поле customer.
metadata
metadata — произвольный JSON-объект. Мы в него не заглядываем и ничего в нём не проверяем: просто сохраняем и возвращаем обратно.
{
"amount": 3000,
"kind": "qr",
"description": "Вода, 19 л",
"externalOrderId": "4471",
"metadata": {
"machineId": "vnd-07",
"slot": "A3",
"branch": "almaty-abay",
"courierId": 22,
"source": "telegram-bot"
}
}
Этот объект в неизменном виде:
- приходит в поле
invoice.metadataвебхуковinvoice.paidи остальных событий; - есть в ответе
GET /api/v1/invoices/{id}; - виден в карточке счёта в кабинете.
Благодаря этому, получив уведомление об оплате, вам не нужно лезть в свою базу, чтобы понять, какую ячейку какого автомата открывать.
Что класть можно
| Пример | Зачем |
|---|---|
orderLineId, dealId | Внутренний идентификатор в CRM |
machineId, slot, terminalId | Вендинговый автомат, ячейка, устройство |
courierId, routeId | Курьер и маршрут — оплата в момент доставки |
branch, pointId | Точка, филиал — чтобы развести отчётность |
source, campaign | Источник трафика, метка кампании |
tableNo, waiterId | Стол и официант в ресторане |
cartHash, attempt | Внутренние технические пометки |
Принцип: здесь должны лежать только технические метки вашей системы.
Что нельзя класть никогда
| Не кладите | Почему |
|---|---|
| Пароли, токены, API-ключи | Их увидит любой сотрудник с доступом в кабинет |
| Номер карты, CVV, банковские реквизиты | У нас нет оснований это хранить, а для вас это прямой риск |
| ИИН, БИН, номер документа | Лишние персональные данные, которые не нужны для платежа |
| Адрес покупателя, диагноз, текст обращения | Персональные и чувствительные данные |
| Всё содержимое корзины, длинные логи | metadata — не база данных; кладите идентификатор, а не дамп |
Если нужны имя, телефон или e-mail покупателя — им место в отдельном поле customer, а не в metadata: только тогда корректно отработают чек и уведомление. Что именно хранится: Данные и конфиденциальность.
Размер
metadata должна оставаться компактной: несколько десятков полей, значения — короткие строки и числа. Если отправлять десятки килобайт JSON, создание счёта замедлится, а тело вебхука станет тяжёлым. Длинный текст держите у себя, а сюда кладите только его идентификатор.
Как это возвращается в вебхуке
{
"event": "invoice.paid",
"invoice": {
"id": "inv_01J8…",
"externalOrderId": "4471",
"status": "paid",
"amount": 3000,
"metadata": { "machineId": "vnd-07", "slot": "A3" },
"receiptUrl": "https://…"
},
"sentAt": "2026-09-14T09:12:04.000Z"
}
Пример обработки:
if (event === 'invoice.paid') {
const { externalOrderId, metadata } = invoice;
await markOrderPaid(externalOrderId); // закрываем заказ
if (metadata?.machineId) openSlot(metadata.machineId, metadata.slot);
}
Одно и то же событие может прийти дважды, поэтому обработчик должен быть идемпотентным — проверяйте по паре (invoice.id, status).
У счетов, выставленных по подписке, в metadata автоматически появляются поля subscriptionId и run. Не занимайте эти имена своими данными.
Что выбрать
| Вопрос | Ответ |
|---|---|
| Хочу искать из кабинета | externalOrderId |
| Должно попасть в выгрузку CSV | externalOrderId |
| Хочу, чтобы увидел покупатель | Ни то, ни другое — пишите в description |
| Достаточно, чтобы вернулось в вебхуке | metadata |
| Нужно несколько значений | metadata |
Вопросы и ответы
Можно ли искать по metadata? Нет. Поиск работает по externalOrderId. Значение, по которому нужно искать, выносите туда.
Можно ли изменить metadata позже? Нет, она фиксируется в момент создания счёта. Новые данные храните у себя.
Видит ли покупатель metadata? Нет. Ни на странице оплаты, ни в приложении Kaspi, ни в чеке. Покупатель видит текст description, сумму и продавца.
Должен ли externalOrderId быть уникальным? Нет. Несколько счетов на один заказ — нормальная ситуация. От дублей защищает Idempotency-Key.
Можно ли положить в metadata массив или вложенный объект? Да, любой валидный JSON. Но обработку сильно упрощает плоский короткий объект.