# Metadata и номер заказа

> Чем externalOrderId отличается от metadata, как оба поля возвращаются в вебхуке, что можно класть в metadata и что туда нельзя класть никогда — с конкретными примерами.

## Коротко

При создании счёта есть два поля, в которые вы кладёте данные своей системы:

- **`externalOrderId`** — ваш номер заказа. Короткая строка, видна в кабинете, по ней работает поиск, возвращается в вебхуке.
- **`metadata`** — произвольный JSON-объект. Покупателю не виден, в поиск не попадает, но возвращается и в вебхуке, и при запросе статуса — ровно в том виде, в каком вы его отправили.

Правило одно на оба поля: **не кладите туда секреты.** `metadata` не шифруется, и её видит любой сотрудник, у которого есть доступ в кабинет.

## externalOrderId

Это номер заказа в вашем магазине или CRM. В Kaspi он не уходит, покупатель его не видит.

```json
{
  "amount": 12500,
  "description": "Заказ №4471",
  "externalOrderId": "4471"
}
```

Что он даёт:

| Возможность | Как работает |
|---|---|
| Возврат в вебхуке | Приходит в поле `invoice.externalOrderId` — сразу понятно, какой заказ закрывать |
| Поиск | `GET /api/v1/invoices?externalOrderId=4471` вернёт счета по этому заказу |
| Видимость в кабинете | Показывается в списке счетов и в карточке счёта |
| Экспорт | Попадает в колонку CSV — удобно сверять с бухгалтерией |

На один заказ может приходиться несколько счетов: первый истёк, покупатель оплатил со второй попытки. Поэтому `externalOrderId` — не уникальный ключ. От дублей защищает отдельный заголовок `Idempotency-Key`: [Идемпотентность](/kb/ru/idempotency).

Что писать: номер заказа, код брони, номер счёта-фактуры. Что не писать: телефон, ИИН, e-mail покупателя — для них есть поле `customer`.

## metadata

`metadata` — произвольный JSON-объект. Мы в него не заглядываем и ничего в нём не проверяем: просто сохраняем и возвращаем обратно.

```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`: только тогда корректно отработают чек и уведомление. Что именно хранится: [Данные и конфиденциальность](/kb/ru/data-and-privacy).

### Размер

`metadata` должна оставаться компактной: несколько десятков полей, значения — короткие строки и числа. Если отправлять десятки килобайт JSON, создание счёта замедлится, а тело вебхука станет тяжёлым. Длинный текст держите у себя, а сюда кладите только его идентификатор.

## Как это возвращается в вебхуке

```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"
}
```

Пример обработки:

```js
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. Но обработку сильно упрощает плоский короткий объект.
