sob-connect-sdk
v0.1.0
Published
Node.js/TypeScript SDK for the Connect API (partner-facing REST API).
Readme
sob-connect-sdk
Lightweight Node.js / TypeScript SDK for seamless SOB Connect API integration — a partner-facing REST API for managing a company's workspace users, access levels, and user types.
- Node >= 22 (uses native
fetch, zero runtime dependencies) - ESM only, written in strict TypeScript
- Automatic retries on rate limiting, automatic idempotency keys on writes
Installation
npm install sob-connect-sdkQuick start
Partners receive a bootstrap config file from the platform admin UI, shaped:
{
"slug": "acme-co",
"base_url": "https://api.example.com",
"token": "sob_xxxxx"
}Point the client at that file's path:
import { SobConnectClient } from 'sob-connect-sdk';
const client = new SobConnectClient('./connect-bootstrap.json');
const { workspace, access_levels, user_types } = await client.config.get();
const { data: users, meta } = await client.users.list({ filter: { status: 'active' } });Manual config
You can also construct the client from an in-memory object instead of a file path — useful when the bootstrap values come from your own secret store:
const client = new SobConnectClient({
slug: 'acme-co',
base_url: 'https://api.example.com',
token: process.env.CONNECT_API_TOKEN!,
});Both forms accept an optional second argument:
const client = new SobConnectClient(source, {
timeoutMs: 15_000, // default 10_000
maxRetries: 3, // default 2
fetch: myCustomFetch, // default globalThis.fetch — useful for tests
});Resource reference
client.config
get(): Promise<WorkspaceConfig>— workspace metadata plus the full list of access levels and user types.
client.accessLevels
list(): Promise<AccessLevel[]>
client.userTypes
list(): Promise<UserType[]>
client.users
list(params?: ListUsersParams): Promise<PaginatedResponse<ConnectUser>>— filter bystatus,access_level_id,user_type_id,email; sort and paginate withsort,perPage,page.upsert(payload, opts?): Promise<{ user: ConnectUser; created: boolean }>— creates or updates a user by email.createdis derived from the HTTP status (201 vs 200).get(email): Promise<ConnectUser>update(email, payload, opts?): Promise<ConnectUser>changeRole(email, accessLevelId, opts?): Promise<ConnectUser>changeStatus(email, status?, opts?): Promise<ConnectUser>— omitstatusto toggle active/inactive.recordLogin(email, loggedInAt?, opts?): Promise<ConnectUser>delete(email): Promise<void>
All mutating methods accept an optional { idempotencyKey?: string } as
their last argument — see Retries & idempotency.
Error handling
Every non-2xx response is mapped to a typed error, all extending
SobConnectError (message, status, type):
| Class | HTTP status | type |
| ------------------------ | ----------- | ---------------- |
| AuthenticationError | 401 | authentication |
| AuthorizationError | 403 | authorization |
| NotFoundError | 404 | not_found |
| ValidationError | 422 | validation |
| RateLimitError | 429 | rate_limited |
| SobConnectNetworkError | — | network_error |
ValidationError additionally exposes errors: Record<string, string[]>.
RateLimitError additionally exposes retryAfterSeconds: number.
An unrecognized error.type from the API falls back to the base
SobConnectError rather than throwing an unrelated error, so future server
error types degrade gracefully instead of crashing the SDK.
Note: config/usage mistakes (a missing or malformed bootstrap file/object)
throw a plain Error, not a SobConnectError — these are local mistakes,
not API failures.
import { NotFoundError, ValidationError, SobConnectError } from 'sob-connect-sdk';
try {
await client.users.get('[email protected]');
} catch (error) {
if (error instanceof NotFoundError) {
// handle 404
} else if (error instanceof ValidationError) {
console.error(error.errors);
} else if (error instanceof SobConnectError) {
console.error(error.type, error.status, error.message);
} else {
throw error;
}
}Retries & idempotency
Retries are automatic by default. The SDK retries only on HTTP 429 (rate limited) responses:
- If the response has a
Retry-Afterheader, it waits that many seconds. - Otherwise it backs off exponentially (
500ms * 2^attempt, capped at 8s, ±20% jitter). - It retries up to
maxRetriestimes (default 2, so 3 attempts total) before throwingRateLimitError. - Any other status (including 5xx) is never retried.
- Set
maxRetries: 0in the client options to disable retries entirely.
Idempotency keys are automatic by default. Every POST/PUT/PATCH
request automatically attaches a fresh Idempotency-Key header (a
crypto.randomUUID()), which the server caches per-token for 24h. GET and
DELETE never send this header. Pass your own key via opts.idempotencyKey
on any mutating resource method to control retries/dedupe across process
restarts:
await client.users.upsert(payload, { idempotencyKey: 'my-own-key-for-this-op' });Rate limits
The Connect API allows 60 requests/min per bearer token. Every response
includes X-RateLimit-Limit and X-RateLimit-Remaining headers; the SDK
tracks the most recently observed values on the client:
await client.users.list();
console.log(client.lastRateLimit); // { limit: 60, remaining: 59 }License
MIT
