> ## Documentation Index
> Fetch the complete documentation index at: https://docs.useplinth.xyz/llms.txt
> Use this file to discover all available pages before exploring further.

# Node.js SDK

> A thin, typed wrapper around the REST API. Install it and you have types for everything.

The SDK wraps every API endpoint and gives you full TypeScript types — request bodies, response shapes, and event payloads. It's built on `fetch`, has no heavyweight dependencies, and works in Node.js 18+.

## Install

```bash theme={null}
npm install @nomba/sdk
```

## Initialise

```typescript theme={null}
import { Plinth } from '@nomba/sdk';

const client = new Plinth({
  apiKey: process.env.NOMBA_API_KEY!,
});
```

Pass `apiKey` explicitly or set `NOMBA_API_KEY` as an environment variable — the SDK picks it up automatically if you don't pass it.

## Resources

Every API resource maps to a method on the client:

### `client.customers`

```typescript theme={null}
client.customers.create(body)              // POST /v1/customers
client.customers.get(id)                   // GET /v1/customers/:id
client.customers.list(params?)             // GET /v1/customers
client.customers.entitlements(id)          // GET /v1/customers/:id/entitlements
client.customers.provisionVirtualAccount(id)  // POST /v1/customers/:id/virtual-account
client.customers.getVirtualAccount(id)        // GET /v1/customers/:id/virtual-account
```

### `client.subscriptions`

```typescript theme={null}
client.subscriptions.create(body, opts?)   // POST /v1/subscriptions
client.subscriptions.get(id)               // GET /v1/subscriptions/:id
client.subscriptions.status(id)            // GET /v1/subscriptions/:id/status
client.subscriptions.cancel(id)            // POST /v1/subscriptions/:id/cancel  (policy-driven)
client.subscriptions.reactivate(id)        // POST /v1/subscriptions/:id/reactivate
client.subscriptions.previewChange(id, body)   // POST /v1/subscriptions/:id/preview-change
client.subscriptions.change(id, body)          // POST /v1/subscriptions/:id/change
client.subscriptions.changeCheckout(id, body)  // POST /v1/subscriptions/:id/change-checkout
client.subscriptions.checkoutLink(id, body?)   // POST /v1/subscriptions/:id/checkout-link
client.subscriptions.cancelScheduledChange(id, changeId) // DELETE /v1/subscriptions/:id/scheduled-change/:changeId
```

### `client.plans`

```typescript theme={null}
client.planGroups.create(body)             // POST /v1/plan-groups
client.planGroups.createPlan(groupId, body) // POST /v1/plan-groups/:id/plans
client.plans.list(params?)                 // GET /v1/plans
```

### `client.invoices`

```typescript theme={null}
client.invoices.list(params?)              // GET /v1/invoices
client.invoices.get(id)                    // GET /v1/invoices/:id
client.invoices.lineItems(id)              // GET /v1/invoices/:id/line-items
```

## Webhook handler

`constructEvent` verifies the `Plinth-Signature` header against your endpoint secret (`whsec_…`) and
returns the parsed event. Pass the **raw body**. For a framework-agnostic version and the exact
signing scheme, see [Receiving webhooks](/guides/receiving-webhooks).

```typescript theme={null}
import { constructEvent } from '@nomba/sdk/webhooks';
import express from 'express';

const app = express();

app.post('/api/plinth/webhook', express.raw({ type: 'application/json' }), async (req, res) => {
  let event;
  try {
    event = constructEvent(
      req.body,                                  // raw body
      req.headers['plinth-signature'] as string, // t=<unix>,v1=<hmac>
      process.env.PLINTH_WEBHOOK_SECRET!,        // whsec_…
    );
  } catch {
    return res.status(401).send('invalid signature');
  }

  switch (event.type) {
    case 'subscription.activated':
      await grantFullAccess(event.data.object.customerId);
      break;
    case 'subscription.past_due':
      await showPaymentWarning(event.data.object.customerId);
      break;
    case 'subscription.delinquent':
      await revokeAccess(event.data.object.customerId);
      break;
    case 'subscription.recovered':
      await restoreAccess(event.data.object.customerId);
      break;
  }

  res.sendStatus(200);
});
```

`constructEvent` throws if the signature doesn't match — return `401` in that case.

## Error handling

The SDK throws typed errors for every failure case:

```typescript theme={null}
import { NombaApiError, NombaSignatureError } from '@nomba/sdk';

try {
  const customer = await client.customers.get('cus_invalid...');
} catch (err) {
  if (err instanceof NombaApiError) {
    console.error(err.status);   // e.g., 404
    console.error(err.message);  // "Customer not found"
    console.error(err.code);     // "not_found"
  }
}
```

`NombaApiError` covers all HTTP errors (4xx, 5xx). `NombaSignatureError` covers invalid webhook signatures.

## Idempotency keys

Pass an idempotency key in options to safely retry:

```typescript theme={null}
const subscription = await client.subscriptions.create(body, {
  idempotencyKey: `sub-${customerId}-${planId}-${Date.now()}`,
});
```

<Note>
  A Python SDK is in progress. In the meantime, the REST API works with any HTTP client — see the curl examples throughout the docs.
</Note>
