Browse docs

Developers

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.

@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).

Install and initialize

npm i @usesoca/sdk
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. 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

// One-time payment request with a hosted checkout.
const invoice = await soca.invoices.create({
amount: _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

// Fixed subscription plan: returns a hosted checkout_url for subscribers.
const plan = await soca.plans.create({
name: 'Pro',
amount: _000_000, // $9.00 per period; minimum _000_000 (1.00 USDC)
interval: 'month', // 'week' | 'biweekly' | 'month' | 'year' | 'custom'
// interval_days: 45 // required only when interval is 'custom' (1365)
});
// plan.checkout_url: access starts on the subscription.active webhook.
Usage-based (metered) plan
// 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: _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

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.

// 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' });
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.

// Top up: idempotent per idempotency_key.
await soca.credits.grant({
external_customer_id: 'user_812',
amount: _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: _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:

// 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: _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:

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:

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.

Was this page helpful?

A quick signal helps us improve the right pages.