@unitpay/sdk
Version:
Official UnitPay SDK for TypeScript — Node, browser, and edge. Portal sessions, subscriptions, invoices, credits, usage tracking.
197 lines (147 loc) • 5.89 kB
Markdown
# /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 [`/react`](../react) instead — it holds a
> customer-scoped **portal token** inside `<UnitPayProvider>`. Portal tokens are not a
> valid option for this server SDK.
```bash
npm install /sdk
# or
bun add /sdk
```
## Quick start
```ts
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: 'billing.com',
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
```ts
// 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`:
```ts
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:
```ts
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:
```ts
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:
```ts
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`:
```ts
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-After` on 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
```ts
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)
```ts
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