@unitpay/sdk
v1.32.0
Published
Official UnitPay SDK for TypeScript — Node, browser, and edge. Portal sessions, subscriptions, invoices, credits, usage tracking.
Readme
@unitpay/sdk
Official UnitPay server SDK for JavaScript — Node, edge, and any runtime with
fetch. Authenticates with a secret key (upay_sk_…) for full server-to-server
access.
Building a customer-facing UI? Use
@unitpay/reactinstead — it holds a customer-scoped portal token inside<UnitPayProvider>. Portal tokens are not a valid option for this server SDK.
npm install @unitpay/sdk
# or
bun add @unitpay/sdkQuick start
import { UnitPay } from '@unitpay/sdk'
const unitpay = new UnitPay({ apiKey: process.env.UNITPAY_SK! })
// apiKey falls back to the UNITPAY_API_KEY env var if omitted.
// 1. Identify a customer
const customer = await unitpay.customers.create({
name: 'Acme, Inc.',
email: '[email protected]',
externalId: 'acme', // your internal id
})
// 2. Gate a feature (the entitlement hot-path)
const { access, remaining } = await unitpay.check({
customerId: customer.id,
featureSlug: 'api_calls',
})
if (!access) throw new Error('Upgrade required')
// 3. Record usage
await unitpay.track({ customerId: customer.id, eventName: 'api_call', quantity: 1 })Resources
All methods require a secret key.
| Resource | Methods |
|---|---|
| client | check(params) · track(event \| event[]) · verifyWebhook (static) |
| customers | create · get · list · update · archive · check · checkBatch · entitlements · listInvoices · listPaymentMethods · listCreditAccounts |
| subscriptions | create · get · list · change · cancel · uncancel |
| portalSessions | create · revoke |
| usage | track |
To list a customer's subscriptions, use subscriptions.list({ customerId }).
Entitlement checks
// Flat check — most common.
const res = await unitpay.check({ customerId, featureSlug: 'seats', requestedUsage: 3 })
// res.access, res.remaining, res.limit, res.feature.type, …
// Customer-scoped, and a batch variant for rendering an access matrix.
await unitpay.customers.check(customerId, { featureSlug: 'seats' })
const { results } = await unitpay.customers.checkBatch(customerId, ['seats', 'api_calls'])Subscriptions & payment outcomes
create and change return a PaymentOutcome — discriminate on kind:
const outcome = await unitpay.subscriptions.create({ customerId, planId })
switch (outcome.kind) {
case 'created_no_charge': // free plan — nothing to collect
case 'charged_inline': // paid off-session with a card on file
break
case 'requires_form': // no card on file — collect payment
// Ship outcome.client to the browser and mount <PaymentForm> (@unitpay/react)
break
case 'invoice_sent': // hosted invoice emailed (send_invoice)
break
}Customer identify (no upsert)
There is no upsert endpoint. create on a duplicate externalId throws a
ConflictError. For an idempotent identify, catch it and look up:
import { ConflictError } from '@unitpay/sdk'
async function identify(params) {
try {
return await unitpay.customers.create(params)
} catch (err) {
if (err instanceof ConflictError && params.externalId) {
const { data } = await unitpay.customers.list({ externalId: params.externalId })
if (data[0]) return data[0]
}
throw err
}
}Usage tracking
Send one event, or an array of up to 100 in a single call:
await unitpay.track({ customerId, eventName: 'api_call', quantity: 1 })
await unitpay.track([
{ customerId, eventName: 'api_call', quantity: 5 },
{ customerId, eventName: 'tokens', quantity: 1200 },
])Pass an idempotencyKey per event to make retries safe.
Pagination
list* methods return a thenable, async-iterable page:
const page = await unitpay.customers.list() // { data, hasMore, nextCursor }
for await (const customer of unitpay.customers.list()) { // auto-follows cursors
console.log(customer.id)
}
const all = await unitpay.customers.list().toArray()Errors
Typed hierarchy — switch on class or on .code:
import { UnitPay, NotFoundError, RateLimitError, ApiError } from '@unitpay/sdk'
try {
await unitpay.customers.get('cus_bad')
} catch (err) {
if (err instanceof NotFoundError) { /* … */ }
if (err instanceof RateLimitError) { console.log('retry after', err.retryAfter) }
if (err instanceof ApiError) { console.log(err.code, err.requestId) }
}Exported: ApiError, AuthenticationError, BadRequestError, ConflictError,
ConnectionError, InternalServerError, NotFoundError, RateLimitError,
TimeoutError, ValidationError.
Behavior
- Retries on 429 / 409 / 5xx / transient network errors. Exponential backoff
(500ms × 2ⁿ), jittered, capped 10s. Respects
Retry-Afteron 429. - Idempotency keys auto-generated on every POST/DELETE. Override with
{ idempotencyKey }in request options. - Timeout 30s default. Override per-client (
timeout: 10_000) or per-request. - AbortController pass
{ signal }to cancel. - Events
unitpay.on('request', …)/on('response', …)for tracing. - Custom fetch inject via
{ fetch: myFetch }(undici in Node, MSW in tests, etc.).
Configuration
new UnitPay({
apiKey: process.env.UNITPAY_SK!, // or UNITPAY_API_KEY env var
baseUrl: 'https://api.useunitpay.com/v1', // default
timeout: 30_000,
retries: 2,
idempotencyKeyPrefix: '',
fetch: globalThis.fetch,
})Webhook verification (Node only)
const event = await UnitPay.verifyWebhook(rawBody, request.headers, process.env.WEBHOOK_SECRET!)Requires the optional svix peer dep: npm i svix.
Compatibility
- Node ≥ 18 (native
fetch,crypto.randomUUID) - Edge runtimes (Cloudflare Workers, Vercel Edge, Deno)
License
MIT
