@openaisdk/billing-sdk-node 1.3.0 → 1.11.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -1,268 +1,59 @@
1
- # @openaisdk/billing-sdk-node
1
+ # `@openaisdk/billing-sdk-node`
2
2
 
3
- Ручной публичный Node.js SDK для Mega-Billing consumer API.
3
+ Библиотека для Node.js: ваш SaaS принимает оплату и открывает фичи через MegaBilling.
4
4
 
5
- ## За что отвечает
5
+ Нужны личный кабинет MegaBilling и npm. Node.js ≥ 20.
6
6
 
7
- - Узкий контракт для backend вашего SaaS: клиенты, checkout, доступ, подписки, usage, чтение счетов, проверка подписи вебхуков.
8
- - Аутентификация только `Authorization: Bearer <integration key>`. Tenant и project из ключа, не из заголовков.
9
- - Типы и ошибки, которые видит интегратор. Поведение совпадает с `GET /api/public-json`.
7
+ ---
10
8
 
11
- ## За что не отвечает
9
+ ## Подготовка в кабинете
12
10
 
13
- - Полный admin/internal OpenAPI (ключи, провайдер, void счёта) сгенерированный [`billing-sdk`](../billing-sdk/README.md) или Admin UI.
14
- - MCP для агентов — [`billing-catalog-mcp`](../billing-catalog-mcp/README.md).
15
- - Доменные пакеты платформы (`*-domain`): SDK только HTTP-клиент.
16
- - Хранение секретов, ретраи провайдера, фискализация.
11
+ Сделайте это до кода. Иначе SDK не к чему подключаться.
17
12
 
18
- Пакет намеренно открывает узкую consumer-facing поверхность:
13
+ 1. **Тарифы и цены.** Заведите хотя бы один активный тариф и цену. Добавьте фичи, которые будете проверять в продукте.
14
+ 2. **URL сервера для событий.** HTTPS-адрес вашего бэкенда (например `https://api.myapp.com/billing/webhook`). Туда MegaBilling шлёт JSON при оплате и смене подписки.
15
+ 3. **Секрет подписи.** Нужен, чтобы через SDK убедиться, что запрос от MegaBilling.
16
+ 4. **Ключ API.** Тестовый `bsk_test_…` или боевой `bsk_live_…` — код тот же. Для проверки берите тестовый (`BILLING_API_KEY`). Поле `livemode` в ответах и событиях показывает, тест это или бой.
19
17
 
20
- | Ресурс | Методы |
21
- | ------------------- | -------------------------------------------- |
22
- | `customers` | `create`, `retrieve`, `list`, `update` |
23
- | `subscriptions` | `retrieve`, `list`, `cancel` |
24
- | `checkout.sessions` | `create` |
25
- | `access` | `retrieve` |
26
- | `meterEvents` | `create`, `summary` |
27
- | `invoices` | `retrieve`, `listCreditNotes` |
28
- | `webhooks` | `constructEvent`, `generateTestHeaderString` |
18
+ ---
29
19
 
30
- Wave 1 (`customers.create`, `checkout.sessions.create`, `access.retrieve`) — реализованный golden path. Customer & Subscription Lifecycle (`customers.retrieve/list/update`, `subscriptions.*`), thin Invoice Read (`invoices.retrieve`, `invoices.listCreditNotes`) и Wave 4 Usage (`meterEvents.create`, `meterEvents.summary`) следуют зафиксированному public contract: customer, invoice и meter остаются публичными идентификаторами, а replay usage-события задаётся body-полем `identifier`, а не внутренними UUID. Void / credit note create остаются admin-only.
20
+ ## Шаги интеграции
31
21
 
32
- Аутентификация только через `Authorization: Bearer <integration key>`. В опциях клиента и сигнатурах методов нет `x-tenant-id`, `xTenantId` или `projectId` — tenant и project определяются самим ключом.
33
-
34
- Требования: Node.js ≥ 20.
35
-
36
- ## Установка
22
+ ### 1. Установить пакет
37
23
 
38
24
  ```bash
39
25
  pnpm add @openaisdk/billing-sdk-node
26
+ # или: npm i @openaisdk/billing-sdk-node
40
27
  ```
41
28
 
42
- ## Инициализация
43
-
44
- Для hosted-окружения достаточно integration key вида `bsk_test_...` или `bsk_live_...`. Для local / self-hosted задайте `BILLING_API_URL` в окружении или передайте `baseUrl` явно.
45
-
46
29
  ```ts
47
- import { BillingError, MegaBilling } from '@openaisdk/billing-sdk-node';
30
+ import { MegaBilling } from '@openaisdk/billing-sdk-node';
48
31
 
49
32
  const billing = new MegaBilling({
50
33
  apiKey: process.env.BILLING_API_KEY!,
51
- // baseUrl: 'http://127.0.0.1:4001', // опционально для local/self-hosted
52
- // timeoutMs: 30_000, // опционально, по умолчанию 30s
53
34
  });
54
35
  ```
55
36
 
56
- ## Sandbox / live contract
57
-
58
- Wave 5 Sandbox не требует второго consumer deploy и не вводит дополнительных env headers:
59
-
60
- - один и тот же `baseUrl` обслуживает и test, и live;
61
- - режим задаётся самим Bearer key: `bsk_test_...` для sandbox/test и `bsk_live_...` для live;
62
- - consumer не передаёт `x-tenant-id`, `x-project-id`, `x-env` или аналогичный mode header;
63
- - все consumer-facing resource responses и outbound webhook events несут `livemode: boolean`, чтобы приложение могло явно различать test и live данные.
37
+ Параметр `baseUrl` или `BILLING_API_URL` — только если API на своём хосте.
64
38
 
65
- Практический migration path: для перехода из sandbox в live достаточно подставить live key и повторить smoke flow на связанном live project.
39
+ ### 2. Каталог
66
40
 
67
- ## Golden path
68
-
69
- Типичный сценарий consumer-backend: создать customer → открыть checkout → прочитать access state.
41
+ Тарифы `products`, цены — `prices`. Код фичи для `access.check` — `Feature.code`.
70
42
 
71
43
  ```ts
72
- import { BillingError, MegaBilling } from '@openaisdk/billing-sdk-node';
73
-
74
- const billing = new MegaBilling({
75
- apiKey: process.env.BILLING_API_KEY!,
76
- });
44
+ const products = await billing.products.list({ active: true, limit: 20 });
45
+ const product = products.data[0]!;
46
+ const prices = await billing.prices.list({ product: product.id, active: true });
47
+ const features = await billing.features.list();
77
48
 
78
- const workspace = {
79
- id: 'ws_123',
80
- name: 'MegaRetro Workspace',
81
- ownerEmail: 'owner@example.com',
82
- };
83
-
84
- async function main() {
85
- const customer = await billing.customers.create(
86
- {
87
- externalType: 'workspace',
88
- externalId: workspace.id,
89
- email: workspace.ownerEmail,
90
- name: workspace.name,
91
- },
92
- {
93
- idempotencyKey: `customer:${workspace.id}`,
94
- }
95
- );
96
-
97
- const checkout = await billing.checkout.sessions.create(
98
- {
99
- customer: customer.id,
100
- price: 'price_pro_monthly',
101
- successUrl: 'https://app.example.com/billing/success',
102
- cancelUrl: 'https://app.example.com/billing',
103
- },
104
- {
105
- idempotencyKey: `checkout:${workspace.id}:price_pro_monthly:v1`,
106
- }
107
- );
108
-
109
- const access = await billing.access.retrieve(customer.id);
110
-
111
- console.log(customer.requestId, checkout.requestId, access.requestId);
112
- console.log(customer.livemode, checkout.livemode, access.livemode);
113
- console.log(checkout.confirmationUrl);
114
- console.log(access.status, access.features, access.limits);
115
- }
116
-
117
- main().catch((error) => {
118
- if (error instanceof BillingError) {
119
- console.error(error.code, error.status, error.requestId, error.param);
120
- return;
121
- }
122
-
123
- throw error;
124
- });
49
+ console.log(product.name, prices.data[0]?.unitAmountMinor, features.data[0]?.code);
125
50
  ```
126
51
 
127
- Запускаемый пример из репозитория:
52
+ ### 3. Создать клиента
128
53
 
129
- ```bash
130
- BILLING_API_KEY=bsk_test_... \
131
- BILLING_API_URL=http://127.0.0.1:4001 \
132
- BILLING_PRICE_ID=price_pro_monthly \
133
- pnpm --filter @openaisdk/billing-sdk-node run quickstart
134
- ```
135
-
136
- ## Lifecycle-сценарий
54
+ Свяжите пользователя с MegaBilling через `externalType` + `externalId`. Передайте `idempotencyKey`, чтобы повтор не создал второго клиента.
137
55
 
138
- Типичный экран «Биллинг» в consumer-продукте: показать текущую подписку и дать отменить её в конце периода.
139
-
140
- ```ts
141
- const customer = await billing.customers.retrieve('cus_123');
142
- const subscriptions = await billing.subscriptions.list({
143
- customer: customer.id,
144
- status: 'active',
145
- });
146
-
147
- const [current] = subscriptions.data;
148
-
149
- if (current) {
150
- await billing.subscriptions.cancel(
151
- current.id,
152
- { atPeriodEnd: true },
153
- { idempotencyKey: `subscription-cancel:${current.id}:v1` }
154
- );
155
- }
156
-
157
- const access = await billing.access.retrieve(customer.id);
158
- ```
159
-
160
- Отмена в конце периода не снимает доступ немедленно: до `currentPeriodEnd` `billing.access.retrieve(...)` продолжит возвращать активный доступ.
161
-
162
- ## Usage-based billing
163
-
164
- Типичный metered-flow в consumer backend: отправить usage по публичному customer и затем прочитать usage summary по публичному meter.
165
-
166
- ```ts
167
- const acceptedEvent = await billing.meterEvents.create({
168
- eventName: 'ai_tokens',
169
- customer: 'cus_123',
170
- value: 1250,
171
- identifier: 'generation_gen_987',
172
- timestamp: '2026-08-10T00:00:00.000Z',
173
- dimensions: { model: 'gpt-5' },
174
- });
175
-
176
- const summary = await billing.meterEvents.summary('ai_tokens', {
177
- customer: 'cus_123',
178
- startTime: '2026-08-10T00:00:00.000Z',
179
- endTime: '2026-08-10T01:00:00.000Z',
180
- valueGroupingWindow: 'hour',
181
- });
182
-
183
- console.log(acceptedEvent.id, acceptedEvent.status, acceptedEvent.requestId);
184
- console.log(summary.data[0]?.aggregatedValue, summary.requestId);
185
- ```
186
-
187
- Правила:
188
-
189
- - `eventName` и аргумент `meterId` в `summary(...)` используют публичный meter code, а не внутренний UUID;
190
- - `identifier` обязателен и задаёт replay/dedup contract для create: повтор с тем же `identifier` и тем же нормализованным payload (customer, meter, value, dimensions и — только если он был передан явно — `timestamp`) возвращает тот же event, а другой payload — `idempotency_key_conflict`;
191
- - явный `timestamp` — часть payload fingerprint, поэтому его отсутствие в исходном запросе не мешает retry-safe повтору без `timestamp`: сервер каждый раз подставляет текущее время, но это поле в такой повтор не входит, поэтому ложного конфликта не возникает;
192
- - открытые события (без `subscriptionId` — SDK его и не принимает) попадают в расчёт нужной подписки по `project + customer + meter` в момент выставления счёта, а не по значению, переданному клиентом;
193
- - `value` можно передать числом или строкой с неотрицательным целым значением;
194
- - `valueGroupingWindow` поддерживает `hour` и `day`; для группировки окно должно быть выровнено по UTC-границам;
195
- - сервер может принять событие асинхронно, поэтому `create(...)` возвращает `status: 'accepted'`.
196
-
197
- ## Webhooks
198
-
199
- Wave 3 добавляет consumer-facing helper для проверки webhook signature поверх уже существующей endpoint/delivery/replay infrastructure.
200
-
201
- Поддерживаемый signature format:
202
-
203
- ```txt
204
- x-billing-signature: t=1760000000,v1=<hex-hmac>
205
- ```
206
-
207
- - подпись считается по строке `"<timestamp>.<rawBody>"`;
208
- - по умолчанию SDK использует tolerance window `300` секунд;
209
- - проверять нужно именно raw body до `JSON.parse(...)`;
210
- - event envelope содержит `livemode`, совпадающий с mode проекта-источника;
211
- - дедупликация consumer-side handler должна идти по `event.id`;
212
- - test event использует тот же `v1` scheme, что и production deliveries.
213
-
214
- Поддерживаемые event types: `billing.test`, `subscription.*`, `invoice.*`, `payment.failed`, `payment.refunded`, `entitlement.updated`, `entitlement.granted`, `entitlement.revoked`.
215
-
216
- ### `billing.webhooks.constructEvent(rawBody, signature, secret, options?)`
217
-
218
- ```ts
219
- import { BillingWebhookVerificationError, MegaBilling } from '@openaisdk/billing-sdk-node';
220
-
221
- const billing = new MegaBilling({
222
- apiKey: process.env.BILLING_API_KEY!,
223
- });
224
-
225
- const rawBody = request.rawBody.toString('utf8');
226
- const signature = request.headers['x-billing-signature'];
227
-
228
- try {
229
- const event = billing.webhooks.constructEvent(
230
- rawBody,
231
- Array.isArray(signature) ? signature[0] : signature,
232
- process.env.MEGABILLING_WEBHOOK_SECRET!
233
- );
234
-
235
- console.log(event.livemode ? 'live webhook' : 'test webhook');
236
-
237
- if (event.type === 'entitlement.updated') {
238
- const access = await billing.access.retrieve(event.data.customer!);
239
- console.log(access.status, access.features, access.limits);
240
- }
241
- } catch (error) {
242
- if (error instanceof BillingWebhookVerificationError) {
243
- console.error(error.code, error.message);
244
- }
245
- }
246
- ```
247
-
248
- `options`:
249
-
250
- | Поле | По умолчанию | Назначение |
251
- | ------------------ | ------------ | --------------------------------------------- |
252
- | `toleranceSeconds` | `300` | Максимальное допустимое расхождение timestamp |
253
- | `receivedAt` | `Date.now()` | Позволяет стабильно тестировать верификацию |
254
-
255
- ### `billing.webhooks.generateTestHeaderString(rawBody, secret, timestamp?)`
256
-
257
- Helper для локальных тестов и unit/integration specs. Генерирует header в формате `t=...,v1=...`, совместимый с `constructEvent(...)`.
258
-
259
- ## API
260
-
261
- ### `billing.customers.create(params, options?)`
262
-
263
- `POST /v1/customers`
264
-
265
- Создаёт (или идемпотентно возвращает) customer, связанный с сущностью consumer-а через `externalType` + `externalId`.
56
+ `externalType`: `'workspace' | 'tenant' | 'company' | 'org'`.
266
57
 
267
58
  ```ts
268
59
  const customer = await billing.customers.create(
@@ -270,350 +61,174 @@ const customer = await billing.customers.create(
270
61
  externalType: 'workspace',
271
62
  externalId: 'ws_123',
272
63
  email: 'owner@example.com',
273
- name: 'MegaRetro Workspace',
274
- phone: '+79990000000',
64
+ name: 'Acme Workspace',
275
65
  },
276
66
  { idempotencyKey: 'customer:ws_123' }
277
67
  );
278
68
  ```
279
69
 
280
- `externalType`: `'workspace' | 'tenant' | 'company' | 'org'`. Поля `email`, `name` и `phone` опциональны. Ответ содержит `livemode`, чтобы consumer мог не смешивать sandbox/live customers в собственных логах и диагностике.
70
+ ### 4. Открыть оплату
281
71
 
282
- ### `billing.customers.retrieve(customerId, options?)`
283
-
284
- `GET /v1/customers/{customerId}`
285
-
286
- Читает текущее состояние customer по публичному `cus_...` идентификатору.
72
+ `checkout.sessions.create` → отправьте человека на `confirmationUrl`. Для create/checkout передавайте `idempotencyKey`.
287
73
 
288
74
  ```ts
289
- const customer = await billing.customers.retrieve('cus_123');
290
- ```
291
-
292
- ### `billing.customers.list(params?, options?)`
293
-
294
- `GET /v1/customers`
295
-
296
- Возвращает страницу customers проекта, определяемого integration key.
297
-
298
- ```ts
299
- const customers = await billing.customers.list({
300
- limit: 20,
301
- startingAfter: 'cus_100',
302
- externalType: 'workspace',
303
- // externalId: 'ws_123',
304
- });
305
-
306
- for (const customer of customers.data) {
307
- console.log(customer.id, customer.externalId);
308
- }
75
+ const checkout = await billing.checkout.sessions.create(
76
+ {
77
+ customer: customer.id,
78
+ price: prices.data[0]!.id,
79
+ successUrl: 'https://app.example.com/billing/success',
80
+ cancelUrl: 'https://app.example.com/billing',
81
+ },
82
+ { idempotencyKey: `checkout:${customer.id}:${prices.data[0]!.id}:v1` }
83
+ );
309
84
 
310
- console.log(customers.hasMore);
85
+ redirect(checkout.confirmationUrl);
311
86
  ```
312
87
 
313
- Ответ list envelope: `{ object: 'list', data: Customer[], hasMore: boolean }` плюс `requestId`. Все параметры опциональны; `undefined` в query не отправляется.
88
+ ### 5. Принять событие на своём сервере
314
89
 
315
- ### `billing.customers.update(customerId, payload, options?)`
90
+ `successUrl` — только UX. **Не** открывайте доступ и **не** меняйте подписку по факту возврата на success-страницу. Правда приходит отдельным HTTP POST на URL из кабинета.
316
91
 
317
- `PATCH /v1/customers/{customerId}`
318
-
319
- Обновляет изменяемые контактные поля customer. `externalType` и `externalId` — иммутабельная связка с сущностью consumer-а и через update не меняются. `null` очищает контактное поле, отсутствие поля оставляет текущее значение без изменений.
92
+ 1. Сырое тело запроса (до `JSON.parse`).
93
+ 2. `webhooks.constructEvent` + секрет + заголовок `x-billing-signature`.
94
+ 3. Дедуп по `event.id`.
95
+ 4. Уже потом — бизнес-логика по `event.type`.
320
96
 
321
97
  ```ts
322
- const customer = await billing.customers.update(
323
- 'cus_123',
324
- {
325
- name: 'MegaRetro Workspace (renamed)',
326
- email: 'billing@example.com',
327
- phone: null,
328
- },
329
- { idempotencyKey: 'customer-update:ws_123:v2' }
330
- );
331
- ```
98
+ import { BillingWebhookVerificationError } from '@openaisdk/billing-sdk-node';
332
99
 
333
- ### `billing.subscriptions.retrieve(subscriptionId, options?)`
100
+ async function handleBillingWebhook(request: {
101
+ rawBody: Buffer;
102
+ headers: Record<string, string | string[] | undefined>;
103
+ }) {
104
+ const rawBody = request.rawBody.toString('utf8');
105
+ const signature = request.headers['x-billing-signature'];
334
106
 
335
- `GET /v1/subscriptions/{subscriptionId}`
107
+ let event;
108
+ try {
109
+ event = billing.webhooks.constructEvent(
110
+ rawBody,
111
+ Array.isArray(signature) ? signature[0] : signature,
112
+ process.env.MEGABILLING_WEBHOOK_SECRET!
113
+ );
114
+ } catch (error) {
115
+ if (error instanceof BillingWebhookVerificationError) return;
116
+ throw error;
117
+ }
336
118
 
337
- Возвращает подписку по публичному `sub_...` идентификатору: `livemode`, `status`, `product`, `price`, границы периода, trial/grace поля и timestamps отмены/завершения.
119
+ if (await alreadyProcessed(event.id)) return;
338
120
 
339
- ```ts
340
- const subscription = await billing.subscriptions.retrieve('sub_123');
341
- ```
121
+ switch (event.type) {
122
+ default:
123
+ console.log(event.id, event.type, event.data);
124
+ }
342
125
 
343
- `status`: `'pending' | 'trialing' | 'active' | 'past_due' | 'paused' | 'canceled'`.
126
+ await markProcessed(event.id);
127
+ }
128
+ ```
344
129
 
345
- Подписка описывает биллинговый lifecycle, а не право доступа. Для gate фич в продукте по-прежнему используйте `billing.access.retrieve(...)`.
130
+ Оболочка события: `id` (дедуп), `type`, `data`, плюс `object`, `livemode`, `apiVersion`, `createdAt`, `version`. Полный список `type` — в типах `BILLING_WEBHOOK_EVENT_TYPES` / `BillingWebhookEventMap`.
346
131
 
347
- ### `billing.subscriptions.list(params, options?)`
132
+ Локально: секрет из кабинета + `constructEvent` на сыром теле.
348
133
 
349
- `GET /v1/subscriptions?customer=...`
134
+ ### 6. Спросить доступ перед фичей
350
135
 
351
- Возвращает подписки одного customer. Параметр `customer` обязателен: SDK не открывает project-wide перебор подписок.
136
+ Перед платной функцией спрашивайте MegaBilling через `access.retrieve` / `access.check`. Не решайте по статусу оплаты сами.
352
137
 
353
138
  ```ts
354
- const subscriptions = await billing.subscriptions.list({
355
- customer: 'cus_123',
356
- status: 'active',
357
- limit: 10,
358
- });
139
+ const access = await billing.access.retrieve(customer.id);
140
+ const check = await billing.access.check(customer.id, 'ai_assistant');
141
+
142
+ console.log(access.status, access.features, check.allowed, check.remaining);
359
143
  ```
360
144
 
361
- ### `billing.subscriptions.cancel(subscriptionId, payload?, options?)`
145
+ Кэш снимка доступа у себя — нормально (обновляйте по webhook). Источник правды — MegaBilling. При сомнении — свежий `access.retrieve`. Не открывайте доступ только по `successUrl`.
362
146
 
363
- `POST /v1/subscriptions/{subscriptionId}/cancel`
147
+ ### 7. Отмена и смена тарифа
364
148
 
365
- Отменяет подписку. `atPeriodEnd: true` планирует отмену на конец оплаченного периода, `false` (или отсутствие поля) отменяет немедленно.
149
+ **Отмена** (`atPeriodEnd: true` до конца периода; `false` сразу):
366
150
 
367
151
  ```ts
368
- const subscription = await billing.subscriptions.cancel(
369
- 'sub_123',
152
+ const subs = await billing.subscriptions.list({ customer: customer.id });
153
+ const subscriptionId = subs.data[0]!.id;
154
+
155
+ await billing.subscriptions.cancel(
156
+ subscriptionId,
370
157
  { atPeriodEnd: true },
371
- { idempotencyKey: 'subscription-cancel:sub_123:v1' }
158
+ { idempotencyKey: `subscription-cancel:${subscriptionId}:v1` }
372
159
  );
373
-
374
- console.log(subscription.status, subscription.cancelAtPeriodEnd);
375
160
  ```
376
161
 
377
- Отменаmutating операция, поэтому она использует `POST` с action-суффиксом, а не `DELETE`: отмена не удаляет ресурс и должна возвращать обновлённое состояние подписки.
378
-
379
- ### `billing.checkout.sessions.create(params, options?)`
380
-
381
- `POST /v1/checkout/sessions`
382
-
383
- Создаёт checkout session и возвращает `confirmationUrl` вместе со связанными публичными идентификаторами (`subscription`, `invoice`, `payment`). Redirect сам по себе не подтверждает оплату — источник истины о доступе: `billing.access.retrieve(...)`.
162
+ **Смена тарифа** сначала preview, потом change:
384
163
 
385
164
  ```ts
386
- const checkout = await billing.checkout.sessions.create(
165
+ const preview = await billing.subscriptions.previewChange(subscriptionId, {
166
+ price: 'price_pro_monthly',
167
+ effectivePolicy: 'IMMEDIATE', // или 'PERIOD_END'
168
+ prorationBehavior: 'PRORATE', // или 'NONE'
169
+ });
170
+
171
+ const change = await billing.subscriptions.change(
172
+ subscriptionId,
387
173
  {
388
- customer: 'cus_123',
389
- price: 'price_pro_monthly',
174
+ preview: preview.id,
175
+ expectedTotalMinor: preview.dueNowMinor,
390
176
  successUrl: 'https://app.example.com/billing/success',
391
177
  cancelUrl: 'https://app.example.com/billing',
392
- // promoCode: 'PROMO', // опционально
393
178
  },
394
- { idempotencyKey: 'checkout:ws_123:pro-monthly:v1' }
179
+ { idempotencyKey: `change:${subscriptionId}:v1` }
395
180
  );
396
- ```
397
-
398
- ### `billing.access.retrieve(customerId, options?)`
399
-
400
- `GET /v1/customers/{customerId}/access`
401
181
 
402
- Возвращает materialized access state: `livemode`, `status`, `features`, `limits`, `currentPeriodEnd`, `graceUntil`.
403
-
404
- ```ts
405
- const access = await billing.access.retrieve('cus_123');
182
+ // Если нужна доплата отправьте пользователя на change.confirmationUrl
183
+ console.log(change.confirmationUrl);
406
184
  ```
407
185
 
408
- Все успешные ответы дополняются полем `requestId` из заголовка `MegaBilling-Request-Id`, если сервер его вернул. Этот header для SDK является единственным authoritative источником correlation ID; возможный `requestId` в JSON body рассматривается только как server echo и не повышается до authoritative client-side значения.
186
+ ### По желанию: usage и счета
409
187
 
410
- ### `billing.meterEvents.create(params, options?)`
411
-
412
- `POST /v1/billing/meter-events`
413
-
414
- Принимает usage event по публичному `eventName` и публичному `customer`, не раскрывая внутренние `meterId`, `customerAccountId` или `subscriptionId`.
188
+ Usage: `meterEvents.create` (повтор защищает `identifier`) и `meterEvents.summary`. Счета: только чтение через `invoices.list` / `invoices.retrieve`.
415
189
 
416
190
  ```ts
417
- const event = await billing.meterEvents.create({
191
+ await billing.meterEvents.create({
418
192
  eventName: 'ai_tokens',
419
- customer: 'cus_123',
193
+ customer: customer.id,
420
194
  value: 1250,
421
195
  identifier: 'generation_gen_987',
422
- timestamp: '2026-08-10T00:00:00.000Z',
423
- dimensions: { model: 'gpt-5' },
424
- });
425
- ```
426
-
427
- Ответ: `billing.meter_event` с полями `id`, `livemode`, `status`, `eventName`, `customer`, `value`, `identifier`, `timestamp`, `dimensions` и `requestId`.
428
-
429
- Для безопасного replay повторяйте тот же нормализованный payload с тем же `identifier`. Если исходный запрос не передавал `timestamp`, сервер подставляет текущее время при каждом вызове (включая retry), но само значение `timestamp` не входит в payload fingerprint, когда клиент его не задавал явно — поэтому такой повтор не считается другим payload и не завершается `idempotency_key_conflict`. Если `timestamp` был передан явно в исходном запросе, он становится частью fingerprint, и retry обязан повторить то же самое значение. Public usage contract опирается именно на `identifier`; даже если transport-level `Idempotency-Key` передан вручную, он не заменяет это поле.
430
-
431
- Событие не привязывается к конкретной подписке клиентом: биллинг сам находит нужную подписку по `project + customer + meter` в момент расчёта счёта.
432
-
433
- ### `billing.meterEvents.summary(meterId, params, options?)`
434
-
435
- `GET /v1/billing/meters/{meterId}/event-summaries`
436
-
437
- Возвращает usage summary по публичному meter code и публичному customer в рамках project, определённого integration key.
438
-
439
- ```ts
440
- const summary = await billing.meterEvents.summary('ai_tokens', {
441
- customer: 'cus_123',
442
- startTime: '2026-08-10T00:00:00.000Z',
443
- endTime: '2026-08-10T01:00:00.000Z',
444
- valueGroupingWindow: 'hour',
445
196
  });
446
197
 
447
- for (const item of summary.data) {
448
- console.log(item.meter, item.aggregatedValue, item.startTime, item.endTime);
449
- }
198
+ const invoices = await billing.invoices.list({ customer: customer.id, limit: 20 });
450
199
  ```
451
200
 
452
- `meterId` здесь означает публичный meter code. Ответ — list envelope: `{ object: 'list', data: MeterEventSummary[], hasMore: false }` плюс `requestId`.
201
+ ---
453
202
 
454
203
  ## Ошибки
455
204
 
456
- SDK бросает `BillingError` для ошибок API и транспортных сбоев (включая timeout и network errors).
457
-
458
205
  ```ts
459
206
  try {
460
- await billing.checkout.sessions.create(
461
- {
462
- customer: 'cus_123',
463
- price: 'price_pro_monthly',
464
- successUrl: 'https://app.example.com/billing/success',
465
- cancelUrl: 'https://app.example.com/billing',
466
- },
467
- { idempotencyKey: 'checkout:ws_123:pro-monthly:v1' }
468
- );
207
+ await billing.checkout.sessions.create(/* ... */);
469
208
  } catch (error) {
470
209
  if (error instanceof BillingError) {
471
- console.error(error.code);
472
- console.error(error.message);
473
- console.error(error.status);
474
- console.error(error.requestId);
475
- console.error(error.param);
210
+ console.error(error.code, error.status, error.requestId, error.param);
476
211
  }
477
212
  }
478
213
  ```
479
214
 
480
- Для HTTP error-path `BillingError.requestId` читается строго из ответного header `MegaBilling-Request-Id`. Если header отсутствует, `requestId === null` даже если body содержит поле `error.requestId`.
481
-
482
- Для локальной верификации webhook SDK бросает отдельный `BillingWebhookVerificationError` с кодами:
483
-
484
- | Код | Когда возникает |
485
- | --------------------------- | -------------------------------------------------------- |
486
- | `webhook_header_invalid` | header отсутствует или не содержит валидный `t=` / `v1=` |
487
- | `webhook_signature_invalid` | payload или secret не совпали с подписью |
488
- | `webhook_timestamp_expired` | timestamp вышел за tolerance window |
489
- | `webhook_payload_invalid` | payload не является валидным JSON event envelope |
490
-
491
- Поля `BillingError`:
492
-
493
- | Поле | Назначение |
494
- | ----------- | ------------------------------------------------------------------------------------------------- |
495
- | `code` | Стабильный код ошибки API или транспортный (`request_timeout`, `network_error`, `http_<status>`) |
496
- | `status` | HTTP status; `408` для timeout; `0` для network failure |
497
- | `requestId` | Корреляционный ID только из `MegaBilling-Request-Id`; при отсутствии header SDK возвращает `null` |
498
- | `param` | Параметр, связанный с ошибкой валидации (если API его вернул) |
499
-
500
- ### Transport / network errors
501
-
502
- Для `network_error` SDK намеренно не прокидывает сырой `fetch`/runtime message наружу. Вместо этого возвращается безопасное сообщение:
503
-
504
- ```txt
505
- Network request failed. Check connectivity or BILLING_API_URL and retry.
506
- ```
507
-
508
- Это поведение защищает consumer logs и UI от случайной утечки низкоуровневых деталей рантайма (`ECONNREFUSED`, DNS internals, stack fragments) и оставляет диагностику в предсказуемом typed error contract.
215
+ `requestId` передайте в поддержку MegaBilling. Неверная подпись webhook `BillingWebhookVerificationError`.
509
216
 
510
- ## Идемпотентность
217
+ ---
511
218
 
512
- Для mutating-операций передавайте стабильный `idempotencyKey` в `options`. SDK пробрасывает его в заголовок `Idempotency-Key` без изменений.
219
+ ## Чем пакет не занимается
513
220
 
514
- - Повтор с тем же ключом и тем же payload возвращает сохранённый результат.
515
- - Тот же ключ с другим payload приводит к конфликту на стороне API.
516
- - Для `meterEvents.create(...)` replay задаётся не header-ом, а обязательным body-полем `identifier`: тот же `identifier` с тем же нормализованным payload возвращает тот же event, а с другим payload приводит к `idempotency_key_conflict`. `timestamp` входит в payload fingerprint только если он был передан явно retry без `timestamp` не создаёт ложный конфликт.
517
- - Для read-методов (`customers.retrieve`, `customers.list`, `subscriptions.retrieve`, `subscriptions.list`, `invoices.retrieve`, `invoices.listCreditNotes`, `access.retrieve`) ключ не используется — это GET.
221
+ - Админку каталога и ключи настраиваете в кабинете.
222
+ - Аннулирование счёта и credit note в кабинете.
223
+ - Настройки платёжного провайдера и фискализации — не через этот пакет.
224
+ - Hosted Billing Portal и выплаты нескольким продавцам в этом публичном API нет.
518
225
 
519
- | Метод | Идемпотентность |
520
- | -------------------------- | --------------------------------- |
521
- | `customers.create` | ключ обязателен на стороне API |
522
- | `customers.update` | ключ опционален, но рекомендуется |
523
- | `subscriptions.cancel` | ключ опционален, но рекомендуется |
524
- | `checkout.sessions.create` | ключ обязателен на стороне API |
525
- | `meterEvents.create` | replay через `identifier` в body |
526
-
527
- Рекомендуемый паттерн ключей: привязка к бизнес-сущности consumer-а, например `customer:{externalId}`, `checkout:{externalId}:{price}:v1`, `customer-update:{externalId}:v2`, `subscription-cancel:{subscriptionId}:v1`. Для usage `identifier` тоже стоит привязывать к бизнес-событию consumer-а, например `generation:{generationId}` или `workspace:{workspaceId}:usage:{sequence}`.
528
-
529
- Подробнее: [`docs/api/idempotency-rules.md`](../../docs/api/idempotency-rules.md).
226
+ ---
530
227
 
531
228
  ## Переменные окружения
532
229
 
533
- | Переменная | Обязательность | Назначение |
534
- | --------------------------------------------------- | ------------------------- | ------------------------------------------------------------------------------------------------ |
535
- | `BILLING_API_KEY` | да (для реальных вызовов) | Project-scoped integration key (`bsk_test_...` или `bsk_live_...`) → `Authorization: Bearer ...` |
536
- | `BILLING_API_URL` | нет | Base URL API; если не задан и не передан `baseUrl`, используется значение по умолчанию пакета |
537
- | `BILLING_PRICE_ID` | нет | Только для `pnpm run quickstart` (пример) |
538
- | `APP_URL` | нет | Только для quickstart: базовый URL success/cancel redirect |
539
- | `WORKSPACE_ID`, `WORKSPACE_EMAIL`, `WORKSPACE_NAME` | нет | Только для quickstart: демо-данные customer |
540
-
541
- В публичном клиенте Wave 1 **не** используются `BILLING_PROJECT_ID` и `BILLING_HTTP_X_TENANT_ID`: scope задаётся integration key.
542
-
543
- ## Отличие от `@openaisdk/billing-sdk`
544
-
545
- | | `@openaisdk/billing-sdk-node` | `@openaisdk/billing-sdk` |
546
- | ------------------ | ------------------------------------------------------------------------------ | ------------------------------------------------------------------- |
547
- | Роль | Канонический handwritten SDK для consumer Public API | Generated OpenAPI/axios compatibility-слой |
548
- | Поверхность | `customers` / `subscriptions` / `checkout.sessions` / `access` / `meterEvents` | Шире: generated client поверх полного Billing API |
549
- | Auth / headers | Только Bearer integration key | Может опираться на legacy env (`BILLING_PROJECT_ID`, `x-tenant-id`) |
550
- | Источник контракта | Public API Wave 1 (`/api/public-json`) | Не источник истины для Wave 1 public contract |
551
-
552
- Для новой consumer-интеграции предпочитайте `@openaisdk/billing-sdk-node`. Пакет `@openaisdk/billing-sdk` допустим как временный compatibility-слой до Wave 2, но не должен диктовать public naming или onboarding.
553
-
554
- ## Документация Public API
555
-
556
- - Обзор публичной границы: [`docs/api/public-api-v1.md`](../../docs/api/public-api-v1.md)
557
- - Сопоставление со Stripe: [`docs/integration/stripe-mapping.md`](../../docs/integration/stripe-mapping.md)
558
- - Правила идемпотентности: [`docs/api/idempotency-rules.md`](../../docs/api/idempotency-rules.md)
559
- - Машиночитаемый контракт runtime: `GET /api/public-json` (Swagger UI: `GET /api/public`)
560
-
561
- ## DX vs Stripe Node SDK
562
-
563
- Сходства:
564
-
565
- - resource-oriented форма: `customers.create`, `customers.retrieve`, `subscriptions.list`, `checkout.sessions.create`
566
- - явная поддержка `Idempotency-Key` на mutating-операциях
567
- - usage передаётся product-level методом `meterEvents.create` по публичным `eventName` + `customer`
568
- - типизированные ошибки через `instanceof` и стабильные коды
569
- - list-ответ как envelope со списком в `data` и признаком продолжения
570
-
571
- Осознанные отличия:
572
-
573
- - Mega-Billing использует project-scoped integration key вместо account-wide Stripe secret
574
- - `externalType` и `externalId` — first-class поля маппинга customer, а не опциональный metadata
575
- - доступ к продукту читается через `billing.access.retrieve(...)`, а не собирается из subscriptions / invoices / payments
576
- - для meter events dedup/replay задаётся обязательным `identifier` в body, а не отдельным `Idempotency-Key` header
577
-
578
- ### Customer & Subscription Lifecycle: сравнение со Stripe
579
-
580
- | Сценарий | Stripe Node SDK | `@openaisdk/billing-sdk-node` | Почему отличается |
581
- | ------------------- | --------------------------------------------------------------------------------------------------------------- | ---------------------------------------------------------------------------- | --------------------------------------------------------------------------------------------------------------- |
582
- | Чтение customer | `stripe.customers.retrieve(id)` | `billing.customers.retrieve(id)` | Совпадает по форме; возвращается публичный `cus_...` без внутренних UUID |
583
- | Список customers | `stripe.customers.list({ limit, starting_after })` | `billing.customers.list({ limit, startingAfter, externalType, externalId })` | camelCase вместо snake_case; фильтр по `externalType`/`externalId` вместо поиска по свободному metadata |
584
- | Обновление customer | `stripe.customers.update(id, params)` | `billing.customers.update(id, payload, options)` | Обновляются только контактные поля; связка `externalType`/`externalId` иммутабельна |
585
- | Список подписок | `stripe.subscriptions.list({ customer })`, `customer` опционален | `billing.subscriptions.list({ customer })`, `customer` обязателен | Consumer работает в контексте своей сущности; project-wide перебор подписок не является consumer-сценарием |
586
- | Отмена подписки | `stripe.subscriptions.cancel(id)` для немедленной и `update(id, { cancel_at_period_end: true })` для отложенной | `billing.subscriptions.cancel(id, { atPeriodEnd })` | Один метод вместо двух разных операций: намерение «отменить» выражается одним вызовом, а момент отмены — флагом |
587
- | Форма списка | `{ object: 'list', data, has_more }` | `{ object: 'list', data, hasMore }` | Та же mental model, camelCase по конвенции остальной части API |
588
- | Право доступа | Собирается из subscription status / entitlements | `billing.access.retrieve(customerId)` | Subscription остаётся биллинговым lifecycle; gate фич даёт materialized access state |
589
-
590
- ## Scope
591
-
592
- Wave 1 golden path, Customer & Subscription Lifecycle, Wave 3 webhook verification helper и Wave 4 public meter events. Пакет не открывает invoices, payments, refunds, admin API и внутренние заголовки.
593
-
594
- ## Troubleshooting
595
-
596
- ### `requestId` в ошибке равен `null`
597
-
598
- Это ожидаемо, если сервер не вернул `MegaBilling-Request-Id` в ответном header. SDK не доверяет `error.requestId` из body как authoritative источнику correlation ID.
599
-
600
- ### `BillingError` с `code === 'network_error'`
601
-
602
- Проверьте:
603
-
604
- - доступность `BILLING_API_URL`;
605
- - локальную сеть / VPN / proxy;
606
- - что backend действительно слушает ожидаемый host и port.
607
-
608
- Низкоуровневая строка `fetch failed ...` специально не попадает в `BillingError.message`; для глубокой диагностики смотрите server logs и локальный runtime output.
609
-
610
- ### Scoped локальная пересборка пакета
611
-
612
- Для локальной проверки используйте узкие команды из корня monorepo:
613
-
614
- ```bash
615
- pnpm --filter @openaisdk/billing-sdk-node build
616
- pnpm --filter @openaisdk/billing-sdk-node test
617
- ```
618
-
619
- Если `node_modules/.pnpm` локально оказался частично повреждён и сборка падает на `ENOENT`, сначала повторите именно scoped-команды выше. Только если проблема воспроизводится стабильно, переходите к минимальному восстановлению lockfile-based install, а не к полному destructive reinstall всего workspace.
230
+ | Переменная | Нужна? | Зачем |
231
+ | ---------------------------- | ----------- | --------------------------------------- |
232
+ | `BILLING_API_KEY` | Да | Ключ API из кабинета |
233
+ | `BILLING_API_URL` | Нет | Адрес API, если он нестандартный |
234
+ | `MEGABILLING_WEBHOOK_SECRET` | Для событий | Секрет для проверки подписи из кабинета |