@fastn-ai/sdk: fastn SDK for Node.js and TypeScript
Embedded integrations for your product and your AI agents.
fastn is the embedded integration platform for B2B SaaS. @fastn-ai/sdk is its official Node.js
and TypeScript SDK: add customer-facing integrations (CRM, calendar, Slack, and 1,000+ more apps) to your product
through one API, and give your AI agents the same integrations as tools.
Your customers connect their own apps inside your product. Your backend uses those connections through the fastn platform API, and your agents call them as workspace tools or through the fastn MCP gateway.
Built for multi-tenant SaaS:
- One key, every customer. Address each customer by your own id (
fastn.customer("acme-42")); fastn's internal org ids never reach your code. Every call is scoped to that customer, and an account or watch id that belongs to another customer is refused, so one customer's data never leaks into another's. - Each customer brings their own accounts. Send a setup link and the customer connects their HubSpot, Google Calendar, Slack, and so on. Credentials stay in fastn's vault, isolated per customer, and refresh automatically.
- Several accounts per app. A customer can connect two Slack workspaces or two calendars; pick one per call with
connectionId. - One API across providers.
customer.crmandcustomer.calendarwork the same whichever CRM or calendar the customer connected; the provider is picked from their connections. - Safe retries and traceability.
idempotencyKeymakes writes safe to retry, andcorrelationIdlets you ask later what fastn did for a request (customer.callLog(...)). - Governed. Role-based access control, audit logs, and per-customer isolation on a SOC 2 Type II platform.
Use fastn when:
- your customers ask for integrations your team cannot build and maintain one by one;
- every customer needs the same integration connected to their own account and configured to their own fields;
- you want one integration to serve your product, your customers, and your AI agents;
- you need per-customer isolation, audit logs, and SOC 2 for a security review.
The package:
- Node 20+ (built-in fetch), ESM and CommonJS, full types. The one runtime dependency (
json-schema-to-typescript) is used only by thefastn-sdk-toolscode generator. - 461 operations in 19 areas (
orgs,embed,connections,unified,events,workflows, ...), one method per operation, named exactly like the operation in the API docs and the Fastn Workspace tools. Platform-administration routes, which a partner key cannot call, are left out on purpose. - Customer context that hides fastn's internal org ids: you only ever use the id you gave fastn.
- Event signature verification, retries, typed errors, pagination.
Install
npm install @fastn-ai/sdk
Get a platform API key from the fastn dashboard. The client does not read environment variables itself: pass the key in.
import { FastnClient } from "@fastn-ai/sdk"; // ESM
// const { FastnClient } = require("@fastn-ai/sdk"); // CommonJS
60-second tour
import { Apps, Categories, Entities, EventTypes, FastnClient, HubSpotEvents } from "@fastn-ai/sdk";
const fastn = new FastnClient({ apiKey: process.env.FASTN_API_KEY! }); // https://api.fastn.dev
// 1. Register a customer under your account, using YOUR id for them.
const customer = fastn.customer("acme-42");
await customer.register({ name: "Acme Dental" }); // idempotent: returns the existing org on repeat
// 2. Give them a link to connect their tools (Google Calendar, HubSpot, ...), by connector slug.
const link = await customer.connect(Apps.googleCalendar);
console.log(link.url); // send this to the customer
// 3. Typed methods on whatever the customer connected: the provider is picked from their connections.
const booked = await customer.calendar.book(
{ title: "Intro call", start_time: "2026-11-03T15:00:00Z", end_time: "2026-11-03T15:30:00Z", timezone: "Europe/London", attendee_email: "[email protected]" },
{ idempotencyKey: "booking-123", correlationId: "req-abc" }, // safe to retry; traceable later
);
console.log(booked.id, booked.provider, booked.data.web_url);
await customer.calendar.reschedule(booked.id, { start_time: "2026-11-03T16:00:00Z", end_time: "2026-11-03T16:30:00Z" });
await customer.calendar.cancel(booked.id, { idempotencyKey: "cancel-123" });
const { records } = await customer.crm.contacts.find({ email: "[email protected]" }); // one page (the search route has no cursor); list() pages
const contact = records[0] ?? await customer.crm.contacts.create({ first_name: "Jane", last_name: "Doe", email: "[email protected]" });
await customer.crm.notes.add({ body: "Booked an intro call", parent_id: contact.id, parent_type: Entities.contact });
// 4. What did fastn do for my request id?
await customer.callLog("req-abc");
Embed fastn in your product (guide for Claude and other AI coding assistants)
The steps to add customer-facing integrations to a SaaS app with fastn. Use the exact names below; do not invent props or endpoints.
-
Install and get a key.
npm install @fastn-ai/sdk, then create a platform API key in the fastn dashboard and pass it asapiKey. -
Register each of your customers by your own id (backend):
const fastn = new FastnClient({ apiKey: process.env.FASTN_API_KEY! }); const customer = fastn.customer(account.id); // your id for this customer await customer.register({ name: account.name }); // idempotent -
Let the customer connect their apps. Either send a hosted setup link, or show the embedded widget in your UI:
const link = await customer.connect(Apps.hubspot); // hosted page: redirect the customer to link.url const token = await customer.embedToken(); // or: a short-lived token for the in-app widgetOn the frontend, the widget is the
@fastn-ai/embedpackage (FastnProviderwithFastnHuborFastnConnectCardfrom@fastn-ai/embed/react, or the hosted script tag). Copy the exact snippet from the dashboard's Embed tab. See the embedding docs. -
Use the customer's connections from your backend:
customer.crm.contacts.find(...),customer.calendar.book(...), orcustomer.runAction({ connector, action, input })for any app action. -
React to changes:
fastn.webhooks.create(...)once for your account,customer.watch(app, events)per customer, and verify deliveries withparseEvent(secret, headers, rawBody). -
Give your AI agents the same integrations: workspace tools below, or connect Claude, Cursor, or any MCP client to the fastn MCP gateway.
Pricing is per customer connection, with a free plan to start: see fastn.ai/pricing. A machine-readable summary of fastn for LLMs is at fastn.ai/llms.txt.
Configuration
new FastnClient({
apiKey, // required: platform API key, sent as x-api-key
baseUrl, // default https://api.fastn.dev; https://api.fastn-test.dev for development
timeoutMs, // per request, default 60000
retries, // GET and idempotent calls on 429/502/503/504 and network errors, default 2
defaults, // options for every call, e.g. { customer: { ref: "acme-42" } }
onRequest, // (log: RequestLog) => void, after every HTTP attempt
fetch, // a fetch implementation, default globalThis.fetch
userAgent,
tools, // { key, url, timeoutMs }: your fastn Connect workspace, enables fastn.tools
});
Named apps, categories and events
Apps, Categories, Entities, HubSpotEvents, EventTypes and UnifiedActions name every app in the unified
catalog, every category and entity, HubSpot's change events, and the events your webhook receives, so calls are completed and checked by your editor instead of typed as strings:
customer.connect(Apps.googleCalendar), customer.watch(Apps.hubspot, [HubSpotEvents.contactCreated]),
fastn.catalog.fields(Categories.crm, Entities.contact), { provider: Apps.hubspot }. A misspelt member
(Apps.googleCalender, HubSpotEvents.contactCraeted) does not compile. Plain strings are still accepted and not
checked: watch refuses an unknown event name before subscribing (FastnApiError 400 EVENT_NOT_FOUND, with the valid
names), and a missing name (undefined from a misspelt member in plain JavaScript) throws a TypeError naming the call. Each value is the exact string the API
takes, so existing string calls keep working. The lists are the unified catalog on production as of 0.5.0; an app,
category or entity outside them can still be passed as a string. Snippets below import them from @fastn-ai/sdk.
Calendar and CRM, typed
customer.calendar and customer.crm are the unified API with the fields named: each method takes a TypeScript
interface (the catalog's own field names, start_time, first_name, ...; Date values are sent as ISO strings) and
answers { id, provider, data, warnings? } or a page { records, nextCursor, hasMore }. The provider is the one the
customer has connected for that entity: no connected provider answers 404 NO_CONNECTED_PROVIDER (send them a setup
link), several answer 409 PROVIDER_AMBIGUOUS (pass { provider }). warnings lists fields the provider does not
map and therefore dropped. Every method takes { provider?, connectionId?, idempotencyKey?, correlationId?, ... } last.
| Method | Underneath |
|---|---|
calendar.freeSlots({ start_time, end_time, timezone?, slot_minutes? }) | scheduling/availability; answers busy, free, and slots of that length |
calendar.book(meeting), get(id), find({ start_time, end_time }), list({ page_size, cursor }), reschedule(id, { start_time, end_time, duration_minutes? }), cancel(id) | scheduling/meeting |
crm.contacts.create / get / find({ email, phone, first_name, last_name }) / list / update / delete | crm/contact |
crm.notes.add({ body, parent_id, parent_type }), create, get, find, list, update, delete | crm/note (created_at defaults to now) |
crm.tasks.add({ subject, due_date, status, ... }), create, get, find, list, update, delete | crm/task |
crm.deals.create({ name, amount, stage, ... }), get, find, list, update, moveStage(id, stage), delete, stages() | crm/opportunity, crm/stage |
What they do not cover is still one call away: customer.records({ category, entity, provider }) takes any entity
in the catalog with untyped input (create, get, list, update, delete, plus search(filters) and
providers()), and customer.runAction(...) runs a connector's own action.
// The catalog: which categories and entities exist, the fields each takes, which providers a customer connected.
const categories = await fastn.catalog.categories(); // [{ slug: "crm", entities: [{ slug: "contact", operations, createFields }] }]
const fields = await fastn.catalog.fields(Categories.crm, Entities.contact); // [{ field: "email", type: "string", required: false }, ...]
const providers = await customer.catalog.providers(Categories.crm, Entities.contact); // [{ connectorSlug: "hubspot", connected: true, operations: [...] }]
Every area is on the client directly, with typed inputs. The partner flow lives in orgs, embed, connections, unified, events and workflows. Platform-administration areas (admin, platform config, support) and backend-only routes (OAuth callbacks, credential reads, staff approvals) are not generated: a partner key cannot call them, and leaving them out keeps the surface the SDK promises to keep stable small. The generator's HIDDEN_TAGS / PUBLIC_INTERNAL lists in scripts/generate.ts are the single place that rule lives.
await fastn.orgs.listOrgs({ type: OrgTypes.endOrg });
await fastn.connections.listConnections({ endOrgId: await customer.id() });
await fastn.events.listEventSubscriptions();
await fastn.embed.getSetupLink({ id: "stp_..." });
await fastn.orgs.getTenant({ id: "..." }); // every operation is a typed method on its area
await fastn.me(); // who this key is: actor, role, org, parentOrg
Customer ids, explained once
Use fastn.customer(ref) (or the customer option) and nothing else. The raw API names the same customer
differently per operation; those generated input fields still exist, but they are the raw API and will be
renamed on the platform side. Scoping calls to a customer needs a multi-tenant partner API key; the platform
rejects the customer headers on a single-tenant key (not exercised by the tests here).
| You see in the API docs | What it is | In this SDK |
|---|---|---|
orgId (createTenant body) | the id YOU choose for the customer | fastn.customer(ref) |
externalRef, endOrgRef, x-end-org-ref | the same id, under other names | filled in for you |
endOrgId, id (createTenant answer), x-end-org-id | fastn's own id of that customer org | await customer.id() (cached) |
customer.find() returns the customer's org row (or undefined when fastn does not know the ref).
customer.register(), connect(app), links, watch(), disconnect(), renameAccount(), update(), callLog(),
setupLink(), embedToken(), connectWithCredentials(), connections(), get(), remove(), calendar, crm and
records(...) all take the ref and resolve fastn's id only where the API insists on it. The lookup is
limited to orgs under your own org, so a key that can see more than your account can still never resolve someone
else's customer; the resolved id is cached per client.
customer.api.<area>.<operation>() runs any operation for that customer. It sends x-end-org-ref and, on routes that
take the customer in the body instead (endOrgId or externalRef: OAuth initiation, stored connections, event
subscriptions, secrets, setup links, ...), fills that field in too. The platform only reads the header on unified record
and workflow execute routes; without the body field those calls would silently run as your own org.
// A setup link by app: the account's widget for that connector is found for you (several: pass widgetId).
const link = await customer.connect(Apps.hubspot, { connectionName: "Main CRM", redirectUrl: "https://app.acme.com/connected" });
redirect(link.url); // then customer.links.get(link.setupId) / links.cancel(...)
// Or start OAuth directly, without a widget:
const { data } = await customer.connect({ connectorId: Apps.googleCalendar, connectionName: "Google Calendar", returnUrl: "https://app.acme.com/connected" });
redirect(data.authorizationUrl); // the connection lands on the customer org
// Typed-in credentials (API key, basic auth): the fields come from the connector's auth method. The platform checks
// them; an incomplete set answers 400 CREDENTIALS_INCOMPLETE with the missing names (err.body.missing).
const fields = await customer.authFields("acme-crm"); // { names, required, schema, authMethodId }
await customer.connectWithCredentials({ connectorId: "acme-crm", credentials: { apiKey: "..." }, name: "Acme CRM" });
// Changes at an app, sent to your webhook as connector.event / unified.event: watch them per customer.
const accountId = "..."; // one of the customer's accounts, from customer.connections()
const w = await customer.watch(Apps.hubspot, [HubSpotEvents.contactCreated, HubSpotEvents.dealStageChanged], { accountId });
// HubSpotEvents.dealStageChanged delivers every deal property change: keep stage moves with propertyName === "dealstage".
await customer.watches(Apps.hubspot); // [{ id, app, events, status }]
await customer.unwatch(Apps.hubspot); // or unwatch(w.id)
await customer.watchableEvents(Apps.hubspot); // the app's event names you can watch
await customer.disconnect(accountId); // the customer gets connection.revoked
await customer.renameAccount(accountId, "Front desk calendar");
await customer.update({ name: "Acme Dental Ltd" }); // or { status: "suspended" }
await customer.callLog("req-abc"); // what fastn did for your request id
// Your webhook, once for the whole account.
const hook = await fastn.webhooks.create({ url: "https://app.acme.com/fastn", eventTypes: [EventTypes.connectionConnected, EventTypes.setupLinkCompleted, EventTypes.connectorEvent, EventTypes.unifiedEvent] });
hook.secret // shown once: the signing secret for parseEvent
await fastn.webhooks.test(hook.id); await fastn.webhooks.deliveries(hook.id); await fastn.webhooks.rotateSecret(hook.id);
// Anything the unified API does not cover: run the connector's own action with the customer's connection.
const r = await customer.runAction({ connector: Apps.hubspot, action: "updateDeal", input: { dealId, properties: { dealstage: "qualifiedtobuy" } } });
r.data.response // the provider's answer
Prefer setupLink() for credentials: fastn's hosted page collects them, so they never pass through your backend. For
auth types other than custom-input forms the platform may store the credential under a fixed field name that differs
from the auth method's contract; that is a platform limitation being tracked, not something the SDK can see.
Responses and errors
Answers are returned exactly as the REST API sends them (usually { data: ... }; unified records are
{ id, provider, data }), so the SDK agrees 1:1 with the REST docs and Postman collection. Typed where the spec
describes the answer, Record<string, any> otherwise.
Non-2xx answers throw FastnApiError with status, code (e.g. NOT_FOUND, VALIDATION_ERROR,
NO_CONNECTED_PROVIDER, OPERATION_NOT_SUPPORTED, PROVIDER_AMBIGUOUS), message, details, body and requestId
(quote it when asking fastn for help), plus the helpers isNotFound, isConflict, isRateLimited, isAuth and
isValidation. Network failures throw FastnNetworkError.
GET calls are retried on 429/502/503/504 (default 2 retries, honouring Retry-After). A write is retried only when
it carries an idempotencyKey and the platform de-duplicates that operation by it (unified record create, update
and delete, workflow execute; each such method says so in its doc). Every other write is never retried, so a lost
answer cannot make it act twice.
try { await fastn.orgs.getTenant({ id }); }
catch (e) { if (e instanceof FastnApiError && e.isNotFound) ... }
Per-call options
Every method takes an optional second argument:
{ idempotencyKey, correlationId, customer, installationId, targetOrg, headers, extraBody, extraQuery, timeoutMs, retries, signal }
extraBody / extraQuery send fields the SDK does not know yet (a new API field before the SDK catches up); they are
passed as given and not validated.
customer is { ref: "acme-42" } (your id) or { id: "<fastn org id>" }. new FastnClient({ defaults: { customer: { ref: "acme-42" } } }) applies options to every call.
Workspace tools: run anything your workspace has turned on
The platform API above is one door. The other is your fastn Connect workspace (connect.fastn.dev > Tools): every connector you switch on there, Google Calendar, HubSpot, Slack, the Fastn Workspace itself, becomes a tool the SDK can list and run. Nothing in the SDK is tied to a connector; turn one on in the workspace and it shows up.
import { Apps, ToolsClient } from "@fastn-ai/sdk"; // or new FastnClient({ apiKey, tools: { key, url } }).tools
const connect = new ToolsClient({ key: process.env.FASTN_TOOLS_KEY!, url: "https://mcp.fastn.dev/<workspace>" });
const tools = await connect.tools(); // every tool your workspace has switched on, with its input schema
const cal = await connect.toolsOf(Apps.googleCalendar); // one connector's tools
// Call them as typed methods, generated from your workspace (next section): no tool names typed by hand.
// tools.customer("acme-42").googleCalendar.listEvents({ calendarId: "primary", timeMin: new Date().toISOString() })
- The workspace key is a different credential from the platform API key. Read it from Settings > API keys in connect.fastn.dev.
customer(ref)sendsX-Org-Id: ref. A delegated key requires it on every call (400 otherwise); set it once withdefaults: { customer: { ref } }when one client serves one customer.userId(sent as X-User-Id) is needed when the workspace runs calls as each person.- Failures throw
ToolErrorwithkind"http" (gateway), "rpc" (JSON-RPC, e.g. unknown tool), "tool" (the tool ran and answered an error) or "auth" (the app is not connected for this customer yet), plusstatus,code,tool,connector, the gateway's message, and the helpersisUnknownToolandisAuth. For "auth",e.isConnectionRequiredis true ande.authorizeUrlis where to send the customer, so an agent can ask the user to connect at runtime instead of failing. - In "one shared account" mode every call runs as that one account, whatever customer you name. Per-customer accounts need the "each customer org" mode in the workspace.
- The customer is named once. Fastn Workspace tools that act for a customer (unified records) take it as the
endOrgRefargument, andcustomer(ref)fills that in for every tool whose input schema has it, from one tools/list per workspace (shared by everycustomer()clone, kept for 10 minutes). AnendOrgReforendOrgIdyou pass yourself wins;customerArg: falseon the client turns the fill off. Tools of other connectors are left alone: the customer reaches them through X-Org-Id. Callawait connect.tools()once at startup if the first call must not pay for the listing. - One name for the customer: write
orgIdin the arguments of any Fastn Workspace tool. Where the tool spells itendOrgIdorend_org_id(and has noorgIdof its own), the SDK passes the value under that spelling, so your code never carries the older names; a value you pass under the old name is left as it is. - Answers of Fastn Workspace tools and parsed events come back without fastn's internal customer id keys
(
endOrgId,end_org_id): the only customer id you see is the one you chose (orgId/tenantRef).r.textkeeps the raw answer;parseEvent(..., { internalIds: true })keeps the id in an event.
Typed methods for your workspace: fastn-sdk-tools
Tool names as strings work, but your editor cannot check them. The generator turns your workspace's tool list into one TypeScript module with a class per connector and a method per tool, each input typed from the tool's schema:
Run it from a project that has @fastn-ai/sdk installed (the command ships in this package; there is no separate
fastn-sdk-tools package on npm). Without a local install, use npx -p @fastn-ai/sdk fastn-sdk-tools ....
# once, and again whenever the workspace changes (commit the file)
export FASTN_TOOLS_KEY=... # the workspace key; it never ends up in the file
npx fastn-sdk-tools --url https://mcp.fastn.dev/<workspace> --out src/fastn-tools.ts
npx fastn-sdk-tools --url https://mcp.fastn.dev/<workspace> --out src/fastn-tools.ts --check # CI: exit 1 on drift
npx fastn-sdk-tools --import ./my-client.js ... # optional: module to import the client from
import { ToolsClient } from "@fastn-ai/sdk";
import { createTools } from "./fastn-tools.js";
const tools = createTools(new ToolsClient({ key: process.env.FASTN_TOOLS_KEY!, url: "https://mcp.fastn.dev/<workspace>" }));
// or createTools(fastn) with a FastnClient created with tools: { key, url }
const acme = tools.customer("acme-42"); // X-Org-Id on every call, endOrgRef filled where a tool takes it
const cals = await acme.googleCalendar.listCalendars({}); // tools.<connector>.<tool>(args, opts?)
const created = await acme.hubspot.createContact({ email: "[email protected]", firstname: "Jane" }); // args typed from the schema
const cats = await tools.fastnPlatform.listUnifiedCategories(); // a tool with no inputs takes no argument
created.data // ToolResult, as from call(); ToolError on failure
--url+--key(orFASTN_TOOLS_URL/FASTN_TOOLS_KEY; only the CLI reads these) fetch the listing;--from tools-list.jsongenerates offline from a saved tools/list (the same list gives the same file).--customer <ref>fetches the listing as that customer. The header records the url, customer and tool count, never the key.- The module exports
createTools,Tools, one class per connector (GoogleCalendarTools), one input interface per tool (GoogleCalendarListEventsInput), an output type where the tool publishes anoutputSchema(else the answer isunknown), andTOOL_NAMES. Tools without a connector prefix (skill,feedback) are methods on the root. - Names are made identifiers (
google-calendar->googleCalendar,create-contact->createContact); a clash after that gets a name-derived variant (create_contact,createContactTool). Property names stay as the schema spells them (quoted when needed). A tool whose schema says nothing about a field types itunknown; an object with no declared properties isRecord<string, unknown>. tools.clientis theToolsClientunder the methods, for a tool the file does not know yet;client.rpc(method, params)sends any other JSON-RPC call to the workspace.- Programmatic use:
import { generateToolsModule } from "@fastn-ai/sdk/codegen". The schema-to-type step isjson-schema-to-typescript, the package's one runtime dependency (the CLI runs from the installed package, so it cannot be a devDependency); it is loaded by the generator only, never by the client.
Events (webhooks)
import { Apps, EventTypes, parseEvent, verifyEventSignature } from "@fastn-ai/sdk";
// Express: app.post("/fastn/events", express.raw({ type: "application/json" }), (req, res) => {
const event = parseEvent(process.env.FASTN_EVENT_SECRET!, req.headers, req.body); // throws on a bad signature
// event.id is stable across retries: drop repeats by id.
switch (event.type) {
case EventTypes.connectionConnected: if (event.app === Apps.hubspot) { /* start a watch */ } break;
case EventTypes.setupLinkCompleted: /* event.connections: app -> account id */ break;
case EventTypes.unifiedEvent: /* event.action: UnifiedActions.created | updated | deleted */ break;
}
The signature is sha256=HMAC-SHA256(secret, "<X-Fastn-Timestamp>.<raw body>"); deliveries older than 5 minutes
are rejected ({ toleranceSeconds }, 0 disables the check). Always pass the raw body, never a re-serialised object.
parseEvent throws a plain Error on a bad signature. verifyEventSignature answers a boolean instead,
readEventHeaders reads the fastn headers, and signEventBody signs a body the same way (for tests).
Pagination
import { paginate } from "@fastn-ai/sdk";
for await (const ev of paginate((i) => fastn.events.listEvents(i), { outcome: EventOutcomes.sentToDlq }, { pageSize: 100 })) ...
Works for limit/offset lists and cursor lists; stops on a short page, a missing cursor, or a list that ignores paging.
Development
npm run generate # spec/openapi-*.json -> src/generated (ops, types, api)
npm test # unit tests with a mocked fetch
FASTN_API_KEY=... FASTN_TEST_CUSTOMER=... npm run test:live # live run on api.fastn-test.dev (creates and removes test data)
# FASTN_BASE_URL=https://api.fastn.dev LIVE_PROD=1 ... runs the same suite on production
# FASTN_API_KEY=... npx tsx scripts/sweep/run.ts # calls every published operation once on dev and writes a per-operation report
npm run build # dist/ (esm + cjs + d.ts)
npm run spec:refresh -- /path/to/fastn-platform # pull a newer spec from a platform checkout
spec/SOURCE.md records which platform commit the spec came from. The spec has more operations than the SDK exposes:
461 remain after the platform-administration and backend-only routes are hidden.
What stays compatible
- The hand-written layer (
FastnClient,customer(...),records(...),ToolsClient,parseEvent,paginate, the error classes) follows semver: breaking changes only in a major version. Until 1.0 they can still land in a minor and are listed under Changed in the CHANGELOG. - The generated methods mirror the REST API 1:1.
spec/surface.lock.jsonrecords every operation with its input keys (where each goes on the wire and whether it is required) and its output fields (path, structural type, whether always present; no descriptions).npm run generaterefuses a spec that removes an operation, renames or moves an input, makes an optional input required, adds a required input, removes or retypes an output field, or stops guaranteeing one, unless run with--accept-breaking, which goes with a major version bump. New operations, new optional inputs, new output fields and description edits pass.npm run generate:check -- --spec=<file>reports what a spec would change without writing anything (exit 1 on a break), for CI and for trying a refresh. - Everything under
unstable(the wire manifestOPS,FetchTransport) has no compatibility promise.
Releasing
- Bump
versioninnode/package.jsonand updatenode/CHANGELOG.mdin a PR. - After merge, tag
mainand push the tag:git tag node/v<version> && git push origin node/v<version> - The
Publish Node SDKworkflow checks the tag matchespackage.json, runs the checks, and runsnpm stage publish. It uses theNPM_TOKENsecret (a stage-only token) from thenpmGitHub environment. - A maintainer opens npmjs.com > Staged Packages, reviews the version and approves it with 2FA. Only then is it installable.
FAQ
What is fastn? The embedded integration platform for B2B SaaS. Your customers connect their own apps (HubSpot, Google Calendar, Slack, and 1,000+ more) inside your product, and your code and AI agents use those connections through one API.
How do I add customer-facing integrations to my Node.js or TypeScript app?
Install @fastn-ai/sdk, register each customer with fastn.customer(yourId).register(...), let them connect apps
with a setup link (customer.connect(app)) or the @fastn-ai/embed widget, then call customer.crm,
customer.calendar, or customer.runAction(...). See
Embed fastn in your product.
Does fastn support multi-tenant apps? Yes. Every call is scoped to one customer, addressed by your own id; credentials are stored and refreshed per customer; a customer can connect several accounts of one app.
Can AI agents like Claude use fastn integrations?
Yes: as workspace tools from this SDK (ToolsClient, typed with fastn-sdk-tools), or through the
fastn MCP gateway from Claude, Cursor, or any MCP client.
Is there a Python SDK?
Yes: fastn-ai (pip install fastn-ai). Its fastn.platform client has the
same surface as this package's platform client, and it adds tool calling for OpenAI, Anthropic, Gemini, and Bedrock,
flows, and a CLI. This package's workspace tools (ToolsClient, fastn-sdk-tools) are Node only.
License
MIT