sowork
v0.4.0
Published
Official TypeScript client for the SoWork API.
Maintainers
Readme
SoWork
The official TypeScript client for the SoWork API.
SoWork is a virtual office for remote teams. This package lets you bring SoWork into your own workflows and automations — read and update presence, look up teammates, trigger in-office actions, send chat messages, and subscribe to webhooks.
Installation
npm install soworkUsage
Authenticate with an API key generated in your SoWork settings, then call the API:
import { SoWork } from 'sowork';
const client = new SoWork('sw_your_key');
// Read the authenticated user
const me = await client.me.getMe();
// Update your presence in the office
await client.me.updatePresence({
textStatus: 'Coding',
availability: 'busy',
});The client is organized into namespaces that mirror the API: me, users, office, chat, meetings, insights, and subscriptions. See the full reference for every method and its parameters.
Streaming events
Subscriptions deliver events three ways: webhook push to a URL, cursor polling, and live streaming. A subscription created without a url is inbox-only — matched events accumulate in a durable server-side log (72-hour retention) that you consume directly. Nothing to host, no tunnel.
// Create an inbox-only subscription (no url) for the events you care about
const { data: sub } = await client.subscriptions.create({
events: ['me.presence_updated', 'me.chat_mentioned'],
});
// Stream it. Auto-reconnect with backoff, resume, goodbye handling, an idle
// watchdog, and duplicate suppression are all built in.
await client.streamEvents({
subscriptionId: sub.id,
cursor: readCursorFromDisk(), // undefined on first run
onEvent: async (event, logId) => {
console.log(event.type, event.data);
writeCursorToDisk(logId); // the resume cursor
},
});Persist logId after each event and pass it back as cursor when your process restarts — the durable log means you catch up losslessly on everything you missed while down.
Routing for centralized apps: every event envelope carries officeId
(the office of the subscription it was delivered for) and — when the
subscription belongs to an app installation — installationId. An app
serving several workspaces should route on event.installationId to pick
which installation's credentials to act with (it's the same value you pass
as installation_id when minting a token). On webhook deliveries both
fields sit inside the signed body, so a verified delivery's routing
context can be trusted directly.
Prefer request/response? client.events.listEvents({ subscriptionId, after }) pages the same log with a cursor. And raw HTTP works too: the stream is standard Server-Sent Events on GET /v1/events/stream (resume via the Last-Event-ID header), the poll is GET /v1/events.
For a complete worked example — a bot that connects, streams, replies, and survives restarts — see examples/echo-bot. For the shape a real product takes — webhook delivery, many installations in one process, and an LLM answering from live workspace data — see examples/office-assistant.
Command-line interface
The package also ships a sowork command. Install it globally and authenticate once:
npm install -g sowork
sowork login # paste an API key generated in your SoWork settingsThen drive the API straight from your shell:
sowork status "Coding" # set your status text
sowork status --availability busy # set your availability
sowork users # list teammatesChannel and DM listings return top-level messages only, so threads and the files hanging off them need their own commands:
sowork chat messages <channelId> # top-level messages (replyCount marks threads)
sowork chat replies <messageId> # the thread: root message + its replies
sowork chat attachment <messageId> <idx> # short-lived signed URL for one attachment
sowork chat send <channelId> "text" --parent-id <rootId> # reply inside a thread
sowork chat dm <userId> "text" --with <userId> <userId> # group DM (up to 8)Ended meetings and their notes, transcripts, chat and recordings live in the library:
sowork meetings library list
sowork meetings library search "onboarding" --kinds note transcript
sowork meetings library get <digestId> --include notes --include transcript
sowork insights working-hours --from 2026-08-01 --to 2026-08-07Commands mirror the SDK namespaces (sowork users, sowork chat, sowork meetings, sowork insights, sowork webhooks) - each with subcommands, plus short top-level aliases for the common ones. Run sowork --help or sowork <command> --help to explore.
The CLI takes its API key from --api-key, then the SOWORK_API_KEY environment variable, then the credentials saved by sowork login (in ~/.config/sowork/config.json). Add --json to any command for machine-readable output.
Authenticating as an app
SoWork apps authenticate with their installation credentials instead of an API key. Pass them to the constructor and the client handles the whole token lifecycle for you — it mints short-lived access tokens, caches them, refreshes ahead of expiry, and retries once if a token stops verifying. Your code never sees a token:
import { SoWork } from 'sowork';
const client = new SoWork({
app: {
clientId: process.env.SOWORK_CLIENT_ID,
clientSecret: process.env.SOWORK_CLIENT_SECRET,
installationId: process.env.SOWORK_INSTALLATION_ID,
},
});
// Who am I installed as?
const { app, installation, group } = await client.app.getApp();
// Post as the app (thread replies via parentId; the Idempotency-Key
// header makes retried deliveries collapse to one message)
await client.chat.sendChannelMessage(
channelId,
{ text: 'Hello from my app!' },
{ headers: { 'Idempotency-Key': eventId } },
);Every resource method works identically across both credential types - the server resolves the principal from the bearer token.
Self-hosted apps bootstrap from a single-use setup token minted in SoWork's "connect your agent" flow: SoWork.exchangeSetupToken('sw_app_setup_…') returns the installation credentials once — persist them like a password, then construct the client with them.
Verifying webhooks
Incoming webhook deliveries are signed. Verify the signature before trusting a payload:
import { verifyWebhookSignature, WebhookSignatureError } from 'sowork';
try {
verifyWebhookSignature({
secret: process.env.SOWORK_WEBHOOK_SECRET,
signatureHeader: request.headers['sowork-signature'],
rawBody, // the raw, unparsed request body string
});
// signature is valid — handle the event
} catch (err) {
if (err instanceof WebhookSignatureError) {
// reject the request (e.g. respond 401)
}
throw err;
}Documentation
Full API reference: https://api.sowork.com/public/documentation
License
MIT © Sophya, Inc.
