UNPKG

@unitpay/sdk

Version:

Official UnitPay SDK for TypeScript — Node, browser, and edge. Portal sessions, subscriptions, invoices, credits, usage tracking.

584 lines (572 loc) 21.7 kB
/** * Domain entity types returned by the UnitPay API. * * These are hand-curated to reflect what the current server actually emits — * not auto-generated from the backend schema, so the SDK's public surface * stays stable even when internal shapes evolve. Keep conservative: only add * fields once they're confirmed stable. * * Fields that the server may return `null` for are typed `T | null`; fields * the server may omit entirely are marked optional (`?`). A field that is *always* present and non-null should have neither. */ type CustomerStatus = 'active' | 'archived'; type CustomerType = 'individual' | 'company'; type CollectionMethod = 'charge_automatically' | 'send_invoice'; interface Customer { addressCity: string | null; addressCountry: string | null; addressLine1: string | null; addressLine2: string | null; addressPostalCode: string | null; addressRegion: string | null; billingEmail: string | null; country: string | null; createdAt: string; createdBy: string | null; defaultPaymentMethodId: string | null; domain: string | null; email: string | null; externalId: string | null; id: string; invoiceEmails: string[]; invoiceRemindersEnabled: boolean; language: string | null; legalName: string | null; metadata: Record<string, unknown>; name: string | null; organizationId: string; phone: string | null; registrationNumber: string | null; salesMotion: string | null; status: CustomerStatus; taxExempt: boolean; taxId: string | null; taxRateOverride: number | null; timezone: string | null; type: CustomerType; updatedAt: string; updatedBy: string | null; } type SubscriptionStatus = 'pending' | 'trialing' | 'active' | 'past_due' | 'ended'; type BillingInterval = 'daily' | 'weekly' | 'monthly' | 'quarterly' | 'yearly'; interface Subscription { activatedAt: string | null; billingInterval: BillingInterval; billingPeriodCount: number; budgetAmount: number | null; budgetCurrency: string | null; cancelAt?: string | null; canceledAt?: string | null; collectionMethod: CollectionMethod; createdAt: string; currency: string; currentBillingPeriodEnd: string; currentBillingPeriodStart: string; customerId: string; endedAt?: string | null; id: string; metadata?: Record<string, unknown>; netPaymentTermDays: number | null; organizationId: string; planId: string; startDate: string; status: SubscriptionStatus; termEndsAt: string | null; trialEnd: string | null; updatedAt: string; } type InvoiceStatus = 'draft' | 'issued' | 'partially_paid' | 'paid' | 'overdue' | 'void' | 'uncollectible'; interface Invoice { amountDue: number; amountPaid: number; createdAt: string; currency: string; dueDate: string | null; id: string; invoiceNumber: string | null; paidAt: string | null; status: InvoiceStatus; totalAmount: number; } type PaymentMethodType = 'card' | 'link'; type PaymentMethodStatus = 'active' | 'detached' | 'expired'; interface CardBrand { brand: string; expMonth: number; expYear: number; last4: string; } interface PaymentMethod { card: CardBrand | null; createdAt: string; id: string; isDefault: boolean; status: PaymentMethodStatus; type: PaymentMethodType; } interface CreditAccount { available: number; balance: number; createdAt: string; currencyId: string; customerId: string; id: string; reserved: number; updatedAt: string; } interface TrackEventResult { accepted: number; rejected: number; rejections?: Array<{ index: number; reason: string; message: string; }>; } interface PortalSession { expiresAt: string; token: string; } type FeatureType = 'boolean' | 'config' | 'metered' | 'credit' | 'enumType'; type EntitlementSourceType = 'promotional' | 'manual' | 'plan' | 'addon'; type ResetInterval = 'billing_period' | 'hour' | 'day' | 'week' | 'month' | 'quarter' | 'year' | 'lifetime'; /** * One row of a customer's active entitlement matrix * (`customers.entitlements(id)` → `GET /customers/:id/entitlements`). * Type-dependent fields are nullable — narrow against `feature.type`. */ interface Entitlement { id: string; feature: { slug: string; type: FeatureType; name: string; units: string | null; }; access: boolean; source: { type: EntitlementSourceType; planId: string | null; subscriptionId: string | null; }; remaining: number | null; usage: number | null; limit: number | null; isUnlimited: boolean; value: number | null; periodStartedAt: string | null; nextResetAt: string | null; resetInterval: ResetInterval | null; } type SalesMotion = 'self_serve' | 'sales_led'; /** * PSP bootstrap bundle the frontend `<PaymentForm>` needs to render an inline * payment/setup form. Mirrors `@unitpay/payments` `ClientInitBundle`. Present * on a `requires_form` outcome — pass to `<PaymentForm>` in `@unitpay/react`. */ interface PaymentClientBundle { accountId?: string; mode: 'payment' | 'setup'; provider: string; providerExtras?: Record<string, unknown>; publishableKey: string; token: string; } /** * Uniform result of every payment-bearing operation (`subscriptions.create`, * `subscriptions.change`, …). Discriminate on `kind`: * - `charged_inline` — paid off-session with a card on file * - `invoice_sent` — hosted invoice emailed (SLG / send_invoice) * - `invoice_added` — appended to an open invoice * - `requires_form` — mount `<PaymentForm>` with `client` to collect a PM * - `deferred` — scheduled for a future moment (renewal / trial / dated start) * - `created_no_charge` — free plan / zero amount, nothing to collect * - `no_action` — nothing to do (already paid, no delta, …) */ type PaymentOutcome = { kind: 'charged_inline'; paymentIntentId: string; paymentMethodId: string; invoice?: Invoice; subscription?: Subscription; creditAccount?: CreditAccount; } | { kind: 'invoice_sent'; invoice: Invoice; emailedTo: string; netPaymentTermDays: number; subscription?: Subscription; } | { kind: 'invoice_added'; invoice: Invoice; lineAmount: number; } | { kind: 'requires_form'; checkoutSessionId: string; client: PaymentClientBundle; reason: string; failureCode?: string; failureMessage?: string; } | { kind: 'deferred'; subscription?: Subscription; invoice?: Invoice; scheduledFor: string; reason: 'plan_change_deferred' | 'trial_pending' | 'future_dated_start'; } | { kind: 'created_no_charge'; subscription: Subscription; } | { kind: 'no_action'; reason: 'zero_amount' | 'already_paid' | 'sub_active_no_change' | 'trial_no_card' | 'trial_card_using_existing_pm'; }; type PaymentOutcomeKind = PaymentOutcome['kind']; interface RequestOptions { idempotencyKey?: string; signal?: AbortSignal; timeout?: number; } interface Page<T> { data: T[]; hasMore: boolean; nextCursor: string | null; prevCursor: string | null; } /** * Configuration for the `UnitPay` server SDK. * * `apiKey` is required — secret key (`upay_sk_…`) for server use. Read from * `UNITPAY_API_KEY` if omitted. Portal-token auth is intentionally NOT * supported here: portal tokens are customer-facing and belong in * `@unitpay/react`, which manages their lifecycle via `<UnitPayProvider>`. */ interface UnitPayConfig { apiKey?: string; baseUrl?: string; fetch?: typeof globalThis.fetch; idempotencyKeyPrefix?: string; retries?: number; timeout?: number; } interface CheckParams { customerId: string; featureSlug: string; /** Metered/credit features — amount you're about to consume. */ requestedUsage?: number; /** Enum features — the value(s) you want to gate on. */ requestedValues?: string[]; } /** `CheckParams` without `customerId` — for `customers.check(id, …)`. */ type CustomerCheckParams = Omit<CheckParams, 'customerId'>; /** * Uniform entitlement answer. Every field beyond `access`/`feature`/ * `isFallback` is type-dependent — narrow against `feature.type`. The index * signature is a forward-compat safety net for fields added server-side. */ interface CheckResponse { access: boolean; feature: { slug: string; type: FeatureType; name: string; units: string | null; }; source?: { type: EntitlementSourceType; subscriptionId?: string; }; isFallback: boolean; deniedReason?: string; remaining?: number; usage?: number; limit?: number; isUnlimited?: boolean; periodStartedAt?: string; nextResetAt?: string; resetInterval?: ResetInterval; creditBalance?: number; value?: number; enumValues?: string[]; [key: string]: unknown; } interface BatchCheckResponse { customerId: string; results: Record<string, CheckResponse>; } interface CreateCustomerParams { name: string; type?: 'individual' | 'company'; email?: string; billingEmail?: string; externalId?: string; invoiceEmails?: string[]; invoiceRemindersEnabled?: boolean; phone?: string; language?: string; timezone?: string; country?: string; legalName?: string; registrationNumber?: string; domain?: string; taxId?: string; taxExempt?: boolean; taxRateOverride?: number | null; defaultNetPaymentTermDays?: number | null; addressLine1?: string | null; addressLine2?: string | null; addressCity?: string | null; addressRegion?: string | null; addressPostalCode?: string | null; addressCountry?: string | null; metadata?: Record<string, unknown>; } /** All fields optional — an omitted key is left untouched (never clobbered). */ type UpdateCustomerParams = Partial<CreateCustomerParams>; interface CreateSubscriptionParams { customerId: string; planId: string; /** Defaults to 'self_serve'. Pair 'sales_led' with collectionMethod 'send_invoice'. */ salesMotion?: 'self_serve' | 'sales_led'; /** Defaults to 'charge_automatically'. */ collectionMethod?: 'charge_automatically' | 'send_invoice'; startDate?: string; termEndsAt?: string; trialEnd?: string; externalId?: string; netPaymentTermDays?: number; currency?: string; budgetAmount?: number; budgetCurrency?: string; billingCycleAnchor?: string; /** Force `requires_form` even when a PM is on file (mount inline `<PaymentForm>`). */ forceForm?: boolean; metadata?: Record<string, unknown>; } interface ChangeSubscriptionParams { newPlanId: string; forceForm?: boolean; idempotencyKey?: string; } interface CancelSubscriptionParams { /** True cancels now; false (default) schedules cancel at period end. */ cancelImmediately?: boolean; cancellationReason?: string; cancellationNote?: string; } type FetchPage<T> = (cursor?: string) => Promise<Page<T>>; declare class PagePromise<T> implements PromiseLike<Page<T>>, AsyncIterable<T> { private readonly fetchPage; private readonly initialPromise; constructor(fetchPage: FetchPage<T>); then<TResult1 = Page<T>, TResult2 = never>(onfulfilled?: ((value: Page<T>) => TResult1 | PromiseLike<TResult1>) | null, onrejected?: ((reason: unknown) => TResult2 | PromiseLike<TResult2>) | null): Promise<TResult1 | TResult2>; catch<TResult = never>(onrejected?: ((reason: unknown) => TResult | PromiseLike<TResult>) | null): Promise<Page<T> | TResult>; [Symbol.asyncIterator](): AsyncGenerator<T>; toArray(): Promise<T[]>; } declare abstract class ApiResource { protected client: UnitPay; constructor(client: UnitPay); protected _get<T>(path: string, query?: Record<string, unknown>, options?: RequestOptions): Promise<T>; protected _post<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>; protected _put<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>; protected _patch<T>(path: string, body?: unknown, options?: RequestOptions): Promise<T>; protected _delete<T>(path: string, options?: RequestOptions): Promise<T>; protected _getPage<T>(path: string, params?: Record<string, unknown>): PagePromise<T>; } /** * Customer resource — sk-side reads + writes. * * There is no upsert endpoint: `create` on a duplicate `externalId` throws a * 409 `ConflictError` (code `CUS_DUPLICATE_EXTERNAL_ID`). For an idempotent * "identify", catch the conflict and fall back to `list({ externalId })`. * * To list a customer's subscriptions, use * `unitpay.subscriptions.list({ customerId })` — the server does not expose * a customer-nested subscriptions route. */ declare class Customers extends ApiResource { create(params: CreateCustomerParams, options?: RequestOptions): Promise<Customer>; get(id: string, options?: RequestOptions): Promise<Customer>; list(params?: Record<string, unknown>): PagePromise<Customer>; update(id: string, params: UpdateCustomerParams, options?: RequestOptions): Promise<Customer>; archive(id: string, options?: RequestOptions): Promise<Customer>; listInvoices(id: string, params?: Record<string, unknown>): PagePromise<Invoice>; listPaymentMethods(id: string, params?: Record<string, unknown>): PagePromise<PaymentMethod>; listCreditAccounts(id: string, params?: Record<string, unknown>): PagePromise<CreditAccount>; /** Active entitlement matrix for the customer. */ entitlements(id: string, params?: Record<string, unknown>): PagePromise<Entitlement>; /** Does this customer have access to `featureSlug` right now? */ check(id: string, params: CustomerCheckParams, options?: RequestOptions): Promise<CheckResponse>; /** Resolve up to 50 feature slugs in one round trip. */ checkBatch(id: string, featureSlugs: string[], options?: RequestOptions): Promise<BatchCheckResponse>; } interface CreatePortalSessionParams { customerId: string; /** Lifetime in seconds. Min 60, max 86400, default 3600. */ ttlSeconds?: number; } type RevokePortalSessionParams = { jti: string; } | { customerId: string; }; /** * Portal sessions — sk-only on this SDK. * * Customer-facing lifecycle (refresh, self-logout) lives in `@unitpay/react`, * which holds the portal token inside `<UnitPayProvider>`. */ declare class PortalSessions extends ApiResource { create(params: CreatePortalSessionParams, options?: RequestOptions): Promise<PortalSession>; revoke(params: RevokePortalSessionParams, options?: RequestOptions): Promise<void>; } /** * Subscription resource — sk-side lifecycle. * * `create` and `change` return a `PaymentOutcome` (discriminate on `kind`): * a paid plan with no card on file yields `requires_form` — mount the returned * `client` bundle in `<PaymentForm>` (`@unitpay/react`) to collect payment. * * `list` supports `?customerId=…&status=…` via query params. */ declare class Subscriptions extends ApiResource { create(params: CreateSubscriptionParams, options?: RequestOptions): Promise<PaymentOutcome>; get(id: string, options?: RequestOptions): Promise<Subscription>; list(params?: Record<string, unknown>): PagePromise<Subscription>; change(id: string, params: ChangeSubscriptionParams, options?: RequestOptions): Promise<PaymentOutcome>; cancel(id: string, params?: CancelSubscriptionParams, options?: RequestOptions): Promise<Subscription>; uncancel(id: string, options?: RequestOptions): Promise<Subscription>; } interface TrackEventInput { customerId: string; eventName: string; idempotencyKey?: string; properties?: Record<string, unknown>; quantity?: number; timestamp?: string; } /** * Usage tracking. Send one event, or an array of up to 100 events in a single * call (`unitpay.usage.track([...])`) — the shortcut `unitpay.track(...)` * forwards here. `POST /usage/track`. */ declare class Usage extends ApiResource { track(params: TrackEventInput | TrackEventInput[], options?: RequestOptions): Promise<TrackEventResult>; } type EventType = 'request' | 'response'; interface RequestEvent { attempt: number; method: string; path: string; } interface ResponseEvent { requestId: string | null; status: number; } type EventCallback = (event: RequestEvent | ResponseEvent) => void; /** * UnitPay server SDK — requires a secret key. * * For browser / customer-facing use, import from `@unitpay/react` instead; * portal tokens are managed there by `<UnitPayProvider>`. * * Resources: * portalSessions create / revoke * customers create / get / list / update / archive / check / checkBatch / * entitlements / listInvoices / listPaymentMethods / * listCreditAccounts * (subscriptions for a customer: use subscriptions.list({ customerId })) * subscriptions create / get / list / change / cancel / uncancel * usage track * * Flat helpers on the client: `check(...)` (entitlement gate) and `track(...)`. * * Resource surface mirrors what `apps/server/` actually exposes today. When * the server grows new endpoints, matching SDK methods land here. The SDK * deliberately does NOT advertise methods the server can't serve. */ declare class UnitPay { private readonly apiKey; private readonly baseUrl; private readonly timeout; private readonly maxRetries; private readonly fetchFn; private readonly idempotencyKeyPrefix; private readonly listeners; private _portalSessions?; get portalSessions(): PortalSessions; private _customers?; get customers(): Customers; private _subscriptions?; get subscriptions(): Subscriptions; private _usage?; get usage(): Usage; constructor(config?: UnitPayConfig); track(...args: Parameters<Usage['track']>): Promise<TrackEventResult>; /** * Entitlement gate — "does this customer have access to `featureSlug` right * now?" Read-only; deductions happen via `track`. `POST /check`. */ check(params: CheckParams, options?: RequestOptions): Promise<CheckResponse>; on(event: EventType, callback: EventCallback): this; off(event: EventType, callback: EventCallback): this; private emit; request<T>(method: string, path: string, body?: unknown, options?: RequestOptions & { query?: Record<string, unknown>; }): Promise<T>; static verifyWebhook(body: string | Buffer, headers: Record<string, string>, secret: string): Promise<Record<string, unknown>>; get redactedApiKey(): string; } declare class ApiError extends Error { readonly status: number; readonly code: string; readonly requestId: string | null; readonly headers: Record<string, string>; constructor(params: { status: number; code: string; message: string; requestId: string | null; headers: Record<string, string>; }); toString(): string; static generate(status: number, body: { error?: { code?: string; message?: string; }; } | null, headers: Headers): ApiError; } declare class BadRequestError extends ApiError { constructor(params: ConstructorParameters<typeof ApiError>[0]); } declare class AuthenticationError extends ApiError { constructor(params: ConstructorParameters<typeof ApiError>[0]); } declare class NotFoundError extends ApiError { constructor(params: ConstructorParameters<typeof ApiError>[0]); } declare class ConflictError extends ApiError { constructor(params: ConstructorParameters<typeof ApiError>[0]); } declare class ValidationError extends ApiError { constructor(params: ConstructorParameters<typeof ApiError>[0]); } declare class RateLimitError extends ApiError { readonly retryAfter: number | null; constructor(params: ConstructorParameters<typeof ApiError>[0] & { retryAfter: number | null; }); } declare class InternalServerError extends ApiError { constructor(params: ConstructorParameters<typeof ApiError>[0]); } declare class ConnectionError extends ApiError { constructor(message: string); } declare class TimeoutError extends ApiError { constructor(timeoutMs: number); } declare const SDK_VERSION = "1.32.0"; export { ApiError, AuthenticationError, BadRequestError, type BatchCheckResponse, type BillingInterval, type CancelSubscriptionParams, type CardBrand, type ChangeSubscriptionParams, type CheckParams, type CheckResponse, type CollectionMethod, ConflictError, ConnectionError, type CreateCustomerParams, type CreatePortalSessionParams, type CreateSubscriptionParams, type CreditAccount, type Customer, type CustomerCheckParams, type CustomerStatus, type CustomerType, Customers, type Entitlement, type EntitlementSourceType, type FeatureType, InternalServerError, type Invoice, type InvoiceStatus, NotFoundError, type Page, PagePromise, type PaymentClientBundle, type PaymentMethod, type PaymentMethodStatus, type PaymentMethodType, type PaymentOutcome, type PaymentOutcomeKind, type PortalSession, PortalSessions, RateLimitError, type RequestOptions, type ResetInterval, type RevokePortalSessionParams, SDK_VERSION, type SalesMotion, type Subscription, type SubscriptionStatus, Subscriptions, TimeoutError, type TrackEventInput, type TrackEventResult, UnitPay, type UnitPayConfig, type UpdateCustomerParams, Usage, ValidationError };