Server-side client for the viaSocket Apps API. Let your users connect Slack, GitHub, Google Sheets or any of 2,300+ apps from your own product, then run actions and receive events through one API — with the OAuth, token refresh, per-app request shapes, polling and retries handled for you.
Zero dependencies. Node 20+, Bun, Deno and edge runtimes.
npm install viasocket-appsThe same calls are documented as plain HTTP in your viaSocket dashboard under Integrations → your Apps API integration → API reference. Use whichever suits your stack; this package is a thin, typed layer over those endpoints and adds nothing they don't have.
This repo also carries the viaSocket integrations skill — what a coding agent reads to integrate any app into your product, with or without this package. Install it into Claude Code, Cursor, Codex, Copilot, Gemini CLI, Windsurf, Cline and 20+ other agents:
npx skills add Walkover-Web-Solution/viasocket-appsAdd -g to install it for every project. No install? Paste this into your agent:
Read https://flow.viasocket.com/documentation/skill.md and follow it.
The agent needs three values from the viaSocket dashboard (Integrations → Create embed →
Install Code): org_id and project_id, which the dashboard's "Copy prompt" hands it, and
VIASOCKET_EMBED_SECRET, which goes in your server's .env.
skills/viasocket-integrations/SKILL.md is the install
file: the description that makes an agent load the skill, and one instruction — fetch the current
skill, https://flow.viasocket.com/documentation/skill.md, every time it is used — so an install
never goes stale.
Before you ship — with or without an agent — check the code against the live catalog:
curl -fsSL https://flow.viasocket.com/viasocket-check.mjs -o viasocket-check.mjs
node viasocket-check.mjs srcIt finds every viaSocket id and URL in your code and says whether each names a real app, action or
trigger, is used where its call expects that kind of id (the two ids of an action look alike), and
whether any viasocket URL was built by hand. No dependencies; exits 1 on a failure, so it can be a
test. It reads no .env file and prints no line of code.
import { ViaSocket } from 'viasocket-apps'
const viasocket = new ViaSocket({
orgId: process.env.VIASOCKET_ORG_ID,
projectId: process.env.VIASOCKET_PROJECT_ID,
secret: process.env.VIASOCKET_EMBED_SECRET // server-side only
})
// Everything is done for one of your end users, identified by an id you choose.
const user = viasocket.user('user_123')
// 1. Your frontend needs a token to open the connect popup.
const embedToken = await user.token()
// The app by its slug, and the event by its action_id and the version your code was written for —
// both read from the live catalog, never copied ids. Each event's page lists them:
// https://flow.viasocket.com/documentation/google-sheets.md
const serviceId = await viasocket.catalog.serviceIdFor('google-sheets')
const actionVersionId = await viasocket.catalog.versionIdFor(serviceId, '<action_id>', '<version>')
// 2. After the popup posts back an auth_id: enable the app, once, to run actions. A paused app is
// found too, and resumed with enableFlow rather than enabled twice.
const scriptId = (await user.findEnabled(serviceId)) ?? (await user.enable(serviceId, authId)).scriptId
// 3. Fill a dropdown with the user's real data.
const { options } = await user.listOptions(actionVersionId, {
fieldKey: 'spreadsheet_Id',
authId,
existingFields: {}
})
// 4. Run an action. No token — the script_id is the credential.
const result = await viasocket.runAction(scriptId, actionVersionId, {
spreadsheet_Id: options[0].value,
grid_Id: '0',
column_key: true
})
// 5. Or subscribe to an event. Needs only the connection, not enable(). `code` is a handler we
// run on our servers when the event fires — see "Events" below. Save the scriptId it returns
// together with the inputData and handler you sent: the flows list never returns those.
const subscription = await user.subscribe(triggerVersionId, {
authId,
inputData: { channel_id: ['C08SXCV3J85'] },
code: handler,
meta: { user_id: 'user_123' }
})import { connect } from 'viasocket-apps/browser'
// embedToken comes from your backend (user.token()), signed for the signed-in user.
// actions (optional): the action_ids and trigger_ids you use — the popup then asks only
// for their scopes; skipActionSelection goes straight to the app's consent screen.
const { authId } = await connect({ embedToken, serviceId: 'rowbu58rc', actions: ['<action_id>'], skipActionSelection: true })
// Send authId to your backend, which enables the app and stores the script_id.
// When user.listConnections() shows a connection with isExpired: true, sign in to it again:
await connect({ embedToken, serviceId: 'rowbu58rc', authId }) // updates it in place; resolves with the same authIdserviceId is the app's service_id exactly as the catalog returns it — a bare id such as
rowbu58rc, never the app's name or slug, never the id with a name joined on. connect() loads
viaSocket's connect script from https://flow.viasocket.com/connect.js and lets it open the popup;
that script is the only supported way to open it, so never build a viasocket URL for it by hand.
connect() rejects with a ViaSocketConnectError whose code is 'closed' (the user seems to
have given up), 'rejected' (the app or viaSocket said no), 'invalid' (serviceId is not a
service id) or 'script' (the connect script did not load). 'closed' is best-effort: once the
popup is on the app's own sign-in page a close can only be inferred, and arrives about a minute
later; if the user then finishes anyway, onLateSuccess is called with the same result a resolved
connect() gives. Treat 'closed' as "nothing yet", not as a verdict.
A chat assistant we run for each of your users: threads and history kept per user, your prompt per call, your own endpoints as its tools — one of which can run an action in the user's apps. Backend only, like everything else here: the prompt and the tools' headers are yours.
const reply = await user.ai.send({
threadId: 'support-7f3a9c', // your own string per conversation; omit to start one
message: 'Where is the order for customer 4471?',
prompt: 'You are an order assistant. Use lookup_order to fetch orders. Answer in one sentence.',
tools: [
{
name: 'lookup_order',
description: 'Fetches an order by customer id',
url: 'https://api.your-product.com/orders/lookup',
headers: { Authorization: `Bearer ${process.env.ORDERS_API_KEY}` },
fields: { id: { type: 'string', description: 'Customer id to look up' } },
requiredParams: ['id']
}
]
})
reply.content // the answer; with responseType 'json_object' or 'json_schema', the parsed object
const threads = await user.ai.threads() // [{ threadId, title, updatedAt }] — title is null until the AI writes it
const page = await user.ai.history(threadId, { page: 1 }) // { messages, page, hasMore } — 40 exchanges a page, page 1 the latest
const socket = await user.ai.rtlayerToken() // for a chat UI: the browser subscribes with it, then you send with delivery: 'rtlayer'The whole contract — tools, JSON answers, the websocket path and what arrives on it — is flow.viasocket.com/documentation/ai.md.
| Call | What it does |
|---|---|
viasocket.user(id) |
Scope every call below to one end user. |
user.token() |
Signed embed token for that user; hand it to the frontend. |
user.enable(serviceId, authId) |
Turn a connection into a scriptId for running actions. Once per connection. |
user.findEnabled(serviceId) |
The scriptId if this app is already enabled, else null. Call before enable. |
user.listOptions(actionVersionId, { fieldKey, authId, existingFields }) |
The values a field accepts, as { options, offset }. Both response shapes normalised; an unreadable connection (revoked, another user's auth_id) throws a ViaSocketError with status 400 even though the API answers 200. |
viasocket.runAction(scriptId, actionVersionId, inputData) |
Run one action. Returns the app's own response. |
viasocket.catalog.search(key) |
The best 30 apps for what the user typed, as { serviceId, name, iconUrl, description }. No token. |
viasocket.catalog.list({ limit, offset, category }) |
Every app, most used first, 200 a page; an empty page ends it. Drop apps whose versions() is null or empty. No token. |
viasocket.catalog.versions(serviceId) |
One app and every published action and trigger of it, as { service, actions, triggers }. Each action has action_id and action_version_id, each trigger trigger_id and trigger_version_id, named as the calls take them: the version id for runAction, listOptions and subscribe; the other for connect, automations and the prebuilt UI. versionid is the published version (V107). input_schema is the form schema. null for an unknown id. No token. |
viasocket.catalog.serviceIdFor(slug) |
The service_id of the app with this slug (slack, google-sheets). Exact and case-sensitive; an unknown slug throws. Cached. No token. |
viasocket.catalog.versionIdFor(serviceId, event, version?) |
The version id the calls take, for one event: event is its action_id / trigger_id (survives a rename) or its page path (send-message). When the app has published a version other than version, it warns and returns the published one. Cached 10 minutes. No token. |
viasocket.catalog.listBuiltIn() |
The built-in plugs (Run AI Model, Pause Workflow, Memory, scraping, GST…): every plug tagged SDK, with its actions and whether it needs a sign-in. No token. |
viasocket.sampleEvent(triggerId, authId, authType, inputData?) |
One real event of a trigger for one connection: what a handler reads at context.req.body. Takes the trigger_id; authType is service.auth_type. Map from this, never from sample_output. |
user.subscribe(triggerVersionId, { authId, inputData, code | webhook, meta }) |
Subscribe to an app event with a handler we run per event. Returns { scriptId, hookUrl, … }. |
user.updateSubscription(scriptId, { code, meta }) |
Change a live subscription in place. |
user.listFlows() |
Every enabled app, subscription and automation this user has, each active, paused or deleted. |
user.setFlowStatus(scriptId, n) |
Pause (2), resume or restore (1), or delete (0) one flow. Setting the state it is already in succeeds. |
user.disableFlow(scriptId) / user.enableFlow(scriptId) / user.deleteFlow(scriptId) |
Pause, resume, delete. |
user.renameFlow(scriptId, { title, description, meta }) |
Rename a flow; meta comes back as metadata in the prebuilt UI's events. |
user.listConnections() |
Every app this user has connected; isExpired: true means reconnect it in place with connect({ authId }). |
user.revokeConnection(authId) |
Disconnect an app. Pause its flows first. |
user.ai.send({ message, threadId, prompt, tools, responseType, delivery }) |
One message to the assistant we run for this user. Returns { threadId, messageId, content, finishReason }, or { threadId, messageId, channel } with delivery: 'rtlayer'. |
user.ai.threads() / user.ai.history(threadId, { page }) |
This user's threads, newest first (title is null until the AI writes it); one thread's messages, 40 exchanges a page. |
user.ai.rtlayerToken() |
A 48-hour subscribe token for the browser, with the channelPrefix every thread's channel starts with. |
signEmbedToken({ orgId, projectId, uniqueIdentifier, secret }) |
The signing step on its own, if you want it without a client. |
Upgrading from 0.6: disableFlow now pauses a flow (status 2). In 0.6 it sent status 0,
which deletes — a deleted flow is still listed as deleted and enableFlow restores it.
findEnabled now also returns a paused app.
Subscribing to an event means telling us what to do when it fires. That is code: a handler we
run on viaSocket's servers per event, with the event in context.req.body. Your server is not in
the path. Most often the handler runs an action in another app the user connected — the run URL
takes no token, the script_id is the credential — or calls your own API with your own auth header
baked in. It is a string executed away from your process, so it must stand alone: no imports,
nothing from your codebase; axios, fetch and context are in scope.
// A string, not a function: it runs on viaSocket's servers each time the event fires.
// In scope there: axios, fetch, context. The event the app sent is context.req.body.
// ${…} are YOUR variables, baked in per user when you subscribe.
const handler = `
const event = context.req.body
const response = await fetch("https://flow.sokt.io/func/${slackScriptId}", {
method: "POST",
headers: { "Content-Type": "application/json" },
body: JSON.stringify({
action_version_id: "${slackSendMessageVersionId}",
// Slack Send Message V107's inputData; the keys of event come from a real sampleEvent
inputData: {
destination: { messageto: "channel", channel_id: ["${channelPickedAtSetup}"] },
markdown_content: "New mail from " + event.from + ": " + event.subject
}
})
})
const result = await response.json()
// Envelope { success, data }, or the action's own body: throw so the run history shows the failure.
if ("data" in result ? !result.success : result.success === false) throw new Error(result.message || "action failed")
return result
`
await user.subscribe(gmailNewMailVersionId, { authId, inputData: {}, code: handler, meta: { user_id: uniqueIdentifier } })webhook is the edge case: a public, unauthenticated URL of yours that we POST every raw event to.
Use it only when that is what you want, and never both.
Some triggers are polled rather than pushed. For those, inputData may carry scheduledTime, the
minutes between checks, as a string: "5" or "15".
Every failure — a non-2xx status or a body with success: false — throws a ViaSocketError
carrying the API's own message, the HTTP status, and the parsed body.
- The secret and the token stay on the server. The frontend gets a token per popup, nothing more.
- A
scriptIdis a credential. Anyone holding it can run that app as that user. - Ids are fetched, never guessed. Spreadsheet ids, channel ids, repository ids all come from
listOptions. existingFieldsis not optional when a field depends on another. Options are scoped by it.- One
uniqueIdentifierper end user, forever. Connections and subscriptions are isolated by it. - Enable only for actions, and check
findEnabledfirst. Subscriptions need just the connection.
Defaults target production: https://flow-api.viasocket.com for the API and
https://flow.sokt.io for running actions. Pass apiBaseUrl / runBaseUrl to point elsewhere,
and scriptUrl to connect() for a non-production connect script — the script picks its own API
and auth hosts from the origin it is served from, so that is that stack's copy
(https://dev-flow.viasocket.com/connect.js).
MIT