Skip to content

Latest commit

 

History

History
620 lines (497 loc) · 55.1 KB

File metadata and controls

620 lines (497 loc) · 55.1 KB

OpenMeter SDK

TypeScript client for the OpenMeter API — usage metering and billing for AI and DevTool companies. This package is generated from the OpenMeter TypeSpec definitions and ships fully-typed request and response models.

Important: This SDK is a work in progress.

This SDK targets the OpenMeter API v3, a rewrite of the OpenMeter API following AIP (API Improvement Proposal) standardization.

Table of Contents

Installation

npm install @openmeter/client

Or with your package manager of choice:

pnpm add @openmeter/client
yarn add @openmeter/client

Initialization

Create a client with a base URL and an API key. The API key is sent as a Bearer token on every request.

import { OpenMeter } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

Konnect regions are addressed with a server template and a region variable:

import { OpenMeter, ServerList } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: ServerList[0],
  serverVariables: { region: 'eu' },
  apiKey: process.env.OPENMETER_API_KEY,
})

The apiKey may also be a function returning a string or Promise<string>, so tokens can be refreshed per request.

Configuration

SDKOptions extends ky's Options, so every transport setting ky supports is a top-level client option: retry policy, per-attempt timeout (ky defaults to 10 seconds), lifecycle hooks, a custom fetch, and so on.

import { OpenMeter } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
  timeout: 30_000,
  hooks: {
    beforeRequest: [
      ({ request }) => {
        console.log(`-> ${request.method} ${request.url}`)
      },
    ],
  },
  fetch: async (input, init) => {
    const start = Date.now()
    const response = await fetch(input, init)
    console.log(`${response.status} in ${Date.now() - start}ms`)
    return response
  },
})

ky only retries the idempotent methods by default — get, put, head, delete, options, trace — never post. That means a dropped client.events.ingest call is not retried on a network error or a 5xx response unless you opt in explicitly:

import { OpenMeter } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
  retry: { limit: 3, methods: ['get', 'put', 'head', 'delete', 'post'] },
})

This is safe specifically for event ingestion: the event id is its deduplication key server-side, so resending the same event on retry is a no-op rather than a duplicate.

Every method also takes a per-request RequestOptions as its second argument — a curated subset of ky's options (signal, headers, timeout, retry) applied to that call only:

import { OpenMeter } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

const controller = new AbortController()
setTimeout(() => controller.abort(), 5_000)

const meters = await client.meters.list(undefined, {
  signal: controller.signal,
  headers: { 'X-Request-Id': 'batch-42' },
})

Usage

Every operation is reachable through a fluent, namespaced client and returns a typed response (or throws an HTTPError on a non-2xx status).

import { OpenMeter } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

const meter = await client.meters.create({
  name: 'Tokens',
  key: 'tokens',
  aggregation: 'sum',
  eventType: 'request',
  valueProperty: '$.tokens',
})

const meters = await client.meters.list()

Each method takes the request object as its first argument and an optional per-request options object (RequestOptions) as its second.

Responses return date-time fields as native Date objects (every createdAt/updatedAt, meter query row windows, …), and requests accept either a Date or an RFC 3339 string — a meter query from/to, an ingested event's time, filter operands, all alike.

Pagination

Every list operation that returns pages also has an …All companion — client.meters.listAll(request?) alongside client.meters.list(request?) — that returns an AsyncIterable of items instead of one page. It fetches each following page lazily, only when the previous page is exhausted, so a break (or a return) partway through never fires a request for a page nothing consumes:

import { OpenMeter } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

for await (const meter of client.meters.listAll()) {
  console.log(meter.key)
}

The request object accepts the same filters, sort, and page size as the single-page method — only the page cursor/number itself advances between requests:

for await (const meter of client.meters.listAll({ filter: { key: 'api' } })) {
  if (meter.key === 'api-requests') {
    break // stops iterating; no further pages are fetched
  }
}

Cursor-paginated resources (events, credit transactions) work the same way:

for await (const event of client.events.listAll()) {
  console.log(event.event.id)
}

Auto-pagination takes the same optional RequestOptions as every other method, so one AbortSignal cancels the whole iteration, not just the page in flight:

const controller = new AbortController()
setTimeout(() => controller.abort(), 30_000)

for await (const meter of client.meters.listAll(undefined, {
  signal: controller.signal,
})) {
  console.log(meter.key)
}

Iteration stops after the server’s last page — an empty or short-of-size page for a page-number resource, or a response with no next cursor for a cursor resource. A misbehaving server that never signals the end of the list fails fast instead of looping forever: after 10,000 pages the iterable throws PaginationLimitExceededError.

Available Resources and Operations

Operations are grouped by resource and exposed as methods on the client. The full call path, HTTP route, and a short description are listed below.

Events

Method HTTP Description
client.events.list GET /openmeter/events List ingested events.
client.events.ingest POST /openmeter/events Ingests an event or batch of events following the CloudEvents specification.

Meters

Method HTTP Description
client.meters.create POST /openmeter/meters Create a meter.
client.meters.get GET /openmeter/meters/{meterId} Get a meter by ID.
client.meters.list GET /openmeter/meters List meters.
client.meters.update PUT /openmeter/meters/{meterId} Update a meter.
client.meters.delete DELETE /openmeter/meters/{meterId} Delete a meter.
client.meters.query POST /openmeter/meters/{meterId}/query Query a meter for usage. Set Accept: application/json (the default) to get a structured JSON response. Set Accept: text/csv to download the same data as a CSV file suitable for spreadsheets. The CSV columns, in order, are: from, to, [subject,] [customer_id, customer_key, customer_name,] <dimensions...>, value The subject column is emitted only when subject is in the query's group_by_dimensions. The three customer_* columns are emitted together only when customer_id is in the query's group_by_dimensions.
client.meters.queryCsv POST /openmeter/meters/{meterId}/query

Customers

Method HTTP Description
client.customers.create POST /openmeter/customers Create customer
client.customers.get GET /openmeter/customers/{customerId} Get customer
client.customers.list GET /openmeter/customers List customers
client.customers.upsert PUT /openmeter/customers/{customerId} Upsert customer
client.customers.delete DELETE /openmeter/customers/{customerId} Delete customer
client.customers.billing.get GET /openmeter/customers/{customerId}/billing Get customer billing data
client.customers.billing.update PUT /openmeter/customers/{customerId}/billing Update customer billing data
client.customers.billing.updateAppData PUT /openmeter/customers/{customerId}/billing/app-data Update customer billing app data
client.customers.billing.createStripeCheckoutSession POST /openmeter/customers/{customerId}/billing/stripe/checkout-sessions Create a Stripe Checkout Session for the customer. Creates a Checkout Session for collecting payment method information from customers. The session operates in "setup" mode, which collects payment details without charging the customer immediately. The collected payment method can be used for future subscription billing. For hosted checkout sessions, redirect customers to the returned URL. For embedded sessions, use the client_secret to initialize Stripe.js in your application.
client.customers.billing.createStripePortalSession POST /openmeter/customers/{customerId}/billing/stripe/portal-sessions Create Stripe Customer Portal Session. Useful to redirect the customer to the Stripe Customer Portal to manage their payment methods, change their billing address and access their invoice history. Only returns URL if the customer billing profile is linked to a stripe app and customer.
client.customers.credits.grants.create POST /openmeter/customers/{customerId}/credits/grants Create a new credit grant. A credit grant represents an allocation of prepaid credits to a customer.
client.customers.credits.grants.get GET /openmeter/customers/{customerId}/credits/grants/{creditGrantId} Get a credit grant.
client.customers.credits.grants.list GET /openmeter/customers/{customerId}/credits/grants List credit grants.
client.customers.credits.balance.get GET /openmeter/customers/{customerId}/credits/balance Get a credit balance.
client.customers.credits.adjustments.create POST /openmeter/customers/{customerId}/credits/adjustments A credit adjustment can be used to make manual adjustments to a customer's credit balance. Supported use-cases: - Usage correction
client.customers.credits.grants.void POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/void Void a credit grant, forfeiting the remaining unused balance. Voiding is a forward-looking, irreversible operation. Credits already consumed by usage remain unaffected — only the remaining balance is forfeited. The grant reads as voided status afterwards. Payment state is not adjusted when payment_adjustment is none, so invoice-backed or externally collected payments may still collect the original amount. Only active grants can be voided; voiding a pending, expired, or fully consumed grant returns a conflict. Retrying a successful void is an idempotent success.
client.customers.credits.grants.updateExternalSettlement POST /openmeter/customers/{customerId}/credits/grants/{creditGrantId}/settlement/external Update the payment settlement status of an externally funded credit grant. Use this endpoint to synchronize the payment state of an external payment with the system so that revenue recognition and credit availability work as expected.
client.customers.credits.transactions.list GET /openmeter/customers/{customerId}/credits/transactions List credit transactions for a customer. Returns an immutable, chronological record of credit movements: funded credits and consumed credits. Transactions are returned in reverse chronological order by default.
client.customers.charges.list GET /openmeter/customers/{customerId}/charges List customer charges. Returns the customer's charges that are represented as either flat fee or usage-based charges.
client.customers.charges.create POST /openmeter/customers/{customerId}/charges Create customer charge.

Entitlements

Method HTTP Description
client.entitlements.listCustomerAccess GET /openmeter/customers/{customerId}/entitlement-access List customer entitlement access

Subscriptions

Method HTTP Description
client.subscriptions.create POST /openmeter/subscriptions Create subscription
client.subscriptions.list GET /openmeter/subscriptions List subscriptions
client.subscriptions.get GET /openmeter/subscriptions/{subscriptionId} Get subscription
client.subscriptions.cancel POST /openmeter/subscriptions/{subscriptionId}/cancel Cancels the subscription. Will result in a scheduling conflict if there are other subscriptions scheduled to start after the cancelation time.
client.subscriptions.unscheduleCancelation POST /openmeter/subscriptions/{subscriptionId}/unschedule-cancelation Unschedules the subscription cancelation.
client.subscriptions.change POST /openmeter/subscriptions/{subscriptionId}/change Closes a running subscription and starts a new one according to the specification. Can be used for upgrades, downgrades, and plan changes.
client.subscriptions.listAddons GET /openmeter/subscriptions/{subscriptionId}/addons List the add-ons of a subscription.
client.subscriptions.getAddon GET /openmeter/subscriptions/{subscriptionId}/addons/{subscriptionAddonId} Get an add-on association for a subscription.

Apps

Method HTTP Description
client.apps.list GET /openmeter/apps List installed apps.
client.apps.get GET /openmeter/apps/{appId} Get an installed app.

Billing

Method HTTP Description
client.billing.listProfiles GET /openmeter/profiles List billing profiles.
client.billing.createProfile POST /openmeter/profiles Create a new billing profile. Billing profiles contain the settings for billing and controls invoice generation. An organization can have multiple billing profiles defined. A billing profile is linked to a specific app. This association is established during the billing profile's creation and remains immutable.
client.billing.getProfile GET /openmeter/profiles/{id} Get a billing profile.
client.billing.updateProfile PUT /openmeter/profiles/{id} Update a billing profile.
client.billing.deleteProfile DELETE /openmeter/profiles/{id} Delete a billing profile. Only such billing profiles can be deleted that are: - not the default profile - not pinned to any customer using customer overrides - only have finalized invoices

Tax

Method HTTP Description
client.tax.createCode POST /openmeter/tax-codes Create tax code
client.tax.getCode GET /openmeter/tax-codes/{taxCodeId} Get tax code
client.tax.listCodes GET /openmeter/tax-codes List tax codes
client.tax.upsertCode PUT /openmeter/tax-codes/{taxCodeId} Upsert tax code
client.tax.deleteCode DELETE /openmeter/tax-codes/{taxCodeId} Delete tax code

Features

Method HTTP Description
client.features.list GET /openmeter/features List all features.
client.features.create POST /openmeter/features Create a feature.
client.features.get GET /openmeter/features/{featureId} Get a feature by id.
client.features.update PATCH /openmeter/features/{featureId} Update a feature by id. Currently only the unit_cost field can be updated.
client.features.delete DELETE /openmeter/features/{featureId} Delete a feature by id.
client.features.queryCost POST /openmeter/features/{featureId}/cost/query Query the cost of a feature.

LLMCost

Method HTTP Description
client.llmCost.listPrices GET /openmeter/llm-cost/prices List global LLM cost prices. Returns prices with overrides applied if any.
client.llmCost.getPrice GET /openmeter/llm-cost/prices/{priceId} Get a specific LLM cost price by ID. Returns the price with overrides applied if any.
client.llmCost.listOverrides GET /openmeter/llm-cost/overrides List per-namespace price overrides.
client.llmCost.createOverride POST /openmeter/llm-cost/overrides Create a per-namespace price override.
client.llmCost.deleteOverride DELETE /openmeter/llm-cost/overrides/{priceId} Delete a per-namespace price override.

Plans

Method HTTP Description
client.plans.list GET /openmeter/plans List all plans.
client.plans.create POST /openmeter/plans Create a new plan.
client.plans.update PUT /openmeter/plans/{planId} Update a plan by id.
client.plans.get GET /openmeter/plans/{planId} Get a plan by id.
client.plans.delete DELETE /openmeter/plans/{planId} Delete a plan by id.
client.plans.archive POST /openmeter/plans/{planId}/archive Archive a plan version.
client.plans.publish POST /openmeter/plans/{planId}/publish Publish a plan version.

Addons

Method HTTP Description
client.addons.list GET /openmeter/addons List all add-ons.
client.addons.create POST /openmeter/addons Create a new add-on.
client.addons.update PUT /openmeter/addons/{addonId} Update an add-on by id.
client.addons.get GET /openmeter/addons/{addonId} Get add-on by id.
client.addons.delete DELETE /openmeter/addons/{addonId} Soft delete add-on by id.
client.addons.archive POST /openmeter/addons/{addonId}/archive Archive an add-on version.
client.addons.publish POST /openmeter/addons/{addonId}/publish Publish an add-on version.

PlanAddons

Method HTTP Description
client.planAddons.list GET /openmeter/plans/{planId}/addons List add-ons associated with a plan.
client.planAddons.create POST /openmeter/plans/{planId}/addons Add an add-on to a plan.
client.planAddons.get GET /openmeter/plans/{planId}/addons/{planAddonId} Get an add-on association for a plan.
client.planAddons.update PUT /openmeter/plans/{planId}/addons/{planAddonId} Update an add-on association for a plan.
client.planAddons.delete DELETE /openmeter/plans/{planId}/addons/{planAddonId} Remove an add-on from a plan.

Defaults

Method HTTP Description
client.defaults.getOrganizationTaxCodes GET /openmeter/defaults/tax-codes Get organization default tax codes
client.defaults.updateOrganizationTaxCodes PUT /openmeter/defaults/tax-codes Update organization default tax codes

Internal Operations

Operations marked internal in the API definition are exposed under client.internal.*, quarantined from the customer surface. They are not intended for customer use: they may require additional permissions, and they can change or be removed without notice or semver consideration.

Internal Subscriptions

Method HTTP Description
client.internal.subscriptions.createAddon POST /openmeter/subscriptions/{subscriptionId}/addons Add add-on to a subscription.

Internal Invoices

Method HTTP Description
client.internal.invoices.list GET /openmeter/billing/invoices List billing invoices. Returns a page of invoices. Gathering invoices are never included. Use filter to narrow by status, customer, dates, or service period start. Use sort to control ordering.
client.internal.invoices.get GET /openmeter/billing/invoices/{invoiceId} Get a billing invoice by ID. Returns the full invoice resource including line items, status details, totals, and workflow configuration snapshot.
client.internal.invoices.update PUT /openmeter/billing/invoices/{invoiceId} Update a billing invoice. Only the mutable fields of the invoice can be edited: description, labels, supplier, customer, workflow settings, and top-level lines. Top-level lines are matched by id; lines without an id are created, and existing lines omitted from lines are deleted. Detailed (child) lines are always computed and cannot be edited directly. Only invoices in draft status can be updated.
client.internal.invoices.delete DELETE /openmeter/billing/invoices/{invoiceId} Delete a billing invoice. Only standard invoices in draft status can be deleted. Deleting an invoice will also delete all associated line items and workflow configuration.
client.internal.invoices.advance POST /openmeter/billing/invoices/{invoiceId}/advance Advance a billing invoice. Advances the invoice to the next workflow state. The next state is determined by the invoice's current status and workflow configuration. Only invoices in draft or issued status can be advanced.
client.internal.invoices.approve POST /openmeter/billing/invoices/{invoiceId}/approve Approve a billing invoice. This call instantly sends the invoice to the customer using the configured billing profile app. This call is valid in two invoice statuses: - draft: the invoice will be sent to the customer, the invoice state becomes issued - manual_approval_needed: the invoice will be sent to the customer, the invoice state becomes issued
client.internal.invoices.retry POST /openmeter/billing/invoices/{invoiceId}/retry Retry sending a billing invoice. Retry advancing the invoice after a failed attempt. The action can be called when the invoice's statusDetails' actions field contain the "retry" action.
client.internal.invoices.snapshotQuantities POST /openmeter/billing/invoices/{invoiceId}/snapshot-quantities Snapshot quantities for usage-based line items. This call will snapshot the quantities for all usage based line items in the invoice. This call is only valid in draft.waiting_for_collection status, where the collection period can be skipped using this action.

Internal Currencies

Method HTTP Description
client.internal.currencies.list GET /openmeter/currencies List currencies supported by the billing system.
client.internal.currencies.createCustomCurrency POST /openmeter/currencies/custom Create a custom currency. This operation allows defining your own custom currency for billing purposes.
client.internal.currencies.listCostBases GET /openmeter/currencies/custom/{currencyId}/cost-bases List cost bases for a currency. For custom currencies, there can be multiple cost bases with different effective_from dates.
client.internal.currencies.createCostBasis POST /openmeter/currencies/custom/{currencyId}/cost-bases Create a cost basis for a currency.

Internal Governance

Method HTTP Description
client.internal.governance.queryAccess POST /openmeter/governance/query Query feature access for a list of customers. The endpoint resolves each provided identifier to a customer and returns the access status for the requested features, plus optional credit balance availability. Designed to be called on a fixed refresh interval and the query response is intended to be cached.

Runtime Validation (validate option)

validate is off by default. The SDK's normal request/response mapping only renames keys and converts dates — it never rejects a payload — so an additive server-side change (a new response field, a new enum value) never breaks a client running an older SDK version.

Set validate: true to additionally check the wire payload — the request body after mapping to snake_case, and the raw response before mapping back — against the generated …Wire schemas. Those schemas are strict: an unknown field or an unrecognized enum value fails validation instead of being silently accepted. That makes validate a tool for catching SDK/server contract drift in development or CI, not something to run in production, where forward compatibility with additive server changes matters more than strict rejection.

import { OpenMeter, ValidationError } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
  validate: true,
})

try {
  await client.meters.get({ meterId: 'meter_123' })
} catch (error) {
  if (error instanceof ValidationError) {
    console.error(error.issues)
  }
}

The standalone functions surface the same failure as a Result instead of throwing — check result.error instanceof ValidationError.

Zod Schemas (./zod export)

@openmeter/client/zod exports every model twice: once shaped like the SDK's public surface (camelCase keys, Date for date-time fields — the same shape meter.eventType/meter.createdAt have in TypeScript) and once as a strict …Wire schema shaped like the literal JSON the server sends and accepts (snake_case keys, RFC 3339 date-time strings, unknown fields rejected) — the same …Wire schemas the validate option checks internally.

Use the public schemas to validate a payload the SDK did not produce — a webhook body, a cached record, a test fixture — before trusting its shape:

import * as schemas from '@openmeter/client/zod'

const parsed = schemas.meter.safeParse({
  id: '01HZY3W6VXQK6H3NPC6DFA0PJT',
  name: 'Tokens',
  key: 'tokens',
  aggregation: 'sum',
  eventType: 'request',
  createdAt: new Date(),
  updatedAt: new Date(),
})

if (parsed.success) {
  console.log(parsed.data.eventType)
}

Error Handling

A non-2xx response rejects with an HTTPError (error.name === 'HTTPError') carrying the problem-details fields (status, type, title, url) from the response.

import { OpenMeter, HTTPError } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

try {
  await client.meters.get({ meterId: 'unknown' })
} catch (error) {
  if (error instanceof HTTPError) {
    console.error(error.status, error.title, error.type)
  }
}

error.retryAfter is the delta-seconds form of a numeric Retry-After header (the common case on 429 and 503 responses) and undefined otherwise. A 400 Bad Request additionally carries a typed error.invalidParameters array describing which fields failed validation; error.getField(key) is an untyped escape hatch for any other problem-details extension member:

import { OpenMeter, HTTPError } from '@openmeter/client'

const client = new OpenMeter({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

try {
  await client.meters.create({
    name: 'Tokens',
    key: 'tokens',
    aggregation: 'sum',
    eventType: 'request',
  })
} catch (error) {
  if (error instanceof HTTPError && error.status === 400) {
    for (const param of error.invalidParameters ?? []) {
      console.error(param)
    }
  }
}

The SDK's other typed errors are ValidationError (see Runtime Validation (validate option)), UnsafeIntegerError (an int64/uint64 value exceeds what JSON can represent without precision loss), DepthLimitExceededError (response data nested deeper than the mapper's safety limit), and PaginationLimitExceededError (an Pagination iterable exceeded its page-count safety limit) — each distinguished the same way, by instanceof or by .name.

Standalone Functions

Every method is also available as a standalone, tree-shakeable function that takes a Client and returns a Result instead of throwing.

import { Client, funcs } from '@openmeter/client'

const client = new Client({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

const result = await funcs.listMeters(client)
if (result.ok) {
  console.log(result.value)
} else {
  console.error(result.error)
}

ok, err, and unwrap — the helpers Result is built from — are exported too, so a func call can be unwrapped back into a throwing call where that is more convenient:

import { Client, funcs, unwrap } from '@openmeter/client'

const client = new Client({
  baseUrl: 'https://openmeter.cloud/api/v3',
  apiKey: process.env.OPENMETER_API_KEY,
})

const meters = unwrap(await funcs.listMeters(client))