{"_id":"@aicirkit/billing-sdk","_rev":"2-7b83de1e2a7a8817305a9ea828be669a","name":"@aicirkit/billing-sdk","dist-tags":{"latest":"0.3.0"},"versions":{"0.2.0":{"name":"@aicirkit/billing-sdk","version":"0.2.0","keywords":["aicirkit","billing","subscriptions","entitlements","usage-based-billing","metering","checkout","payments","sdk","typescript"],"author":{"name":"Aicirkit"},"license":"Apache-2.0","_id":"@aicirkit/billing-sdk@0.2.0","maintainers":[{"name":"aicirkit","email":"info@aicirkit.com"}],"homepage":"https://payment.aicirkit.com","bugs":{"email":"support@aicirkit.com"},"dist":{"shasum":"67a6770113c3eaf500106fa8e63e52cafd9b5e5e","tarball":"https://registry.npmjs.org/@aicirkit/billing-sdk/-/billing-sdk-0.2.0.tgz","fileCount":29,"integrity":"sha512-Nbq/oTmo5X7PCkPKh/0xVGOdu9rnSRa1GFY918DMWBp1YPkxwJCm2gjE+8ZnTyyw9fvvLdYHsfNWPqPnCk9Rcg==","signatures":[{"sig":"MEYCIQDAY+Adih3Re651Wfl4ixMoKjkJyGm0IF9WP+Eht/0woQIhAOAJdO/FvxQwywnIb/u2s6tnxXW9xrq8VDk07LwxCPYz","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"}],"unpackedSize":93823},"main":"./dist/index.js","_from":"file:aicirkit-billing-sdk-0.2.0.tgz","types":"./dist/index.d.ts","engines":{"node":"^22.22.3 || ^24.15.0 || >=26.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"private":false,"scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","typecheck":"tsc -p tsconfig.json --noEmit"},"_npmUser":{"name":"aicirkit","email":"info@aicirkit.com"},"_resolved":"/private/var/folders/r1/dgs0p9393yl3nnw2nx0nj2xw0000gn/T/a4bf2777815ec55e1112a084d28ab3d3/aicirkit-billing-sdk-0.2.0.tgz","_integrity":"sha512-Nbq/oTmo5X7PCkPKh/0xVGOdu9rnSRa1GFY918DMWBp1YPkxwJCm2gjE+8ZnTyyw9fvvLdYHsfNWPqPnCk9Rcg==","_npmVersion":"11.8.0","description":"Server-side client for the Aicirkit Billing API: customers, entitlements, usage and hosted checkout.","directories":{},"sideEffects":false,"_nodeVersion":"22.14.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/","provenance":false},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.5","vitest":"^4.0.8","@eslint/js":"^9.39.5","typescript":"~5.9.3","@types/node":"^24.10.1","typescript-eslint":"^8.66.0"},"_npmOperationalInternal":{"tmp":"tmp/billing-sdk_0.2.0_1788343582613_0.0929480733779866","host":"s3://npm-registry-packages-npm-production"}},"0.3.0":{"_id":"@aicirkit/billing-sdk@0.3.0","bugs":{"email":"support@aicirkit.com"},"dist":{"shasum":"7575c54a37740d3b64d70a793d1465bc27487d8c","tarball":"https://registry.npmjs.org/@aicirkit/billing-sdk/-/billing-sdk-0.3.0.tgz","fileCount":29,"integrity":"sha512-B9qH4DEBgS4MwFWNJm4+HmGyE/3tCyCJqG9l3JluOmjns1OWMy8BvHCuT50IJddX9PG+/5qWwiQtkz8c4lwZog==","signatures":[{"sig":"MEYCIQCTfCIGqegcuh02t/Hyth2wdwNaNdm1PxHIDBEqfFNpEwIhALwcrB84O8uvPpxg830YpI2wgLHRIb9Vqqr08g+cqMmT","keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U"},{"keyid":"SHA256:DhQ8wR5APBvFHLF/+Tc+AYvPOdTpcIDqOhxsBHRwC7U","sig":"MEUCIHNewe6iDV/mqEpruZNTDYYW6UVqJSXLQHEsZypyCHoOAiEApLBeMlsuLxpcXk+cRkij0DQijuaU5cHojcDWYVY3GZA="}],"unpackedSize":109761},"main":"./dist/index.js","name":"@aicirkit/billing-sdk","_from":"file:aicirkit-billing-sdk-0.3.0.tgz","types":"./dist/index.d.ts","author":{"name":"Aicirkit"},"engines":{"node":"^22.22.3 || ^24.15.0 || >=26.0.0"},"exports":{".":{"types":"./dist/index.d.ts","default":"./dist/index.js"},"./package.json":"./package.json"},"license":"Apache-2.0","private":false,"scripts":{"lint":"eslint .","test":"vitest run","build":"tsc -p tsconfig.build.json","typecheck":"tsc -p tsconfig.json --noEmit"},"version":"0.3.0","_npmUser":{"name":"aicirkit","email":"info@aicirkit.com"},"homepage":"https://payment.aicirkit.com","keywords":["aicirkit","billing","subscriptions","entitlements","usage-based-billing","metering","checkout","payments","sdk","typescript"],"_resolved":"/tmp/88946390ef161d22c0b11cbe24a6323c/aicirkit-billing-sdk-0.3.0.tgz","_integrity":"sha512-B9qH4DEBgS4MwFWNJm4+HmGyE/3tCyCJqG9l3JluOmjns1OWMy8BvHCuT50IJddX9PG+/5qWwiQtkz8c4lwZog==","_npmVersion":"11.19.0","description":"Server-side client for the Aicirkit Billing API: customers, entitlements, usage, hosted checkout and plan changes.","directories":{},"maintainers":[{"name":"aicirkit","email":"info@aicirkit.com"}],"sideEffects":false,"_nodeVersion":"24.20.0","publishConfig":{"access":"public","registry":"https://registry.npmjs.org/","provenance":false},"_hasShrinkwrap":false,"devDependencies":{"eslint":"^9.39.5","vitest":"^4.0.8","@eslint/js":"^9.39.5","typescript":"~5.9.3","@types/node":"^24.10.1","typescript-eslint":"^8.66.0"},"_npmOperationalInternal":{"host":"s3://npm-registry-packages-npm-production","tmp":"tmp/billing-sdk_0.3.0_1788777134232_0.9488918954325205"}}},"time":{"created":"2026-09-02T10:06:22.405Z","modified":"2026-09-07T10:32:15.570Z","0.2.0":"2026-09-02T10:06:22.752Z","0.3.0":"2026-09-07T10:32:14.320Z"},"bugs":{"email":"support@aicirkit.com"},"author":{"name":"Aicirkit"},"license":"Apache-2.0","homepage":"https://payment.aicirkit.com","keywords":["aicirkit","billing","subscriptions","entitlements","usage-based-billing","metering","checkout","payments","sdk","typescript"],"description":"Server-side client for the Aicirkit Billing API: customers, entitlements, usage, hosted checkout and plan changes.","maintainers":[{"name":"aicirkit","email":"info@aicirkit.com"}],"readme":"# @aicirkit/billing-sdk\n\nThe server-side client for the [Aicirkit Billing API](https://payment.aicirkit.com) —\ncustomers, entitlements, usage, hosted checkout and plan changes.\n\n> ### This belongs on a server\n>\n> The client secret is a bearer of authority for a whole organization and\n> environment. Putting one in a browser bundle publishes it — to every visitor,\n> permanently, whatever the bundler does.\n>\n> **Never put an API client secret in:**\n>\n> - browser JavaScript\n> - an Angular, React, Vue or Svelte frontend bundle\n> - a mobile application\n> - a public environment variable (`NEXT_PUBLIC_…`, `VITE_…`, `REACT_APP_…`)\n> - anything committed to source control\n>\n> This package reads no browser storage and ships no browser build, on purpose.\n\n```text\n   Your frontend                Your backend               Aicirkit\n  ┌──────────────┐            ┌──────────────┐          ┌──────────────────────┐\n  │ browser /    │  session   │ @aicirkit/   │  Basic   │ payment.aicirkit.com │\n  │ mobile app   │ ─────────► │ billing-sdk  │ ───────► │                      │\n  │              │            │              │  auth    │                      │\n  │ no secret    │            │ holds the    │          │                      │\n  │ ever         │ ◄───────── │ secret       │ ◄─────── │                      │\n  └──────────────┘  your API  └──────────────┘          └──────────────────────┘\n```\n\nThe customer is sent to a hosted checkout page to pay, so card data never\nreaches your servers either.\n\n## Install\n\n```bash\nnpm install @aicirkit/billing-sdk\n```\n\n```bash\npnpm add @aicirkit/billing-sdk\n# or\nyarn add @aicirkit/billing-sdk\n```\n\nNode 22.22.3+ or 24.15.0+. **No runtime dependencies.**\n\n### Module format\n\nThe package ships **CommonJS** with TypeScript declarations beside it.\n\n```js\nconst { AicirkitBilling } = require('@aicirkit/billing-sdk');\n```\n\n```ts\nimport { AicirkitBilling } from '@aicirkit/billing-sdk';\n```\n\nThe `import` form works from ESM and from TypeScript through Node's CommonJS\ninterop. There is no separate ESM build, and no browser build.\n\n### TypeScript\n\nDeclarations ship beside the entry point — nothing to install from\nDefinitelyTyped. TypeScript consumers need **`@types/node`** in the project:\nthese declarations name `AbortSignal` and `fetch`, which are Node's globals, and\nwithout it the compiler reports them as missing names.\n\n```bash\nnpm install --save-dev @types/node\n```\n\nIt is a types-only requirement. The package still has no runtime dependencies,\nand a plain JavaScript consumer needs nothing at all.\n\n## Configure\n\n```ts\nimport { AicirkitBilling } from '@aicirkit/billing-sdk';\n\nconst billing = new AicirkitBilling({\n  baseUrl: 'https://payment.aicirkit.com',\n  clientId: process.env.BILLING_CLIENT_ID!,\n  clientSecret: process.env.BILLING_CLIENT_SECRET!,\n});\n```\n\n| Option         | Default | What it is                                       |\n| -------------- | ------- | ------------------------------------------------ |\n| `baseUrl`      | —       | Where the API lives                              |\n| `clientId`     | —       | The public half — `cli_test_…` / `cli_live_…`    |\n| `clientSecret` | —       | The secret half. Server-side only                |\n| `timeoutMs`    | `15000` | Per-request budget                               |\n| `maxRetries`   | `2`     | Extra attempts for calls that are safe to repeat |\n\n## Test and live\n\n**There is no `environment` option, and that is not an omission.** An API client\nis issued for `test` or `live` and that record is immutable, so the world your\ncalls land in comes from the credential. An option here would be a second answer\nto the same question, and the first time the two disagreed somebody would be\nreading live money through a test integration.\n\nUse two credentials, one per environment, and keep them in separate secrets:\n\n```ts\nconst identity = await billing.apiClients.me();\n\nidentity.environment; // 'test' | 'production'  ← a `live` client reads as 'production'\nidentity.effectiveScopes; // what this credential may actually do, now\nidentity.applicationIds; // which applications it may act for\n```\n\nRead this at boot rather than discovering a missing scope on the first refused\nwrite. Test records and live records never see each other: a test credential\ncannot read a live customer, and the reverse holds too.\n\n`billing.health.check()` answers a different question — which **installation**\nresponded (`production`, `staging`, `development`). That is deployment metadata,\nnot the environment your records live in.\n\n## Find or create your customer\n\nStore the reference you assigned — usually your own tenant id — and resolve it\nexactly. This is not a search: it will not match a similar reference or an email.\n\n```ts\nimport { NotFoundError, ConflictError, type Customer } from '@aicirkit/billing-sdk';\n\nasync function billingCustomerFor(tenantId: string): Promise<Customer> {\n  try {\n    return await billing.customers.getByReference(tenantId);\n  } catch (error) {\n    if (!(error instanceof NotFoundError)) throw error;\n\n    return billing.customers.create({\n      reference: tenantId,\n      companyName: 'Acme',\n      legalName: 'Acme A.Ş.',\n      billingEmail: 'billing@acme.example',\n      country: 'TR',\n      currency: 'EUR',\n      language: 'tr',\n      contact: { firstName: 'Deniz', lastName: 'Kaya', email: 'deniz@acme.example' },\n      billingAddress: {\n        line1: 'Bağdat Cd. 1',\n        city: 'İstanbul',\n        postalCode: '34728',\n        country: 'TR',\n      },\n      applicationIds: ['app_your_application'],\n    });\n  }\n}\n```\n\nIf two requests race, the second `create` answers `ConflictError` with\n`details.reason === 'reference_taken'` — catch it and read the customer.\n\n## Can they use this feature\n\n```ts\nconst seats = await billing.entitlements.check(customer.id, 'SEATS');\n\nif (!seats || !seats.resolves) {\n  // `null` means nothing grants it. `resolves: false` means something does and\n  // access is stopped. They are different answers.\n  return deny(seats?.status ?? 'not_included');\n}\n\nif (seats.value?.kind === 'limit' && !seats.value.limit.unlimited) {\n  const max = seats.value.limit.max;\n}\n```\n\nUnlimited says `unlimited: true`. There is no `-1` to compare against.\n\n## Report what they used\n\n```ts\nawait billing.usage.record({\n  idempotencyKey: `${jobId}:images`,\n  meterCode: 'PROCESSED_IMAGES',\n  subscriptionId: subscription.id,\n  quantity: 12,\n  occurredAt: startedAt, // when it happened, not when you are reporting it\n});\n```\n\n## Send them somewhere to pay\n\nHosted checkout. You give two identifiers and a key; the amount, the currency,\nthe environment, the application and both redirect URLs are the platform's.\n\n```ts\nconst checkout = await billing.checkouts.open({\n  customerReference: tenant.id,\n  priceId: 'price_your_plan',\n  idempotencyKey: `signup:${tenant.id}:pro-monthly`,\n});\n\ncheckout.checkoutAttemptId; // your handle on this attempt\ncheckout.status; // 'pending' | 'opened' | 'completed' | 'failed' | 'expired'\ncheckout.redirectUrl; // send the customer here; null once no longer open\n```\n\nCard data never touches your servers — the customer pays on the hosted page.\nThe result reaches you as a webhook from the platform, not as a return value\nhere.\n\n## Change a plan\n\nFor a customer who already has an agreement, moving them to a different price\ndoes not go through checkout — preview it, then change it.\n\n```ts\nconst preview = await billing.subscriptions.previewPlanChange('sub_example', {\n  priceId: 'price_target',\n});\n\nif (preview.blockedReason) {\n  // e.g. `same_price`, `unsupported_quantity_change` — nothing to retry.\n} else {\n  preview.changeType; // 'upgrade' | 'downgrade' | 'same_plan' — the platform's\n  // own classification, by price. Not your product's plan names, and nothing\n  // you should re-derive from one either.\n  preview.financialPreview?.amountDueNow; // what settles today, if anything\n\n  const result = await billing.subscriptions.changePlan('sub_example', {\n    priceId: 'price_target',\n    idempotencyKey: `plan-change:sub_example:price_target:${Date.now()}`,\n  });\n\n  result.state; // 'applied' | 'pending_payment' | 'requires_action' |\n  // 'payment_failed' | 'scheduled' — only 'applied' means the new price is\n  // already in effect. 'scheduled' is a downgrade booked for the current\n  // period's end: poll `subscriptions.get` and read `pendingChange`, not\n  // `result.subscription.priceId`, to see it actually land.\n}\n```\n\n`previewPlanChange` needs `subscriptions:read`; `changePlan` needs\n`subscriptions:write`. `Idempotency-Key` is required only when the change is\nprovider-backed — an immediate upgrade genuinely charges a proration, a\nperiod-end downgrade books a real provider schedule — and optional for a\nconsole-only agreement, which charges nothing either way.\n\nChanged your mind about a booked downgrade before it takes effect?\n\n```ts\nawait billing.subscriptions.cancelPlanChange('sub_example');\n```\n\nWithdraws the one open plan change and keeps the current plan. Needs\n`subscriptions:write`, same as `changePlan`.\n\n## Idempotency\n\nTwo calls carrying the same key are **one operation attempted twice**, not two\noperations. The key makes a retry safe after a timeout, a crash or a lost\nresponse, when you cannot know whether the first attempt landed.\n\n| Call                       | Key                                | A repeat answers                                            |\n| -------------------------- | ---------------------------------- | ----------------------------------------------------------- |\n| `usage.record`             | `idempotencyKey` on the input      | `outcome: 'duplicate'` with the first event's id            |\n| `checkouts.open`           | `idempotencyKey` on the input      | the same checkout, not a second one                         |\n| `subscriptions.changePlan` | `idempotencyKey` on the input      | the same booked attempt, when the change is provider-backed |\n| `customers.create`         | the `reference` is the natural key | `ConflictError`, `reason: 'reference_taken'`                |\n\n```ts\n// Same purchase retried → the same checkout, so the customer cannot pay twice.\nconst key = `signup:${tenant.id}:pro-monthly`;\n\nconst first = await billing.checkouts.open({\n  customerReference: tenant.id,\n  priceId,\n  idempotencyKey: key,\n});\nconst retry = await billing.checkouts.open({\n  customerReference: tenant.id,\n  priceId,\n  idempotencyKey: key,\n});\n\nfirst.checkoutAttemptId === retry.checkoutAttemptId; // true\n```\n\nDerive the key from what makes the operation unique on your side — an order id,\na job id, a billing period — not from a random value, which would make every\nretry a new operation. A key of 8 characters or more.\n\n## Errors\n\nEvery refusal is a typed error carrying `code`, `kind`, `status`, `requestId`\nand `details`. The server's `message` is written for an operator and is **not**\npart of this contract — branch on `code` and `details`.\n\n```ts\nimport {\n  BillingError,\n  AuthenticationError,\n  ForbiddenError,\n  NotFoundError,\n  ConflictError,\n  ValidationError,\n  RateLimitedError,\n  UnavailableError,\n} from '@aicirkit/billing-sdk';\n\ntry {\n  await billing.usage.record(event);\n} catch (error) {\n  if (error instanceof RateLimitedError) {\n    await wait(error.retryAfterSeconds ?? 5);\n    return retry();\n  }\n  if (error instanceof ValidationError) {\n    return reject(error.details); // which field, and why\n  }\n  if (error instanceof BillingError) {\n    log.warn({ code: error.code, requestId: error.requestId });\n  }\n  throw error;\n}\n```\n\n| Error                 | When                                                  |\n| --------------------- | ----------------------------------------------------- |\n| `AuthenticationError` | The credential is wrong, revoked or expired           |\n| `ForbiddenError`      | Authenticated, but this credential lacks the scope    |\n| `NotFoundError`       | No such record **in this environment**                |\n| `ConflictError`       | It already exists, or the state forbids this          |\n| `ValidationError`     | The request was malformed; `details` says what        |\n| `RateLimitedError`    | Too many requests; `retryAfterSeconds` says how long  |\n| `UnavailableError`    | Network, timeout or the API is down                   |\n| `BillingError`        | The base class — catch this to catch all of the above |\n\n`requestId` is the value to quote in a support request. Retries for timeouts,\n`429` and `5xx` are already handled by the client.\n\n## What stays console-only\n\nOpening an agreement, pausing, resuming, cancelling it outright, extending a\ntrial, applying a discount: every one of them prorates, writes invoices or\nqueues provider calls, and none of them is a plan change — the one write this\ncredential may make on a subscription (see [Change a plan](#change-a-plan)\nabove). The API refuses the rest to a machine credential. They are done from\nthe billing console.\n\n## Support\n\n- Service and documentation: <https://payment.aicirkit.com>\n- Support: <support@aicirkit.com> — quote the `requestId` from the error\n\n## Licence\n\nApache-2.0. See [LICENSE](./LICENSE) and [NOTICE](./NOTICE). Use of the Aicirkit\nBilling service itself is governed by the Aicirkit Terms of Service.\n","readmeFilename":"README.md"}