# The Soca SDK

> The official @usesoca/sdk TypeScript client: zero dependencies, fully typed, one method per real endpoint: invoices, plans, payments, subscriptions, events, metering, credits, entitlements, and webhook verification.

Part of the Soca documentation (https://soca.finance/docs). Human version: https://soca.finance/docs/developers/sdk
`@usesoca/sdk` is the official TypeScript client for the Soca API. It is a thin, zero-dependency, fully-typed wrapper over the versioned REST surface. Every method maps 1:1 to a real `/v1` endpoint, so anything you can do here you can also do with plain HTTP (see the [API reference](/docs/developers/api-reference)).

## Install and initialize

```bash
npm i @usesoca/sdk
```

```typescript
import { Soca } from '@usesoca/sdk';

const soca = new Soca(process.env.SOCA_SECRET_KEY);

// Self-hosting or testing against another environment? Point baseUrl at it:
const staging = new Soca(process.env.SOCA_SECRET_KEY, { baseUrl: 'https://staging.example.com' });
```

The constructor takes your secret key (`sk_test_...` or `sk_live_...`) and optional `{ baseUrl, fetch }` options. `baseUrl` defaults to `https://soca.to`. The key must stay server-side. See [Authentication](/docs/developers/authentication). Amounts everywhere are USDC base units (6 decimals): $9.00 = `9_000_000`.

> The SDK also lives in the Soca monorepo at `packages/sdk`. If you are working inside the repo, import it from there instead of npm.

## Invoices

```typescript
// One-time payment request with a hosted checkout.
const invoice = await soca.invoices.create({
  amount: 2_500_000,            // $2.50
  description: 'Acme Pro, March',
  invoice_number: 'INV-1041',   // optional: your own reference
  recipient: 'billing@acme.com',// optional, who it's billed to
  due_at: '2026-08-01',         // optional: ISO-8601 due date
});
// → invoice.checkout_url: send the customer here; fulfill on invoice.paid.

const invoices = await soca.invoices.list({ limit: 50 });
```

## Plans

```typescript
// Fixed subscription plan: returns a hosted checkout_url for subscribers.
const plan = await soca.plans.create({
  name: 'Pro',
  amount: 9_000_000,   // $9.00 per period; minimum 1_000_000 (1.00 USDC)
  interval: 'month',   // 'week' | 'biweekly' | 'month' | 'year' | 'custom'
  // interval_days: 45 // required only when interval is 'custom' (1–365)
});
// → plan.checkout_url: access starts on the subscription.active webhook.
```

**Usage-based (metered) plan**

```typescript
// Auto-charges the customer's metered usage each period, capped at 'amount'.
// Create the meter first (see below), then reference its id.
const meter = await soca.meters.create({ key: 'api_calls', display_name: 'API calls' });

const metered = await soca.plans.create({
  name: 'API',
  amount: 100_000_000,        // per-period CAP (max ever pulled) — $100.00
  interval: 'month',
  usage_type: 'metered',
  meter: meter.id,
  pricing_model: { kind: 'per_unit', unitAmount: 100 }, // $0.0001 per call
});
// Subscribers approve the cap once; usage is priced and pulled at each period end,
// never above the cap. No charge at subscribe. Send them to metered.checkout_url.
```

Plans are create-only over the API for now; there is no `plans.list()`. `pricing_model` also supports `package`, `tiered` (`graduated`/`volume`), and `flat` — see the `SocaPricingModel` type.

## Payments, subscriptions, and events

```typescript
const payments = await soca.payments.list();            // confirmed on-chain payments
const subscriptions = await soca.subscriptions.list();  // subscription records
const events = await soca.events.list({ limit: 100 });  // the feed backing your webhooks

// Every list method returns the same cursor-paginated envelope:
// { object: 'list', data: [...], has_more: boolean, next_cursor: string | null }
```

## Meters and meter events

Create a meter (or in the dashboard), then ingest usage against it. `meters.create` is idempotent by `key`, and event ingestion is idempotent per `idempotency_key`: retrying with the same key never double-counts.

```typescript
// Create a meter — its 'key' is the event_name you send usage with.
const meter = await soca.meters.create({
  key: 'api_calls',
  display_name: 'API calls',
  aggregation: 'sum',   // 'sum' | 'count' | 'max' | 'last'
});

// A feature to gate with entitlements (boolean, or metered against a meter).
await soca.features.create({ key: 'pro_models', name: 'Pro models' });
```

```typescript
const meters = await soca.meters.list();

// Single event. Count-aggregated meters default each event's value to 1.
await soca.meterEvents.create({
  event_name: 'api_call',
  external_customer_id: 'user_812',  // your own stable customer id
  value: 3,
  idempotency_key: 'req_7f3a1c',     // safe to retry
});

// Batch: up to 500 events per request.
await soca.meterEvents.createBatch([
  { event_name: 'api_call', external_customer_id: 'user_812', value: 3, idempotency_key: 'req_7f3a1c' },
  { event_name: 'tokens', external_customer_id: 'user_812', value: 1520, idempotency_key: 'req_7f3a1d' },
]);
```

## Credits

Prepaid customer credits: grant a balance, debit it as usage happens, read it back. A debit only draws down what the credits cover and reports the rest as `shortfall` for you to bill separately, or pass `allow_negative: true` to let the balance go below zero instead.

```typescript
// Top up: idempotent per idempotency_key.
await soca.credits.grant({
  external_customer_id: 'user_812',
  amount: 50_000_000,               // $50.00 of credit
  reason: 'Starter pack',
  expires_at: '2027-01-01',         // optional expiry
  idempotency_key: 'grant_2026_07_starter_812',
});

// Draw down for usage.
const debit = await soca.credits.debit({
  external_customer_id: 'user_812',
  amount: 1_250_000,                // $1.25 of usage
  reason: 'July overage',
});
// debit.covered  : what the balance paid for
// debit.shortfall: the remainder to bill by other means (0 when fully covered)

// Read the balance (takes the Soca customer id).
const balance = await soca.credits.balance('cus_...');
```

## Entitlements

Entitlements are the access-control layer tied to what a customer has paid for. Grant a feature, then check it from your backend before serving the feature. This is the pattern for gating anything behind a plan or a manual grant:

```typescript
// Grant a feature (optionally metered with a limit, optionally expiring).
await soca.entitlements.grant({
  external_customer_id: 'user_812',
  feature_key: 'api_access',
  limit_value: 10_000,        // optional: usage limit for metered features
  active_until: '2026-12-31', // optional: when the entitlement lapses
});

// Gate a feature: check before serving.
const access = await soca.entitlements.check('cus_...', 'api_access');
if (!access.entitled) {
  throw new Error(`Access denied: ${access.reason}`);
}
// For metered features: access.limit, access.used, access.remaining
```

## Error handling

Every non-2xx response throws a `SocaError` carrying the HTTP `status` and the machine-readable `code` from the [error envelope](/docs/developers/errors-and-testing):

```typescript
import { Soca, SocaError } from '@usesoca/sdk';

try {
  await soca.invoices.create({ amount: 0, description: 'Oops' });
} catch (err) {
  if (err instanceof SocaError) {
    console.error(err.status, err.code, err.message);
    // e.g. 400 'invalid_request' 'amount must be greater than zero.'
    if (err.status === 429) {
      // back off and retry
    }
  } else {
    throw err;
  }
}
```

## Webhook verification

The SDK ships the same verification the platform uses: HMAC-SHA256 over `timestamp.rawBody`, timing-safe comparison, and a 5-minute replay window:

```typescript
import { verifyWebhookSignature, WEBHOOK_SIGNATURE_HEADER } from '@usesoca/sdk';

// WEBHOOK_SIGNATURE_HEADER === 'x-wc-signature'
const ok = verifyWebhookSignature(process.env.SOCA_WEBHOOK_SECRET, rawBody, signatureHeader);
if (!ok) {
  // Reject with 400. Do not process the payload.
}
```

Pass the **raw** request body string, before any JSON parsing. Full details on headers, retries, and the event table are in [Webhooks and events](/docs/developers/webhooks).
