Server
`ConnectClient` and `createConnectHandler`. Holds the API key — never import this from browser code.
Generated from @cosmoops/connect-sdk/server's TypeScript source and its own doc comments - see Reference for the other entry points. Regenerated on every deploy; don't hand-edit.
Classes
Class: ConnectClient
The CosmoOps Connect application-partner API.
Covers /api/v1/application/* and nothing else - the surface an application platform
integrates against. Connect's internal API is not part of this package and
never will be.
const connect = new ConnectClient({
apiKey: process.env.CONNECT_API_KEY!,
clientId: process.env.CONNECT_CLIENT_ID,
});
const session = await connect.identity.exchangeCode({ code, redirectUri });
const { ready } = await connect.entitlements.getReadiness(session.user.workspaceId);Every failure throws a ConnectError carrying a stable code. Nothing here
holds state between calls, so one client can be built once and shared.
Constructors
Constructor
new ConnectClient(options: ConnectClientOptions): ConnectClient;Parameters
| Parameter | Type |
|---|---|
options | ConnectClientOptions |
Returns
ConnectClient
Properties
identity
readonly identity: IdentityResource;"Login with Connect" - the OAuth2 handshake and the session after it.
access
readonly access: AccessResource;What a signed-in member may do on your application.
apps
readonly apps: AppsResource;The applications a workspace can open - for a launcher grid.
entitlements
readonly entitlements: EntitlementsResource;Whether a workspace may use your application, and on what plan.
billing
readonly billing: BillingResource;Your plan catalog, and metering usage against add-on balances.
drive
readonly drive: DriveResource;Files in a workspace's Drive.
scheduling
readonly scheduling: SchedulingResource;A subscribing workspace's own calendar - what is bookable, free, and booked.
contacts
readonly contacts: ContactsResource;Who a workspace already knows how to reach, by phone or by id.
channels
readonly channels: ChannelsResource;The phone numbers Connect has assigned to your application, and where each one routes.
calls
readonly calls: CallsResource;The calls that arrived on those channels, with their transcripts.
telephony
readonly telephony: TelephonyResource;Calls on the numbers your application handles, when it answers its own calls - see TelephonyResource.
doNotCall
readonly doNotCall: DoNotCallResource;The workspace's do-not-call list, which Connect enforces on every outbound call - see DoNotCallResource.
credit
readonly credit: CreditResource;Prepaid credit a workspace buys from Connect and your application spends per use - see CreditResource.
ai
readonly ai: AiResource;RAG chat and structured extraction.
enrichment
readonly enrichment: EnrichmentResource;data
readonly data: DataResource;Your application's own document store - see DataResource.
kv
readonly kv: KvResource;Your application's key/value store - see KvResource.
otp
readonly otp: OtpResource;WhatsApp/SMS phone verification.
logs
readonly logs: LogsResource;Feature events into the Console's audit log, filterable to your application.
analytics
readonly analytics: AnalyticsResource;Emit product events and read back this application's aggregated numbers.
sessions
readonly sessions: SessionsResource;This Connect account's own login sessions - account-wide, not scoped to this application. Requires the sessions:manage scope.
integrations
readonly integrations: IntegrationsResource;Composio-hosted third-party connections (Google Calendar, Slack, ...) a workspace has connected.
workspaces
readonly workspaces: WorkspacesResource;Which workspaces the signed-in user belongs to, switching between them, and the team - see WorkspacesResource.
visitor
readonly visitor: VisitorResource;The public chat bubble on your own site - see VisitorResource. The one
resource here whose caller is not one of your users.
platform
readonly platform: PlatformResource;The platform's own legal identity and DPDP grievance channel - see PlatformResource.
push
readonly push: PushResource;Web Push. Connect holds the VAPID keys, the devices and the member's own
preferences; you supply a title, a body and a URL. See PushResource.
tasks
readonly tasks: TasksResource;Background tasks on this application's declared Cloud Tasks queues - see TasksResource.
readonly whatsapp: WhatsAppResource;Your application's own approved WhatsApp templates, to a workspace's customers.
readonly email: EmailResource;Your application's own email, sent from Connect's address for a subscribing workspace - see EmailResource.
Class: ConnectTaskVerificationError
Receiving a Connect task - verify Google's OIDC token, then run. See
TasksResource for the sending half.
Extends
Error
Constructors
Constructor
new ConnectTaskVerificationError(message: string): ConnectTaskVerificationError;Parameters
| Parameter | Type |
|---|---|
message | string |
Returns
ConnectTaskVerificationError
Overrides
Error.constructorProperties
stackTraceLimit
static stackTraceLimit: number;The Error.stackTraceLimit property specifies the number of stack frames
collected by a stack trace (whether generated by new Error().stack or
Error.captureStackTrace(obj)).
The default value is 10 but may be set to any valid JavaScript number. Changes
will affect any stack trace captured after the value has been changed.
If set to a non-number value, or set to a negative number, stack traces will not capture any frames.
Inherited from
Error.stackTraceLimitcause?
optional cause?: unknown;The cause of the error.
Inherited from
Error.causename
name: string;Inherited from
Error.namemessage
message: string;Inherited from
Error.messagestack?
optional stack?: string;Inherited from
Error.stackMethods
captureStackTrace()
Call Signature
static captureStackTrace(targetObject: object, constructorOpt?: Function): void;Creates a .stack property on targetObject, which when accessed returns
a string representing the location in the code at which
Error.captureStackTrace() was called.
const myObject = {};
Error.captureStackTrace(myObject);
myObject.stack; // Similar to `new Error().stack`The first line of the trace will be prefixed with
${myObject.name}: ${myObject.message}.
The optional constructorOpt argument accepts a function. If given, all frames
above constructorOpt, including constructorOpt, will be omitted from the
generated stack trace.
The constructorOpt argument is useful for hiding implementation
details of error generation from the user. For instance:
function a() {
b();
}
function b() {
c();
}
function c() {
// Create an error without stack trace to avoid calculating the stack trace twice.
const { stackTraceLimit } = Error;
Error.stackTraceLimit = 0;
const error = new Error();
Error.stackTraceLimit = stackTraceLimit;
// Capture the stack trace above function b
Error.captureStackTrace(error, b); // Neither function c, nor b is included in the stack trace
throw error;
}
a();Parameters
| Parameter | Type |
|---|---|
targetObject | object |
constructorOpt? | Function |
Returns
void
Inherited from
Error.captureStackTraceCall Signature
static captureStackTrace(targetObject: object, constructorOpt?: Function): void;Create .stack property on a target object
Parameters
| Parameter | Type |
|---|---|
targetObject | object |
constructorOpt? | Function |
Returns
void
Inherited from
Error.captureStackTraceprepareStackTrace()
static prepareStackTrace(err: Error, stackTraces: CallSite[]): any;Parameters
| Parameter | Type |
|---|---|
err | Error |
stackTraces | CallSite[] |
Returns
any
See
https://v8.dev/docs/stack-trace-api#customizing-stack-traces
Inherited from
Error.prepareStackTraceisError()
static isError(value: unknown): value is Error;Check if a value is an instance of Error
Parameters
| Parameter | Type | Description |
|---|---|---|
value | unknown | The value to check |
Returns
value is Error
True if the value is an instance of Error, false otherwise
Inherited from
Error.isErrorInterfaces
Interface: AccessResource
What a signed-in member may do on your application, and the role catalog behind it.
Methods
getUserRoles()
getUserRoles(userId: string, input: {
workspaceId: string;
}): Promise<UserRolesResponse>;Re-check one member's role and granted permissions, live. The same block
identity.getUser() returns - use this when you need only the role axis
and already know who the user is.
Parameters
| Parameter | Type |
|---|---|
userId | string |
input | { workspaceId: string; } |
input.workspaceId | string |
Returns
Promise<UserRolesResponse>
listRoles()
listRoles(): Promise<RolesResponse>;Your application's own permission catalog and role definitions - the definitions getUserRoles resolves against.
Returns
Promise<RolesResponse>
Interface: AiResource
RAG chat and structured extraction against a workspace's data.
asUser on chat behaves as it does on the store resources: pass a
signed-in user's sessionToken and Connect resolves the workspace from it,
ignoring workspaceId. Without it you are the application, naming the
workspace yourself.
Methods
chat()
chat(input: {
workspaceId: string;
messages: ChatMessage[];
asUser?: string;
conversationId?: string;
screens?: unknown;
screenContext?: unknown;
}, options?: {
signal?: AbortSignal;
}): Promise<Response>;RAG-grounded chat. Returns the raw streaming Response rather than a
parsed body - this endpoint answers with server-sent events, and buffering
it here would throw away the only reason to stream. Pipe
response.body onward, or hand it to the AI SDK's own reader.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | { workspaceId: string; messages: ChatMessage[]; asUser?: string; conversationId?: string; screens?: unknown; screenContext?: unknown; } | - |
input.workspaceId | string | - |
input.messages | ChatMessage[] | - |
input.asUser? | string | - |
input.conversationId? | string | Your own id for this conversation, sent with every turn of it. What makes the conversation resumable: Connect upserts against it and conversations.list/get read it back. Persisted only alongside asUser - a conversation belongs to a person, and a server-to-server call acts as nobody. Omit it to keep a turn out of history entirely. |
input.screens? | unknown | Your application's screens, so the assistant answers in your product's words and can link into it - see ConnectScreen. Prose and links only: Connect never uses these to decide what anyone may do. |
input.screenContext? | unknown | Where the user is, as { pathname }. Matched against screens. |
options | { signal?: AbortSignal; } | - |
options.signal? | AbortSignal | - |
Returns
Promise<Response>
listConversations()
listConversations(input: {
asUser: string;
limit?: number;
cursor?: string;
}): Promise<AiConversationList>;The signed-in user's past conversations in your application, most recent first.
asUser is required rather than optional here, unlike on chat: the rows
are scoped to a person, and there is no application-wide view of them to
fall back to. Connect reads the user and the workspace from the token.
Cursor-paginated with no total - the list is ordered by last use, which moves while it is being read.
Parameters
| Parameter | Type |
|---|---|
input | { asUser: string; limit?: number; cursor?: string; } |
input.asUser | string |
input.limit? | number |
input.cursor? | string |
Returns
Promise<AiConversationList>
getConversation()
getConversation(input: {
conversationId: string;
asUser: string;
}): Promise<AiConversationDetail>;One stored transcript, oldest turn first.
turns[].parts comes back exactly as the model produced it - prose,
reasoning and tool calls in order - so it can be handed straight back to a
chat UI. Not this user's, or not your application's, is a 404 either way.
Parameters
| Parameter | Type |
|---|---|
input | { conversationId: string; asUser: string; } |
input.conversationId | string |
input.asUser | string |
Returns
Promise<AiConversationDetail>
deleteConversation()
deleteConversation(input: {
conversationId: string;
asUser: string;
}): Promise<{
deleted: true;
}>;Deletes a conversation and every turn beneath it.
Idempotent - one already gone answers { deleted: true }, so a retry after
a dropped response does not read as a failure.
Parameters
| Parameter | Type |
|---|---|
input | { conversationId: string; asUser: string; } |
input.conversationId | string |
input.asUser | string |
Returns
Promise<{
deleted: true;
}>
generate()
generate(input: AiGenerateInput): Promise<AiGenerateResult>;One generation from your own prompt, against a model Connect allows -
messages, optional tool declarations, an optional JSON schema for the
answer. Needs ai:generate, and the platform must have it enabled.
Not chat. That is Connect's assistant, grounded in a workspace's
Context Index; this is a bare model you prompt yourself, billed to the
workspace you name against its monthly AI budget.
Tools come back to you. Connect never runs one: a requested call
arrives in toolCalls, you run it, and the next generate carries the
model's turn and your result. One model step per call.
Parameters
| Parameter | Type |
|---|---|
input | AiGenerateInput |
Returns
Promise<AiGenerateResult>
analyzeCall()
analyzeCall(input: CallAnalysisInput): Promise<CallAnalysis>;Transcript, summary, intent, sentiment and action items for a call
recording your application stored in the workspace's Drive - e.g. the
fileId connect.telephony.archiveRecording returned. Counted against
the workspace's monthly AI budget like generate; Connect stores nothing.
not_found for a file your application did not upload there;
unavailable (503) when the platform has no call analyzer.
Parameters
| Parameter | Type |
|---|---|
input | CallAnalysisInput |
Returns
Promise<CallAnalysis>
extract()
extract<T>(input: {
schemaId: string;
fileUrl: string;
}): Promise<T>;Extract structured JSON from a file or image URL, against a platform extraction schema referenced by id.
Type Parameters
| Type Parameter | Default type |
|---|---|
T | unknown |
Parameters
| Parameter | Type |
|---|---|
input | { schemaId: string; fileUrl: string; } |
input.schemaId | string |
input.fileUrl | string |
Returns
Promise<T>
draftAssessment()
draftAssessment(input: {
assessmentKey: string;
seed: string;
}): Promise<AssessmentDraft>;Draft every section of a platform-defined assessment from one sentence.
The intended flow is draft first, then let the person correct it section by section: a blank questionnaire is the reason most people abandon one, and editing a strawman is a far smaller ask than authoring from nothing.
Unattributed - assessments run against platform-wide definitions and the
first consumers run before anyone has signed in, so there is no asUser
and no workspace here.
Parameters
| Parameter | Type |
|---|---|
input | { assessmentKey: string; seed: string; } |
input.assessmentKey | string |
input.seed | string |
Returns
Promise<AssessmentDraft>
reviewAssessment()
reviewAssessment(input: {
assessmentKey: string;
answers: AssessmentAnswers;
}): Promise<AssessmentReview>;Critique a completed assessment - the tensions between sections, the riskiest assumption, and a coarse score.
Unknown section keys in answers are ignored rather than rejected, so a
definition changing under a half-finished assessment costs the reader
nothing.
Parameters
| Parameter | Type |
|---|---|
input | { assessmentKey: string; answers: AssessmentAnswers; } |
input.assessmentKey | string |
input.answers | AssessmentAnswers |
Returns
Promise<AssessmentReview>
readiness()
readiness(input: {
domain: string;
asUser?: string;
refresh?: boolean;
}, options?: {
signal?: AbortSignal;
}): Promise<ReadinessAssessmentResult>;How ready the site at a domain is for AI agents.
Probes for an MCP server, an OpenAPI description, llms.txt, structured
data and a markdown twin; evaluates the site's robots.txt against the
assistants that actually fetch pages; checks whether its content survives
without JavaScript and whether a bot wall refuses agents outright. Then
grades what it found and says what to fix first.
Needs an API key scoped to ai:readiness, which is separate from every
other AI scope because it is the widest outbound surface on the API - it
POSTs to candidate MCP endpoints and fetches the home page under two user
agents.
const result = await connect.ai.readiness({ domain: "acme.com" });
if (result.status === "assessed") {
console.log(result.report.grade, result.report.topFix?.fix);
}Four things to know before wiring it in:
- Set your client timeout to at least 90 seconds. A cold assessment runs the whole probe before answering and can take up to a minute; many HTTP clients default to 30s and abandon a request that was about to succeed. Cached reads return in milliseconds and are the normal case - reports are held for a week.
- A week, not ninety days, because a report is a to-do list somebody is
working through. Pass
refreshafter shipping a fix to see it land. - Handle
status: "error"checks as unknown, not as failures. They mean Connect could not measure that check, and they are excluded from the score rather than counted against it. skippedsays whose problem it is.unreachablemeans the site did not answer;provider_unavailablemeans Connect's own assessment service did. Only the second is worth retrying silently, and neither should be shown as a failing grade.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | { domain: string; asUser?: string; refresh?: boolean; } | - |
input.domain | string | A domain or a URL in any form - acme.com, www.acme.com, https://acme.com/pricing. |
input.asUser? | string | - |
input.refresh? | boolean | Re-assess even if Connect has a fresh report cached. For an operator who has just shipped a fix. |
options | { signal?: AbortSignal; } | - |
options.signal? | AbortSignal | - |
Returns
Promise<ReadinessAssessmentResult>
Interface: AnalyticsResource
Your application's own analytics: emit product events, and read back the daily numbers Connect aggregates from them.
There is no way to name an application in either call. The scope is always the one your API key belongs to - that is the endpoint's security model, not an omission.
Methods
series()
series(query: AnalyticsQuery): Promise<AnalyticsResponse>;Daily series for the requested metrics, up to 90 days.
Throws on failure, unlike emit below. A read whose result you are
about to render has to be able to say "I could not load"; silently
returning an empty series would draw a chart of zeroes.
Parameters
| Parameter | Type |
|---|---|
query | AnalyticsQuery |
Returns
Promise<AnalyticsResponse>
emit()
emit(events: EmitEventInput[]): Promise<EmitEventsResponse>;Never throws - the same judgement LogsResource.write makes, for the same
reason: telemetry that takes down the code path it was observing is worse
than a lost event. A failure is reported through delivered.
Up to 50 events per call. Pass sessionToken on an event to attribute it
to the signed-in user; without one it still lands, just unattributed.
Parameters
| Parameter | Type |
|---|---|
events | EmitEventInput[] |
Returns
Promise<EmitEventsResponse>
Interface: AppsResource
The applications a workspace can open - the data behind a launcher grid.
Methods
list()
list(workspaceId: string): Promise<WorkspaceApp[]>;Launchable apps only, so a grid tile is never a dead end. The calling
application is included and flagged isCurrent rather than filtered out.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
Returns
Promise<WorkspaceApp[]>
Interface: AuthorizeUrlInput
Properties
| Property | Type | Description |
|---|---|---|
redirectUri | string | - |
state | string | Fresh and random per request; check it matches on the way back. createState() makes one. |
prompt? | "none" | "create" | "create" opens registration instead of sign-in, so one entry point serves both buttons. "none" is OIDC silent authentication: Connect answers from the visitor's existing session without showing them anything, returning a code if they are signed in and have already authorised this client, and error=login_required/consent_required/interaction_required if not. Use it to sign in a returning visitor with no click - never to start a sign-in, since by definition it will never show them a login screen. |
codeChallenge? | string | Required only if this application enforces PKCE. |
Interface: BillingResource
The plan catalog you sell, and metering usage back against a subscriber's add-on balance.
Methods
listPlans()
listPlans(): Promise<PlansResponse>;Your application's active plans - render live pricing instead of hardcoding it.
features is normalised on the way out: Connect used to send bare keys
and now sends { key, name, description }, and this SDK is versioned
independently of the instance it talks to. Without the coercion a partner
pointed at a Connect that has not upgraded yet renders a feature list of
blanks - which is exactly what happened. See normalizePlanFeatures.
limits is defaulted for the same reason, one version later: an instance
that predates limits sends no such field, and a pricing grid mapping over
plan.limits would throw rather than simply show no limit rows.
Returns
Promise<PlansResponse>
getAddonUsage()
getAddonUsage(workspaceId: string): Promise<AddonUsageResponse>;What is left of every consumable add-on on a workspace's
subscription - the read half of consumeAddon.
Needs a key scoped to addons:read, which is separate from
addons:consume on purpose: a key that only renders a usage meter should
not also be able to spend the balance down.
Returns an empty addons array rather than throwing when there is
nothing to meter, so a caller can render it unconditionally.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
Returns
Promise<AddonUsageResponse>
consumeAddon()
consumeAddon(input: ConsumeAddonInput, options?: {
idempotencyKey?: string;
}): Promise<ConsumeAddonResponse>;Debit a consumable add-on's per-cycle balance on behalf of an workspace's users. Call it at whatever cadence your usage happens.
An Idempotency-Key is always sent, generated per call when you do not
supply one: a balance cannot be un-spent, so a retried consume without a
key double-debits. Pass your own when the natural unit of work is
something you can name (a job id, a message id) and might submit twice.
Parameters
| Parameter | Type |
|---|---|
input | ConsumeAddonInput |
options | { idempotencyKey?: string; } |
options.idempotencyKey? | string |
Returns
Promise<ConsumeAddonResponse>
Interface: CallsResource
Every call that arrived on a channel assigned to your application.
The application is resolved from your API key and is never a parameter, so there is no request that reads another partner's calls.
channelId is the only filter, and that is a budget decision rather than an
oversight: each filter costs a Firestore composite index against a project
already near its ceiling. Filter the rest client-side, or read the
aggregates from analytics.
Methods
list()
list(input?: ListCallsInput): Promise<CallsPage>;Your calls, newest first, across all of your subscribing workspaces.
Page until hasMore is false; do not stop on an empty items. A
page can come back empty with more behind it.
Parameters
| Parameter | Type |
|---|---|
input | ListCallsInput |
Returns
Promise<CallsPage>
get()
get(callId: string): Promise<CallDetail>;One call and every transcript turn, ordered by sequence - which is the
sort to trust, since two turns can share a millisecond.
recordingFileId and transcriptFileId are Drive file ids, readable with
a drive:read key. Both are null until the carrier posts the audio,
which is the normal state for a call that has only just ended - branch on
recordingStatus rather than treating null as a failure.
Throws ConnectError with code: "not_found" for a call that does not
exist or belongs to another application.
Parameters
| Parameter | Type |
|---|---|
callId | string |
Returns
Promise<CallDetail>
getCallingAvailability()
getCallingAvailability(input: {
asUser: string;
}): Promise<CallingAvailability>;Whether the member can call, and if not why - what <ConnectCallButton>
renders from. Throws ConnectError 403 for a member without "Make
calls"; the bridge turns that into status: "no_permission".
Parameters
| Parameter | Type |
|---|---|
input | { asUser: string; } |
input.asUser | string |
Returns
Promise<CallingAvailability>
getRouting()
getRouting(input: {
asUser: string;
}): Promise<any>;Reads the inbound call routing flow and published agents for the workspace.
Parameters
| Parameter | Type |
|---|---|
input | { asUser: string; } |
input.asUser | string |
Returns
Promise<any>
setRouting()
setRouting(input: {
routing: Record<string, unknown>;
asUser: string;
}): Promise<any>;Sets the inbound call routing flow (smart CRM owner routing, AI agent, single number, team, fallback).
Parameters
| Parameter | Type |
|---|---|
input | { routing: Record<string, unknown>; asUser: string; } |
input.routing | Record<string, unknown> |
input.asUser | string |
Returns
Promise<any>
getCallSettings()
getCallSettings(input: {
asUser: string;
}): Promise<CallSettings>;Whether the workspace records its member calls, and whether this member may change that. The same switch as Connect's Calling screen.
Parameters
| Parameter | Type |
|---|---|
input | { asUser: string; } |
input.asUser | string |
Returns
Promise<CallSettings>
setCallRecording()
setCallRecording(input: {
recordingEnabled: boolean;
asUser: string;
}): Promise<CallSettings>;Switches recording on or off for calls started from now. Throws
ConnectError 403 when the member lacks channel:manage; the change is
audited against them. Answers the settings as they now stand.
Parameters
| Parameter | Type |
|---|---|
input | { recordingEnabled: boolean; asUser: string; } |
input.recordingEnabled | boolean |
input.asUser | string |
Returns
Promise<CallSettings>
getCallingReadiness()
getCallingReadiness(input:
| {
asUser: string;
}
| {
workspaceId: string;
}): Promise<CallingReadiness>;Whether the workspace may legally make each kind of call - service or
sales, dialled by a rep or by software - and the steps left on Connect's
calling compliance journey. Send the member to setupUrl to finish them.
Pass asUser to ask for a signed-in member, or workspaceId to ask from
your own server with nobody signed in - a dialer checking before it places
a call. The answer is the same either way, except that canManage is
always false without a member.
Parameters
| Parameter | Type |
|---|---|
input | | { asUser: string; } | { workspaceId: string; } |
Returns
Promise<CallingReadiness>
getBrowserCallingToken()
getBrowserCallingToken(input: {
asUser: string;
}): Promise<BrowserCallingToken>;A carrier token the member's browser signs in to the phone network with.
Parameters
| Parameter | Type |
|---|---|
input | { asUser: string; } |
input.asUser | string |
Returns
Promise<BrowserCallingToken>
startBrowserCall()
startBrowserCall(input: {
phone: string;
asUser: string;
purpose?: ConnectCallPurpose;
}): Promise<StartedBrowserCall>;Approves one call from the member to phone (E.164) and records it.
Throws ConnectError with status 412 when the workspace has no active
number, is not allowed to place calls, or the member is already on one -
the message is written to be shown to them. With a purpose, also when
no number may carry it and the workspace enforces purposes.
Parameters
| Parameter | Type |
|---|---|
input | { phone: string; asUser: string; purpose?: ConnectCallPurpose; } |
input.phone | string |
input.asUser | string |
input.purpose? | ConnectCallPurpose |
Returns
Promise<StartedBrowserCall>
cancelBrowserCall()
cancelBrowserCall(input: {
callId: string;
asUser: string;
}): Promise<void>;Closes a call the browser never managed to place - recorded as
unanswered/cancelled. A no-op once the carrier has it.
Parameters
| Parameter | Type |
|---|---|
input | { callId: string; asUser: string; } |
input.callId | string |
input.asUser | string |
Returns
Promise<void>
Interface: ChannelsResource
The phone numbers Connect has assigned to your application.
Inventory is platform-owned. A number is a regulated asset bought under
Connect's carrier account and reassignable between workspaces, so your
application never provisions, prices or releases one - it claims from a pool
it cannot see, and reads back what it was given. That is why claim takes a
workspace and a kind rather than a number, and why nothing here exposes a
carrier, a provider reference or a monthly rate.
The application is resolved from your API key and is never a parameter, so
no request shape here can reach another partner's channels. A channel
belonging to someone else answers 404, not 403 - confirming it exists
would make these reads an oracle for enumerating their channel ids.
Methods
list()
list(input?: ListChannelsInput): Promise<ChannelsPage>;Every channel assigned to your application, across all of your subscribing workspaces.
Page until hasMore is false.
Parameters
| Parameter | Type |
|---|---|
input | ListChannelsInput |
Returns
Promise<ChannelsPage>
get()
get(channelId: string): Promise<ConnectChannel>;The channel behind an id you already hold - a webhook's channelId, or a
call record's.
Throws ConnectError with code: "not_found" for an id that does not
exist or belongs to another application.
Parameters
| Parameter | Type |
|---|---|
channelId | string |
Returns
Promise<ConnectChannel>
claim()
claim(input: ClaimChannelInput): Promise<ClaimedChannel>;Takes a number out of Connect's inventory and binds it to one of your subscribing workspaces.
You do not pick the number. A workspace may hold exactly one, across
every application it uses, and it is then fixed for the life of the
engagement - there is no re-claim and no swap. It leaves the workspace when
you release it or a Connect operator unassigns it.
Two different 409s, and they need different handling: the workspace
already holds a channel of this kind (fix the call), or the pool is empty
right now (retry - Connect is notified, and this is not permanent).
The claimed channel comes back without routing, because it has none yet.
Call setRouting next, or the number refuses every call that reaches it.
Parameters
| Parameter | Type |
|---|---|
input | ClaimChannelInput |
Returns
Promise<ClaimedChannel>
release()
release(channelId: string, input: {
workspaceId: string;
}): Promise<ConnectChannel>;Gives a number back: its rental closes, routing is cleared and it returns
to Connect's inventory. A number your application handled stops reaching
your telephony webhook. The inverse of claim, under the same scope.
Throws code: "not_found" for another application's channel, or one held
by a different workspace than the one you name.
Parameters
| Parameter | Type |
|---|---|
channelId | string |
input | { workspaceId: string; } |
input.workspaceId | string |
Returns
Promise<ConnectChannel>
setRouting()
setRouting(channelId: string, destination: Record<string, unknown>): Promise<ChannelRoutingRecord>;Points a channel at wherever inbound conversations should go.
destination is yours: Connect stores it verbatim and never reads into
it. It replaces the stored record rather than merging, so send the
whole destination each time; it must serialize to 16 KB or fewer.
Throws code: "conflict" when the channel is suspended or released - use
clearRouting to stop a suspended number answering, which is deliberately
still allowed.
Parameters
| Parameter | Type |
|---|---|
channelId | string |
destination | Record<string, unknown> |
Returns
Promise<ChannelRoutingRecord>
clearRouting()
clearRouting(channelId: string): Promise<ChannelRoutingRecord>;Takes a number out of service without giving it up - the channel stays assigned to you and stays billable, and inbound calls are refused until you write routing again.
This is the only way to say "stop answering": overwriting destination
can only ever point the number somewhere else, and unassigning it is a
Console action that returns the number to Connect's inventory.
Idempotent - clearing already-clear routing answers 200 with
routing: null, so a retry after a lost response is safe.
Parameters
| Parameter | Type |
|---|---|
channelId | string |
Returns
Promise<ChannelRoutingRecord>
Interface: ClaimChannelInput
Properties
| Property | Type | Description |
|---|---|---|
workspaceId | string | - |
kind? | ChannelKind | Defaults to "voice", the only kind Connect stocks inventory for today. |
Interface: ConnectAccountSessionOptions
Account mode: one CosmoOps sign-in shared by every first-party app.
Connect sets an opaque account-session id in a cookie on the parent domain;
the bridge only ever reads it (and clears it on sign-out). Every read
resolves it through identity.getAccountUser, so the answer is still this
application's own role, permissions and subscription.
Properties
| Property | Type | Description |
|---|---|---|
cookieName | string | The cookie Connect sets - must equal Connect's CONNECT_ACCOUNT_COOKIE. |
cookieDomain? | string | null | The Domain Connect set it with (CONNECT_ACCOUNT_COOKIE_DOMAIN), needed to clear it on sign-out - a Set-Cookie without the same Domain clears a different, host-only cookie and leaves the shared one in place. Unset means host-only, which is what local development uses. |
Interface: ConnectClientOptions
Properties
| Property | Type | Description |
|---|---|---|
apiKey | string | The API key plaintext from this application's Connect client section, shown once at creation. One credential does three jobs: it is the Authorization: Bearer value for every endpoint, the OAuth2 client_secret for the /sso/* exchange, and the HMAC key identity.verifyIdToken() checks against. Revoking the key revokes all three. |
clientId? | string | Required only for identity.*. Public - it goes in the authorize URL. |
baseUrl? | string | Defaults to https://connect.cosmoops.com. Point at your own instance if you run one. |
timeoutMs? | number | Per attempt, not per call. Defaults to 20s. |
maxRetries? | number | Extra attempts after the first, for requests that are safe to repeat. Defaults to 2. See request() in http.ts for what qualifies - a bare POST never does. |
fetch? | FetchLike | Swap in your own instrumented fetch. Defaults to the runtime's. |
Interface: ConnectHandlerOptions
Properties
| Property | Type | Description |
|---|---|---|
client | ConnectClient | - |
session | ConnectSessionStore | - |
basePath? | string | Where this handler is mounted, so it can strip the prefix off incoming paths. Must match basePath on <ConnectProvider>. Defaults to /api/connect. |
defaultRedirect? | string | Where /callback sends the user when the request carried no next. Defaults to /. |
stateCookieName? | string | Name of the short-lived cookie holding the OAuth state. Only change it if it collides with something you already set. |
account? | ConnectAccountSessionOptions | Account mode - see ConnectAccountSessionOptions. When set, /session and every route resolve the shared account cookie through getAccountUser, /signin goes to Connect's continue URL, /logout ends the session in every app, /workspaces/switch moves every app, and /callback and One Tap do not exist. session is still read when the account cookie is absent, so a per-application session from before the switch keeps working until it expires. |
sessionCacheTtlMs? | number | How long, in milliseconds, this handler reuses a resolved session for the routes that only need to know whose request it is. Defaults to SESSION_CACHE_TTL_MS (15 seconds); 0 turns the cache off. See createConnectHandler for exactly what it trades. |
Interface: ConnectSessionStore
What the bridge needs from the partner's own session, and what it may change about it.
Deliberately an interface rather than a cookie this SDK sets: the partner already has a session mechanism, and a second one owned by a dependency would be a second source of truth about who is signed in.
Methods
read()
read(request: Request): string | Promise<string | null> | null;The Connect sessionToken for this caller, or null if signed out.
Parameters
| Parameter | Type |
|---|---|
request | Request |
Returns
string | Promise<string | null> | null
write()?
optional write(
request: Request,
response: Response,
sessionToken: string): void | Promise<void>;Persist a freshly exchanged session. Supplying this turns on /signin and
/callback, so the handler runs the whole OAuth round trip instead of
leaving you to write the callback route - see createConnectHandler.
Set cookies on response.
Parameters
| Parameter | Type |
|---|---|
request | Request |
response | Response |
sessionToken | string |
Returns
void | Promise<void>
clear()?
optional clear(request: Request, response: Response): void | Promise<void>;Called on sign-out, before Connect is told. Clear your own session here - set cookies on response.
Parameters
| Parameter | Type |
|---|---|
request | Request |
response | Response |
Returns
void | Promise<void>
migrate()?
optional migrate(
request: Request,
response: Response,
sessionToken: string,
session: UserResponse): void | Promise<void>;Called by GET /session once Connect has accepted the token read
returned, so a store that found it somewhere legacy can re-persist it
where it now belongs - cookieSessionStore uses it to move a token from
the shared connect_session cookie to this application's own name.
session is the resolved answer (its sessionExpiresAt bounds the new
cookie). Optional, and a no-op for any store with nothing to migrate.
Parameters
| Parameter | Type |
|---|---|
request | Request |
response | Response |
sessionToken | string |
session | UserResponse |
Returns
void | Promise<void>
Interface: ConnectTaskHandlerOptions<TPayload>
Extends
Type Parameters
| Type Parameter |
|---|
TPayload |
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
invoker? | string | The invoker service account's email - the one the platform gives you with your API key. Defaults to CONNECT_TASK_INVOKER. | VerifyConnectTaskOptions.invoker |
audience? | string | Your own origin, which Connect sets as the token's audience. Defaults to CONNECT_TASK_AUDIENCE, then to the request URL's origin. | VerifyConnectTaskOptions.audience |
fetch? | FetchLike | Injectable for tests. Defaults to the global fetch. | VerifyConnectTaskOptions.fetch |
now? | () => number | Injectable for tests. Milliseconds since the epoch. | VerifyConnectTaskOptions.now |
handle | (delivery: ConnectTaskDelivery<TPayload>) => void | Promise<void> | Run one task. Return normally and Cloud Tasks counts it done; throw and it is retried with backoff, up to the queue's declared attempts. A failure no retry can fix (the record is gone) should return, not throw. | - |
Interface: ConnectWebhookHandlerOptions
Webhook verification and a ready-made POST route. Server-only for the obvious reason: the signing secret must never reach a browser bundle.
Properties
| Property | Type | Description |
|---|---|---|
secret | string | The endpoint's signing secret, from Console. Keep it in an environment variable. |
on | ConnectWebhookHandlers | - |
onError? | (error: unknown, envelope: ConnectWebhookEnvelope) => void | Promise<void> | Called when a handler throws. Return nothing to have the delivery retried (Connect responds to a 5xx by retrying for about 33 hours); the default logs and rethrows, which produces exactly that. |
Interface: ContactsResource
Who a workspace already knows how to reach.
This is the telephony identity, not a CRM record. A contact is keyed by
primaryPhone - the one fact every inbound call carries - and it is written
automatically: a voice agent creates one on the first call from a new number
and fills fields in as it learns them. source says which kind you are
holding, and it is worth rendering: call means an agent guessed and nobody
has confirmed, manual means a person typed it.
Reference it, do not copy it. If your application keeps its own customer
records, store this contact's id on yours rather than duplicating the
fields. Two writable copies of a person's name with no single owner drift,
and Connect's is the one an agent keeps updating from calls. The id is also
an exact join to anything else keyed by it - Booking.contactId, most
usefully - so "which of my records is this appointment for" becomes a lookup
instead of a phone match.
Writing is contributing, not editing. upsertFromInteraction is the one
write, and it declares where its facts came from (call or extraction):
Connect merges them the way it merges a call's own, so they fill blanks and
never overwrite what a person typed.
Methods
lookupByPhone()
lookupByPhone(input: LookupContactInput): Promise<ConnectContact | null>;"Do we already know this number?" - one point read, cheap enough for a screen pop on every inbound call.
Resolves to null for a number nobody has called from before. That is the
ordinary case for a first-time caller, not an error, which is why it is a
null rather than a thrown ConnectError.
Parameters
| Parameter | Type |
|---|---|
input | LookupContactInput |
Returns
Promise<ConnectContact | null>
get()
get(contactId: string, options: {
workspaceId: string;
asUser?: string;
}): Promise<ConnectContact>;The record behind an id you already hold.
Throws ConnectError with code: "not_found" for an id that does not
exist or belongs to another workspace - deliberately the same answer,
since telling those apart would confirm a contact exists somewhere else.
Parameters
| Parameter | Type |
|---|---|
contactId | string |
options | { workspaceId: string; asUser?: string; } |
options.workspaceId | string |
options.asUser? | string |
Returns
Promise<ConnectContact>
upsertFromInteraction()
upsertFromInteraction(input: UpsertContactFromInteractionInput): Promise<ContactInteractionResult>;Finds the workspace's contact for phone, or creates it, and folds in
what a call or conversation learned. Needs contacts:write; server-side
only (no asUser).
Automated facts fill blanks; they never overwrite. A name somebody
typed into the contact stays, a phone or email is never marked verified,
and arrays are added to. changedFields is what actually landed, so
answer the caller from it - "I've saved your email" when nothing changed
is a promise the record does not keep.
Count the call once. Leave countCall off for every save made while
the interaction is still running and pass true on the one write that
closes it.
The workspace's applications receive contact.created or
contact.updated when something changed - including yours, if you
subscribe to them.
Parameters
| Parameter | Type |
|---|---|
input | UpsertContactFromInteractionInput |
Returns
Promise<ContactInteractionResult>
Interface: CreateBookingInput
Extends
ActingAsOption
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
asUser? | string | The signed-in member's opaque Connect session token. | ActingAsOption.asUser |
workspaceId | string | - | - |
eventTypeId | string | - | - |
startAt | string | Date | The startAt from a findSlots result, passed back unchanged. Re-deriving it from a local wall time is how an integration books an hour that was never offered - the slot list is computed in the business's timezone, which is rarely the one your process is running in. | - |
attendee | BookingAttendee | - | - |
notes? | string | null | - | - |
source? | "api" | "agent-call" | api (the default) for your integration, agent-call for a booking an AI agent you run made on a call - with that call's id as sourceCallId. | - |
sourceCallId? | string | null | - | - |
Interface: CreditResource
Prepaid credit - a rupee balance a workspace buys from Connect for your application (topping up whenever it likes, GST invoiced by Connect), and your application spends per use: calling minutes, SMS.
Unlike a consumable add-on it never resets with the billing cycle, can be topped up mid-cycle, and can be credited back. Every amount is paise, pre-tax.
const { spendable, topUpPath } = await connect.credit.getBalance(workspaceId);
// ...after the call ends:
await connect.credit.consume({ workspaceId, amount: 360, idempotencyKey: call.id, description: "Call (4 min)" });A consume larger than spendable throws ConnectError code
insufficient_credit; send the customer to topUpPath through
useConnect().openConnect(). Needs credit:read / credit:write.
Methods
getBalance()
getBalance(workspaceId: string): Promise<CreditBalance>;Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
Returns
Promise<CreditBalance>
consume()
consume(input: ConsumeCreditInput): Promise<CreditMovement>;Debits credit for one unit of work. idempotencyKey is required and is
enforced by Connect's ledger, so the request is safe to retry - a replay
answers duplicate: true with the original entry.
Parameters
| Parameter | Type |
|---|---|
input | ConsumeCreditInput |
Returns
Promise<CreditMovement>
refund()
refund(input: RefundCreditInput): Promise<CreditMovement>;Credits back part or all of one earlier consume. Never more than it took; idempotent like consume.
Parameters
| Parameter | Type |
|---|---|
input | RefundCreditInput |
Returns
Promise<CreditMovement>
listLedger()
listLedger(workspaceId: string, options?: {
cursor?: string;
limit?: number;
}): Promise<CreditLedgerPage>;Every movement, newest first. Pass the previous page's nextCursor to continue.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
options | { cursor?: string; limit?: number; } |
options.cursor? | string |
options.limit? | number |
Returns
Promise<CreditLedgerPage>
Interface: DataResource
Your application's own document store - a dedicated Firestore database that only Connect can reach.
You hold no Firebase credential and write no security rules. Collections are
declared in the Console; an undeclared name is refused rather than created,
and workspaceId is a path segment rather than a filter, so a query cannot
cross tenants.
A query needs an index for its shape, and there are two ways to have one.
Firestore maintains a single-field index automatically, which covers one
filter, or an orderBy on that same field. Anything wider - two filters, or
a filter ordered by a different field - needs a composite index declared
on your application's Store tab, which Connect then builds in your
database. A query with no matching index is refused up front, naming the
index to declare, rather than failing later with a Firestore index error
pointing at a console you cannot open.
Dates are ISO-8601 strings. The store writes what you send it, and JSON
has no date type, so a Date must be sent as toISOString(). That is not a
compromise: UTC ISO strings sort lexicographically in chronological order,
so ordering and range filters on them behave exactly as they would on a
timestamp - and Connect's own _createdAt/_updatedAt come back in the
same form.
workspaceId is omitted for master data. A collection declared
application-scoped in the Console belongs to no workspace - one copy, owned
by you, read by every tenant. Those are written with your API key only,
never on behalf of a user: a write permission is something a workspace
grants, so honouring one on shared data would give one tenant's admin reach
over every other tenant's reference data.
asUser is how you act on behalf of a signed-in user. Pass the opaque
sessionToken you were given at sign-in and Connect resolves the user and
their workspace from it - checking the collection's declared permission
against what that member actually holds, and ignoring workspaceId
entirely. Without it you are the application itself: your API key's scopes
are the only gate, and the per-user permissions do not apply. The bridge
(createConnectHandler) always passes it, which is what makes the browser
path safe.
Methods
get()
get(input: {
workspaceId?: string;
collection: string;
id: string;
asUser?: string;
}): Promise<StoreDocument>;Reads one document. Throws ConnectError with status 404 when it does not exist.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; id: string; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.id | string |
input.asUser? | string |
Returns
Promise<StoreDocument>
getMany()
getMany(input: {
workspaceId?: string;
collection: string;
ids: string[];
asUser?: string;
}): Promise<StoreGetManyResult>;Reads up to 200 documents of one collection by id, in one request -
the batch form of get.
Reach for it wherever you would loop over get: the contacts on a page of
deals, the plans behind a list of subscriptions. It is gated exactly like
get and spends one request of your rate limit however many ids it names.
const { items } = await connect.data.getMany({
workspaceId,
collection: "contacts",
ids: deals.map((deal) => deal.data.contactId as string),
});
// items[i] belongs to ids[i]; null where that contact no longer exists.Answers positionally, null where a document does not exist - unlike
get, a missing document does not throw.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; ids: string[]; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.ids | string[] |
input.asUser? | string |
Returns
Promise<StoreGetManyResult>
query()
query(input: {
workspaceId?: string;
collection: string;
filters?: StoreQueryFilter[];
orderBy?: StoreOrderBy;
limit?: number;
cursor?: string | null;
asUser?: string;
}): Promise<StoreQueryPage>;Lists a collection, one page at a time.
filters are ANDed. More than one, or one combined with an orderBy on a
different field, needs a composite index declared on the Store tab - see
the class note above.
limit defaults to 50 and caps at 200. Page by passing the previous
page's nextCursor; a null nextCursor is the end of the collection.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; filters?: StoreQueryFilter[]; orderBy?: StoreOrderBy; limit?: number; cursor?: string | null; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.filters? | StoreQueryFilter[] |
input.orderBy? | StoreOrderBy |
input.limit? | number |
input.cursor? | string | null |
input.asUser? | string |
Returns
Promise<StoreQueryPage>
aggregate()
aggregate(input: {
workspaceId?: string;
collection: string;
aggregations: StoreAggregation[];
filters?: StoreQueryFilter[];
asUser?: string;
}): Promise<StoreAggregatePage>;Counts, sums and averages a filtered collection - in Firestore, not by reading the rows back and adding them up.
That distinction is the whole point. An aggregation is billed by the index entries it scans rather than by the documents it would have returned, so a dashboard tile over ten thousand rows costs a handful of reads and stays flat as the collection grows. Paging the rows into your own process to sum them is an unbounded read, and it is what every integration does when there is no aggregate call to reach for.
const { values } = await connect.data.aggregate({
workspaceId,
collection: "deals",
filters: [{ field: "outcome", op: "==", value: "won" }],
aggregations: [
{ op: "count", alias: "count" },
{ op: "sum", field: "value", alias: "total" },
],
});A sum or avg over nothing answers null rather than 0, because
"nothing matched" and "the total is zero" are different facts.
The index it needs is the one its filters need plus the aggregated field - Firestore reads a sum out of the index rather than out of the documents.
There is no orderBy, limit or cursor: the aggregate of an ordered
page is the aggregate of the whole filtered set.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; aggregations: StoreAggregation[]; filters?: StoreQueryFilter[]; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.aggregations | StoreAggregation[] |
input.filters? | StoreQueryFilter[] |
input.asUser? | string |
Returns
Promise<StoreAggregatePage>
scan()
scan(input: {
collection: string;
filters?: StoreQueryFilter[];
orderBy?: StoreOrderBy;
limit?: number;
cursor?: string | null;
}): Promise<StoreScanPage>;Reads one collection across every workspace at once - your own
back-office view of your whole customer base. Each row carries the
workspaceId it came from.
Needs the data:read:all scope, and the collection must be marked
cross-workspace in the Console. There is deliberately no asUser: a
scan is never available on behalf of a signed-in user, because a workspace
admin is an admin of their own workspace and not of your other customers.
Filtering and ordering follow the same index rules as query, and
additionally need collection-group scope. Connect creates those from the
declaration - the single-field ones when you mark a field queryable, and a
group-scoped copy of every composite index the collection declares. A scan
issued in the minutes after that declaration may fail while the index
builds.
Parameters
| Parameter | Type |
|---|---|
input | { collection: string; filters?: StoreQueryFilter[]; orderBy?: StoreOrderBy; limit?: number; cursor?: string | null; } |
input.collection | string |
input.filters? | StoreQueryFilter[] |
input.orderBy? | StoreOrderBy |
input.limit? | number |
input.cursor? | string | null |
Returns
Promise<StoreScanPage>
create()
create(input: {
workspaceId?: string;
collection: string;
data: Record<string, unknown>;
id?: string;
asUser?: string;
}): Promise<StoreDocument>;Creates a document. Pass id to choose it, or omit it and one is
allocated. An id that already exists throws with status 409 - use
replace to overwrite deliberately.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; data: Record<string, unknown>; id?: string; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.data | Record<string, unknown> |
input.id? | string |
input.asUser? | string |
Returns
Promise<StoreDocument>
replace()
replace(input: {
workspaceId?: string;
collection: string;
id: string;
data: Record<string, unknown>;
asUser?: string;
}): Promise<StoreDocument>;Replaces every field, creating the document if it does not exist. _createdAt/_createdBy survive.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; id: string; data: Record<string, unknown>; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.id | string |
input.data | Record<string, unknown> |
input.asUser? | string |
Returns
Promise<StoreDocument>
update()
update(input: {
workspaceId?: string;
collection: string;
id: string;
data: Record<string, unknown>;
asUser?: string;
}): Promise<StoreDocument>;Merges top-level fields into an existing document. Throws with status 404 if it does not exist.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; id: string; data: Record<string, unknown>; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.id | string |
input.data | Record<string, unknown> |
input.asUser? | string |
Returns
Promise<StoreDocument>
delete()
delete(input: {
workspaceId?: string;
collection: string;
id: string;
asUser?: string;
}): Promise<void>;Idempotent - deleting a document that is not there succeeds.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; collection: string; id: string; asUser?: string; } |
input.workspaceId? | string |
input.collection | string |
input.id | string |
input.asUser? | string |
Returns
Promise<void>
commit()
commit(input: {
workspaceId?: string;
operations: StoreCommitOperation[];
asUser?: string;
}): Promise<StoreCommitResult>;Applies a set of writes atomically, across as many of your collections as they name. All of them land, or none.
This is the store's answer to a database transaction, and the shape is deliberate: you never hold one. A transaction handle passed across an HTTP boundary is a server-side lock whose duration your network controls, with Firestore's sixty-second ceiling and lock contention as the failure mode. Instead you read, decide, and commit conditional on nothing having moved - Connect runs the whole set as one short transaction on its side.
await connect.data.commit({
workspaceId,
operations: [
{ op: "create", collection: "ledger", id, data: entry },
{
op: "merge",
collection: "funds",
id: entry.fundId,
data: { balancePaise: increment(entry.amountPaise) },
},
],
});A precondition that no longer holds throws ConnectError with status 409
and writes nothing - see transaction, which re-reads and retries for
you. Note that the example above needs no precondition at all: an
increment is commutative, so concurrent commits both land correctly.
Returns the id each operation wrote, in order - which is how you learn the
id a create without one was given.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; operations: StoreCommitOperation[]; asUser?: string; } |
input.workspaceId? | string |
input.operations | StoreCommitOperation[] |
input.asUser? | string |
Returns
Promise<StoreCommitResult>
transaction()
transaction(build: () => Promise<StoreCommitOperation[]>, options?: {
workspaceId?: string;
asUser?: string;
attempts?: number;
}): Promise<StoreCommitResult>;Read, decide, commit - retried when the world moved underneath you.
The optimistic-concurrency loop, written once. build reads whatever it
needs and returns the operations to apply; if any precondition among them
has gone stale by the time Connect looks, the commit is rejected whole and
build runs again against fresh reads.
await connect.data.transaction(async () => {
const fund = await connect.data.get({ workspaceId, collection: "funds", id });
if (!fund.data.active) throw new Error("That fund is closed.");
return [
{
op: "merge",
collection: "funds",
id,
data: { balancePaise: increment(amount) },
precondition: unchangedSince(fund),
},
];
});Only a 409 is retried. A 403 or a validation error means the operation
was wrong rather than late, and retrying it would turn one clear failure
into attempts identical ones. An error thrown by build itself - the
closed-fund check above - is not retried either: it is a decision, not a
collision.
build must be free of side effects outside the returned operations,
because it can run more than once. That is the price of not holding a
lock, and it is the same contract Firestore's own runTransaction states
for the same reason.
Parameters
| Parameter | Type |
|---|---|
build | () => Promise<StoreCommitOperation[]> |
options | { workspaceId?: string; asUser?: string; attempts?: number; } |
options.workspaceId? | string |
options.asUser? | string |
options.attempts? | number |
Returns
Promise<StoreCommitResult>
Interface: DoNotCallActorInput
Who made a change, in your own terms - kept as your application's claim.
Properties
| Property | Type | Description |
|---|---|---|
id? | string | Your id for whoever made it - a user id, or "backfill". |
name? | string | - |
Interface: DoNotCallResource
A workspace's do-not-call list - numbers that asked the business not to ring them.
Connect enforces it; you record it. Every outbound call to a listed
number - a member's <ConnectCallButton> in any application, Connect's own
dialer, an agent placing a call - is refused with ConnectError code
do_not_call (HTTP 409) before anything reaches the carrier. So when a
person asks your application not to be called, write it here; you do not
need to check the list before dialling, and should not treat a read as
permission to dial.
Every purpose: a person who said "stop ringing me" meant the service call
too. Every change is kept in an append-only ledger (history: true).
Needs do-not-call:read / do-not-call:write.
Methods
get()
get(input: DoNotCallScope & {
phone: string;
history?: boolean;
}): Promise<ConnectDoNotCallLookup>;A number's standing. Never-listed numbers resolve, with doNotCall: false.
Parameters
| Parameter | Type |
|---|---|
input | DoNotCallScope & { phone: string; history?: boolean; } |
Returns
Promise<ConnectDoNotCallLookup>
set()
set(input: DoNotCallScope & {
phone: string;
reason: ConnectDoNotCallReason;
note?: string | null;
actor?: DoNotCallActorInput;
}): Promise<ConnectDoNotCallChange>;Puts a number on the list. Idempotent - a number already listed keeps its
original entry and answers changed: false, so a backfill can re-run.
Parameters
| Parameter | Type |
|---|---|
input | DoNotCallScope & { phone: string; reason: ConnectDoNotCallReason; note?: string | null; actor?: DoNotCallActorInput; } |
Returns
Promise<ConnectDoNotCallChange>
clear()
clear(input: DoNotCallScope & {
phone: string;
note: string;
actor?: DoNotCallActorInput;
}): Promise<ConnectDoNotCallChange>;Lifts a number's entry, so calls to it are placed again. note is
required - it is a claim the person took their request back.
Parameters
| Parameter | Type |
|---|---|
input | DoNotCallScope & { phone: string; note: string; actor?: DoNotCallActorInput; } |
Returns
Promise<ConnectDoNotCallChange>
Interface: DriveResource
Files in a workspace's Drive - upload, and signed URLs to read back.
Methods
upload()
upload(input: UploadInput): Promise<UploadedFile>;Parameters
| Parameter | Type |
|---|---|
input | UploadInput |
Returns
Promise<UploadedFile>
getFileUrl()
getFileUrl(fileId: string, input: {
workspaceId: string;
}): Promise<string>;A signed URL for the original bytes. Short-lived - fetch it when you need it, do not store it.
Parameters
| Parameter | Type |
|---|---|
fileId | string |
input | { workspaceId: string; } |
input.workspaceId | string |
Returns
Promise<string>
getPreviewUrl()
getPreviewUrl(fileId: string, input: {
workspaceId: string;
size?: ThumbnailSize;
}): Promise<string>;A signed URL for the thumbnail. Same lifetime caveat as getFileUrl.
Parameters
| Parameter | Type |
|---|---|
fileId | string |
input | { workspaceId: string; size?: ThumbnailSize; } |
input.workspaceId | string |
input.size? | ThumbnailSize |
Returns
Promise<string>
list()
list(input: {
workspaceId: string;
limit?: number;
}): Promise<DriveFileList>;The files your application uploaded to this workspace's Drive, newest
first - never a member's files, never another application's. limit
defaults to 50 and is capped at 100; truncated says there were more.
Needs drive:read.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId: string; limit?: number; } |
input.workspaceId | string |
input.limit? | number |
Returns
Promise<DriveFileList>
delete()
delete(fileId: string, input: {
workspaceId: string;
}): Promise<void>;Deletes a file your application uploaded. Throws ConnectError 404 for a
file that is not yours, does not exist, or is already deleted - treat that
as done when retrying. Needs drive:write.
Parameters
| Parameter | Type |
|---|---|
fileId | string |
input | { workspaceId: string; } |
input.workspaceId | string |
Returns
Promise<void>
getUsage()
getUsage(input: {
workspaceId: string;
}): Promise<DriveUsage>;How much of the workspace's Drive is used, against its limit. Needs
drive:read and an active subscription to your application.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId: string; } |
input.workspaceId | string |
Returns
Promise<DriveUsage>
Interface: EmailResource
Your application's own email - a support reply, a portal sign-in code - sent through Connect for a workspace that subscribes to you.
You hold no email provider credential. Every message leaves from Connect's
sending address under the display name you choose (the workspace's name by
default), with your own address in replyTo so the answer reaches you.
Server-side only, and deliberately absent from the browser bridge: mail to a customer is your backend's act, and Connect refuses a send that carries a member's session.
Methods
send()
send(input: SendEmailInput, options?: {
idempotencyKey?: string;
}): Promise<SendEmailResponse>;Send one email.
Store the returned messageId. It is the Message-ID the email
actually left under - Connect's mail provider replaces any you propose -
and a customer's reply names it in In-Reply-To. Thread the next reply by
passing it as headers.inReplyTo (and appending it to headers.references).
suppressed is not an error. An address that hard-bounced or marked
Connect's mail as spam is skipped and listed there while the others are
sent; when every recipient is suppressed nothing is sent and the call
still resolves. Retrying changes nothing - show the address as
undeliverable.
Idempotent on idempotencyKey (a fresh UUID when you pass none, which
protects only this call's own retries). Pass your own - the ticket and
reply ids - so a retry after a timeout cannot put a second copy in
somebody's inbox.
Parameters
| Parameter | Type |
|---|---|
input | SendEmailInput |
options | { idempotencyKey?: string; } |
options.idempotencyKey? | string |
Returns
Promise<SendEmailResponse>
Interface: EnrichmentResource
What Connect knows about the company at a domain.
The thing behind "the account filled itself in": a record holding nothing
but priya@acme.com can offer a name, an industry, a size band and an
address instead of an empty form.
Two properties are worth knowing before you wire it in, because they decide how you should render the result:
- Every value is cited, and nothing is recalled. Connect reads the
company's own website and returns the URL each claim came from. A field
the pages never stated comes back
nullrather than as a plausible guess. - It is a suggestion, not a save. Nothing is written to your store, and nothing should be written to it without the person in front of it agreeing - which is also how HubSpot and Zoho present the same feature.
Methods
company()
company(input: {
email?: string;
domain?: string;
asUser?: string;
refresh?: boolean;
}, options?: {
signal?: AbortSignal;
}): Promise<CompanyEnrichmentResult>;Look a company up from a work email address or a domain.
Send whichever you hold; domain wins when both are given. A consumer
address (founder@gmail.com) is answered with
{ status: "skipped", reason: "consumer_domain" } rather than an error -
it names a person, not an employer.
asUser attributes the generation to the signed-in caller's workspace,
resolved from their session token. Without it the call is attributed to
your application.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | { email?: string; domain?: string; asUser?: string; refresh?: boolean; } | - |
input.email? | string | - |
input.domain? | string | - |
input.asUser? | string | - |
input.refresh? | boolean | Re-read the site even if Connect has a fresh profile cached. For an operator pressing "refresh" - not for automatic calls, which should take the cache. |
options | { signal?: AbortSignal; } | - |
options.signal? | AbortSignal | - |
Returns
Promise<CompanyEnrichmentResult>
brand()
brand(input: {
domain: string;
asUser?: string;
refresh?: boolean;
}, options?: {
signal?: AbortSignal;
}): Promise<BrandExtractionResult>;What the company at a domain looks like - logo, palette, typefaces, and the radius, spacing and breakpoint scales its own stylesheets declare.
The sibling of company() above: that one answers what a company does,
this one answers what it looks like. Useful for white-labelling a portal
to a customer's brand, pre-filling a design brief, or showing a real
account card instead of a grey placeholder.
Needs an API key scoped to enrichment:brand - separate from
enrichment:read because it is far more expensive: it crawls
stylesheets, may render the page in a headless browser, and caches image
bytes.
const result = await connect.enrichment.brand({ domain: "acme.com" });
if (result.status === "extracted") {
const primary = result.profile.brandKit.palette.find((c) => c.role === "primary");
}Three things to know before wiring it in:
- Set your client timeout to at least 90 seconds. A cold read runs the whole extraction before answering and can take up to a minute; many HTTP clients default to 30s and abandon a request that was about to succeed. Cached reads return in milliseconds, and cached is the normal case - profiles are held for 90 days, so you pay the cold path roughly once per domain.
- Never store
brandKit.logos[].url. It is a short-lived signed URL minted per read. Render it directly or re-read the profile; persistsourceUrlif you need something stable. - The values were measured, not generated. Check
palette[].source- acss-varentry is the company naming its own colour, afrequencyentry is a ranked count and much weaker.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | { domain: string; asUser?: string; refresh?: boolean; } | - |
input.domain | string | A domain or a URL in any form - acme.com, www.acme.com, https://acme.com/pricing. |
input.asUser? | string | - |
input.refresh? | boolean | Re-extract even if Connect has a fresh profile cached. For an operator pressing "refresh" - not for automatic calls. |
options | { signal?: AbortSignal; } | - |
options.signal? | AbortSignal | - |
Returns
Promise<BrandExtractionResult>
presence()
presence(input: {
domain: string;
asUser?: string;
refresh?: boolean;
}, options?: {
signal?: AbortSignal;
}): Promise<PresenceScanResult>;How visible the business at a domain is online, scored out of 100.
The third sibling of company() and brand(): that one answers what a
business does, the other what it looks like, and this one answers how
findable it is - by a search engine, by a customer, and by the assistants
that increasingly answer questions about a business before anybody visits
its site.
Needs an API key scoped to enrichment:presence - separate from
enrichment:read and enrichment:brand because it is the widest outbound
read of the three: roughly twenty fetches across the site's well-known
paths, and sometimes a headless browser.
const result = await connect.enrichment.presence({ domain: "acme.com" });
if (result.status === "scanned") {
for (const next of result.scan.recommendations) {
console.log(`${next.priority}. ${next.title} - ${next.fix}`);
}
}Four things to know before wiring it in:
- The score is computed, not generated. Every point comes from a check
run against something actually observed, and every check ships with its
weight, what was seen and the URL that settles it - so the total is
reconstructable. A model only orders
recommendationsand writes the prose; it cannot move the number. That is the whole reason this is worth showing a customer: ask a model how good a company's web presence is and it answers 72 with total confidence for a site it never read. socialis claimed, never verified. Those are URLs the site itself printed. Nothing proves the account belongs to that business - render it as "linked from their site". CheckmachineReadabletoo:falsemeans a real profile that search engines have to infer from a footer link, which is a specific, common and easily fixed finding.- A low score is a success, not a skip. Every check failing is the most
useful report this produces. Only
unreachablemeans the site did not answer - andprovider_unavailablemeans Connect did not, which is never something to tell your user about their own site. - Set your client timeout to at least 90 seconds. A cold scan runs the whole probe before answering; many HTTP clients default to 30s and abandon a request that was about to succeed. Cached scans return in milliseconds, and cached is the normal case - results are held for a fortnight, so you pay the cold path roughly once per domain.
Parameters
| Parameter | Type | Description |
|---|---|---|
input | { domain: string; asUser?: string; refresh?: boolean; } | - |
input.domain | string | A domain or a URL in any form - acme.com, www.acme.com, https://acme.com/pricing. |
input.asUser? | string | - |
input.refresh? | boolean | Re-scan even if Connect has a fresh result cached. For an operator who has just shipped a fix - not for automatic calls. |
options | { signal?: AbortSignal; } | - |
options.signal? | AbortSignal | - |
Returns
Promise<PresenceScanResult>
Interface: EntitlementsResource
The two checks worth making on every request: may this workspace be here at all, and what is it paying for.
Methods
getReadiness()
getReadiness(workspaceId: string): Promise<ReadinessResponse>;Every gate Connect owns between a workspace and your application - subscription, KYC/KYB, a chargeable payment method, seat headroom - resolved in one call.
Blocked users are the point: each blocker carries a resolveUrl into the
Connect screen that fixes it, so you can route them somewhere useful
instead of refusing them with nowhere to go.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
Returns
Promise<ReadinessResponse>
getSubscription()
getSubscription(workspaceId: string): Promise<SubscriptionResponse>;The live plan and subscription, including plan.features.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
Returns
Promise<SubscriptionResponse>
hasFeature()
hasFeature(workspaceId: string, feature: string): Promise<boolean>;Whether an active subscription unlocks a plan feature - the check that gates a plan-restricted module.
Deliberately false rather than a throw when there is no subscription at
all: "not entitled" is the same answer either way, and a feature gate
should not need a try/catch around it.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
feature | string |
Returns
Promise<boolean>
getLimit()
getLimit(workspaceId: string, key: string): Promise<PlanLimit>;How much of a metered resource this workspace may have - the numeric
counterpart of hasFeature, for the resources your application counts
itself (sites, projects, pipelines).
This is the effective ceiling, not the plan's own number. Connect has
already folded in whatever the add-ons on the subscription raise it by, so
"Starter includes 1 site, they bought two 5-site packs" arrives here as
{ model: "limited", value: 11 }. You never join the plan against the
add-on list yourself, and a customer who buys another pack needs no change
on your side.
Returns { model: "none" } rather than throwing when there is no active
subscription, when the plan carries no answer for this key, and when the
Connect instance predates limits entirely - the same reasoning
hasFeature gives for answering false: "not entitled" is the same answer
either way, and a limit check should not need a try/catch around it.
Connect does not enforce this. It cannot see what you are counting, so it
hands you the number and your code refuses the next row - canAdd is that
comparison if you already have the count.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
key | string |
Returns
Promise<PlanLimit>
canAdd()
canAdd(
workspaceId: string,
key: string,
currentCount: number,
additional?: number): Promise<boolean>;Whether this workspace may add additional more of a limited resource,
given how many it already has.
You supply the count, because only your application can produce it. This
is getLimit plus planLimitAllows, which exists so the comparison is
not rewritten per call site - see that helper for the three ways of
getting it wrong.
if (!(await connect.entitlements.canAdd(workspaceId, "sites", sites.length))) {
return refuse("Your plan's site limit is reached.");
}Parameters
| Parameter | Type | Default value |
|---|---|---|
workspaceId | string | undefined |
key | string | undefined |
currentCount | number | undefined |
additional | number | 1 |
Returns
Promise<boolean>
reportUsage()
reportUsage(workspaceId: string, counts: Readonly<Record<string, number>>): Promise<ReportUsageResponse>;Tell Connect how many of your limited resources a workspace has right
now, so the customer's Usage page can show "640 of 1,000" against the
ceiling getLimit returns. Needs a key scoped to usage:report.
Each value is the current count, not a change - the number you
already pass to canAdd. Reporting it twice is harmless and a lost
report is healed by the next, which is why no idempotency key is sent.
Keys you leave out keep their last value. A key outside your limit
catalog is refused by name.
Connect enforces nothing on these numbers; your canAdd check is still
the gate. So a failure here should never fail the user's action - report
after it, and catch:
await db.insert(record);
connect.entitlements
.reportUsage(workspaceId, { records: await db.count(workspaceId) })
.catch(() => {}); // the Usage page is a moment stale; nothing else is affectedParameters
| Parameter | Type |
|---|---|
workspaceId | string |
counts | Readonly<Record<string, number>> |
Returns
Promise<ReportUsageResponse>
listSubscribers()
listSubscribers(options?: {
status?: "created" | "authenticated" | "active" | "paused" | "cancelled";
before?: string;
limit?: number;
}): Promise<SubscribersResponse>;One page of the workspaces subscribed to your application, newest first.
The inverse of getSubscription, which asks about one workspace you
already know. This asks who your subscribers are - what a partner
dashboard, an MRR total or a churn report is built on.
Rate-limited far more tightly than the per-workspace read (10/min per
key, against 60): it walks your whole subscriber base and is a reporting
call, not a per-request gate. Use getSubscription in a hot path.
Your application is resolved from the API key, so there is no parameter for it and no way to ask about anyone else's.
Parameters
| Parameter | Type | Description |
|---|---|---|
options | { status?: "created" | "authenticated" | "active" | "paused" | "cancelled"; before?: string; limit?: number; } | - |
options.status? | "created" | "authenticated" | "active" | "paused" | "cancelled" | - |
options.before? | string | Cursor from a previous page's nextBefore. |
options.limit? | number | 1..100, default 25. |
Returns
Promise<SubscribersResponse>
iterateSubscribers()
iterateSubscribers(options?: {
status?: "created" | "authenticated" | "active" | "paused" | "cancelled";
pageSize?: number;
}): AsyncGenerator<ApplicationSubscriber>;Every subscriber, paged through for you.
An async generator rather than an array: a large subscriber base is
exactly where an accidental full materialisation hurts, and for await
reads the same as a loop over one page.
Parameters
| Parameter | Type |
|---|---|
options | { status?: "created" | "authenticated" | "active" | "paused" | "cancelled"; pageSize?: number; } |
options.status? | "created" | "authenticated" | "active" | "paused" | "cancelled" |
options.pageSize? | number |
Returns
AsyncGenerator<ApplicationSubscriber>
Interface: ExchangeCodeInput
Properties
| Property | Type | Description |
|---|---|---|
code | string | - |
redirectUri | string | Must be byte-identical to the one sent to authorizeUrl(). |
codeVerifier? | string | - |
Interface: ExecuteIntegrationActionInput
Properties
| Property | Type | Description |
|---|---|---|
toolkitSlug | string | - |
actionSlug | string | - |
params? | Record<string, unknown> | Passed through to Composio verbatim - its shape is defined by the toolkit's own action schema, which Connect does not model. |
Interface: FindSlotsInput
Extends
ActingAsOption
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
asUser? | string | The signed-in member's opaque Connect session token. | ActingAsOption.asUser |
workspaceId | string | - | - |
eventTypeId | string | - | - |
from | string | Date | ISO-8601 instant, or a Date. | - |
to | string | Date | - | - |
excludeBookingId? | string | A booking to treat as if it were not there - set this when offering times to move an existing appointment to. Without it the appointment counts as blocking its own replacement, so the adjustment people actually make - same day, half an hour later - comes back with nothing available. | - |
Interface: IdentityResource
"Login with Connect" - the OAuth2 handshake, and the session that outlives it.
The whole flow is five calls: authorizeUrl to send them out,
exchangeCode when they come back, verifyIdToken once, then getUser on
each later request and logout at the end.
Methods
authorizeUrl()
authorizeUrl(input: AuthorizeUrlInput): string;Step 1 - the URL to send the user's browser to.
Parameters
| Parameter | Type |
|---|---|
input | AuthorizeUrlInput |
Returns
string
oneTapEndpoints()
oneTapEndpoints():
| {
hintUrl: string;
authorizeUrl: string;
}
| null;The two Connect endpoints One Tap talks to from the browser, or null
when this client has no clientId configured and the flow cannot run.
Handed out by the bridge rather than built in the page, so One Tap needs no
client-side configuration at all - a ./next application sets
CONNECT_CLIENT_ID server-side and nothing else. Both URLs carry only the
public client_id; neither is a credential.
Returning null rather than throwing (as authorizeUrl does) because this
is read on the /session route, which must keep answering for an
application that has no OAuth client configured at all.
Returns
| {
hintUrl: string;
authorizeUrl: string;
}
| null
publicClientId()
publicClientId(): string | null;This client's public client_id, or null when none is configured.
The bridge hands it to the browser on /session so openConnect can name
the application on every hand-off without the host passing clientId to
<ConnectProvider> too - Connect only honours a return_to it can match
against a client's registered redirect origins. Public by definition:
it already rides on every authorize URL.
Returns
string | null
exchangeCode()
exchangeCode(input: ExchangeCodeInput): Promise<TokenResponse>;Step 2 - trade the one-time code for a session. Server-side only.
Parameters
| Parameter | Type |
|---|---|
input | ExchangeCodeInput |
Returns
Promise<TokenResponse>
verifyIdToken()
verifyIdToken(idToken: string): Promise<IdTokenClaims>;Step 3 - verify the id_token once, then discard it.
HS256 over header.payload, keyed with your API key, plus the exp and
aud checks. There is no JWKS to fetch: Connect stores only your key's
sha256 hash, so the token is signed with the shared secret rather than an
asymmetric key. Signature comparison goes through WebCrypto's own
verify, which is why this is async - hand-rolling the compare is the
step integrations most often get wrong.
Parameters
| Parameter | Type |
|---|---|
idToken | string |
Returns
Promise<IdTokenClaims>
getUser()
getUser(sessionToken: string): Promise<UserResponse>;Step 4 - who is signed in, right now. Same blocks as exchangeCode,
re-resolved live, so a role change or a lapsed subscription shows up here
without another sign-in.
Any ConnectError with isSessionExpired means send them back through
authorizeUrl().
Parameters
| Parameter | Type |
|---|---|
sessionToken | string |
Returns
Promise<UserResponse>
getAccountUser()
getAccountUser(accountSession: string): Promise<UserResponse>;getUser, for a CosmoOps account session instead of this
application's own session token - first-party applications only.
The account session is the opaque id in the parent-domain cookie Connect
sets at sign-in (CONNECT_ACCOUNT_COOKIE). One id is shared by every
CosmoOps app, but the answer is per application: the client credentials
say which app is asking, so role, permissions and subscription are
this application's, and every entry check getUser implies still runs.
Same wire shape and the same mapping as getUser, and the same failure:
an unknown, revoked or expired id is invalid_token, so
isSessionExpired is still the "send them to sign in" test. A client
that is not marked first-party in Console is refused invalid_client.
Parameters
| Parameter | Type |
|---|---|
accountSession | string |
Returns
Promise<UserResponse>
logoutAccount()
logoutAccount(accountSession: string): Promise<void>;Ends the account session - in every CosmoOps app and in Connect itself,
because it revokes the one session record they all point at. Clearing the
cookie is the caller's job (the ./next bridge does it on /logout).
Always succeeds for an unknown or already-revoked id, like logout.
Parameters
| Parameter | Type |
|---|---|
accountSession | string |
Returns
Promise<void>
switchAccountWorkspace()
switchAccountWorkspace(accountSession: string, workspaceId: string): Promise<UserResponse>;Moves the account session to another workspace, so every CosmoOps app
follows - the active workspace lives on the shared session record rather
than in a per-application token. Answers the re-resolved session, in
getAccountUser's shape. forbidden when the member does not belong to
workspaceId.
The account id does not rotate, so there is no cookie to rewrite.
Parameters
| Parameter | Type |
|---|---|
accountSession | string |
workspaceId | string |
Returns
Promise<UserResponse>
accountContinueUrl()
accountContinueUrl(input: {
returnTo: string;
}): string;Where to send a browser with no account session.
Connect's continue endpoint signs them in if needed, makes sure the
account cookie is set, and redirects to returnTo - no OAuth code
exchange and no consent screen. returnTo must be absolute, and its
origin must be one of this client's registered redirect URI origins,
or Connect answers 400.
Parameters
| Parameter | Type |
|---|---|
input | { returnTo: string; } |
input.returnTo | string |
Returns
string
logout()
logout(sessionToken: string, options?: {
scope?: LogoutScope;
}): Promise<void>;Step 5 - end the sign-in immediately rather than leaving it valid for the
rest of its ~15-day life. Discard your own copy of sessionToken too;
this does not do that for you.
scope defaults to "device": this sign-in, every other application signed
into from the same browser, and Connect's own session on that one device.
"all" ends every session on every device - an explicit "sign out
everywhere" action only.
Always succeeds. An unknown or already-revoked token is an indistinguishable no-op, so there is nothing to branch on.
Parameters
| Parameter | Type |
|---|---|
sessionToken | string |
options | { scope?: LogoutScope; } |
options.scope? | LogoutScope |
Returns
Promise<void>
Interface: InitiateIntegrationConnectionInput
Properties
| Property | Type | Description |
|---|---|---|
toolkitSlug | string | - |
returnUrl | string | - |
userId | string | The signed-in member starting the connection, recorded by Connect as connectedByUserId. Required, and deliberately not defaulted: the connection is shared by the whole workspace, so "who turned this on" is the only attribution anyone gets - and a call that cannot name a person is one where nobody is present to finish the OAuth screen either. |
connectionData? | Record<string, string> | Pre-fills the toolkit's own connection fields on the provider screen - { extension: "in" } puts a Zoho connection on the India data centre, which otherwise defaults to zoho.com. Which keys a toolkit reads is Composio's catalogue; Connect passes them through. |
Interface: IntegrationsResource
Composio-hosted third-party connections (Google Calendar, Slack, Twitter/X, ...) a workspace has connected. Connect proxies every call here - your application never sees or holds the underlying OAuth token, which stays with Composio.
Methods
list()
list(workspaceId: string): Promise<IntegrationToolkit[]>;Enabled toolkits and this workspace's connection status for each.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
Returns
Promise<IntegrationToolkit[]>
initiateConnect()
initiateConnect(workspaceId: string, input: InitiateIntegrationConnectionInput): Promise<InitiateIntegrationConnectionResponse>;Starts a Composio-hosted connection for one toolkit. The returned
redirectUrl is where the browser completes the provider's OAuth
screen, then Composio bounces it back to returnUrl.
The connection is not active when this returns. Treat the toolkit
as pending until a later list() call reports active - only
Connect's own webhook from Composio confirms that, never the redirect
itself. Reconnecting a disconnected/failed toolkit calls this same
method - there is no separate reconnect.
Meant to be called on behalf of a signed-in workspace admin (this is what
the bridge's fixed POST /integrations/connect route does), not as a bare
background action with no admin present to complete the OAuth screen. That
is why userId is part of the input rather than inferred - see
InitiateIntegrationConnectionInput.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
input | InitiateIntegrationConnectionInput |
Returns
Promise<InitiateIntegrationConnectionResponse>
disconnect()
disconnect(workspaceId: string, toolkitSlug: string): Promise<DisconnectIntegrationResponse>;Idempotent - disconnecting a toolkit that was never connected, or is already disconnected, is a success.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
toolkitSlug | string |
Returns
Promise<DisconnectIntegrationResponse>
request()
request(workspaceId: string, input: ProxyIntegrationRequestInput): Promise<ProxyIntegrationRequestResponse>;One raw request to a connected provider's own API, for an endpoint the toolkit has no action for (Zoho Books ships no "create sales order").
Resolves whenever Connect relayed the request, whatever the provider
answered: read status and data for that. A provider-side 4xx is a
business answer ("that GSTIN is invalid") rather than a transport
failure, so it is not thrown as one.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
input | ProxyIntegrationRequestInput |
Returns
Promise<ProxyIntegrationRequestResponse>
execute()
execute(workspaceId: string, input: ExecuteIntegrationActionInput): Promise<ExecuteIntegrationActionResponse>;Calls a Composio action against this workspace's connected account for the named toolkit.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
input | ExecuteIntegrationActionInput |
Returns
Promise<ExecuteIntegrationActionResponse>
Interface: KvResource
Your application's key/value store - small values addressed by namespace and key, per workspace.
Use it for what a document store is bad at: a feature flag, a cached
computation, a counter, a draft nobody needs to query. Anything you want to
find rather than fetch belongs in data.
Namespaces are declared in the Console exactly like collections. Keys are
[A-Za-z0-9_-] up to 64 characters, and values are JSON up to 128 KB.
asUser behaves exactly as it does on DataResource - pass a signed-in
user's sessionToken and Connect derives the user and workspace from it,
checking the namespace's declared permission against their live roles.
Methods
get()
get(input: {
workspaceId?: string;
namespace: string;
key: string;
asUser?: string;
}): Promise<KvEntry>;Reads one key. Throws ConnectError with status 404 when the key is
absent or expired - expiry is evaluated on read, so there is no window
where a stale value is still served.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; namespace: string; key: string; asUser?: string; } |
input.workspaceId? | string |
input.namespace | string |
input.key | string |
input.asUser? | string |
Returns
Promise<KvEntry>
list()
list(input: {
workspaceId?: string;
namespace: string;
limit?: number;
cursor?: string | null;
asUser?: string;
}): Promise<KvPage>;Pages a namespace in key order.
Expired entries are filtered out after paging, so a page can come back
shorter than limit while still having a nextCursor - treat
nextCursor as the only signal that more keys exist, never an empty page.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; namespace: string; limit?: number; cursor?: string | null; asUser?: string; } |
input.workspaceId? | string |
input.namespace | string |
input.limit? | number |
input.cursor? | string | null |
input.asUser? | string |
Returns
Promise<KvPage>
set()
set(input: {
workspaceId?: string;
namespace: string;
key: string;
value: unknown;
ttlSeconds?: number;
asUser?: string;
}): Promise<KvEntry>;Sets a value, replacing whatever was there. ttlSeconds expires it; omit for no expiry.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; namespace: string; key: string; value: unknown; ttlSeconds?: number; asUser?: string; } |
input.workspaceId? | string |
input.namespace | string |
input.key | string |
input.value | unknown |
input.ttlSeconds? | number |
input.asUser? | string |
Returns
Promise<KvEntry>
delete()
delete(input: {
workspaceId?: string;
namespace: string;
key: string;
asUser?: string;
}): Promise<void>;Idempotent - deleting an absent key succeeds.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId?: string; namespace: string; key: string; asUser?: string; } |
input.workspaceId? | string |
input.namespace | string |
input.key | string |
input.asUser? | string |
Returns
Promise<void>
Interface: ListBookingsInput
Extends
ActingAsOption
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
asUser? | string | The signed-in member's opaque Connect session token. | ActingAsOption.asUser |
workspaceId | string | - | - |
status? | BookingStatus | - | - |
hostMemberId? | string | A ConnectWorkspaceMember.memberId - only bookings that committed them. Not a userId. Pass one and the filter matches nothing, which comes back as an empty diary rather than an error. | - |
from? | string | Date | - | - |
to? | string | Date | - | - |
order? | "asc" | "desc" | asc for a forward diary, desc (the default) for a history. | - |
cursor? | string | - | - |
limit? | number | - | - |
Interface: ListCallsInput
Properties
| Property | Type | Description |
|---|---|---|
channelId? | string | Narrow to one channel. A channel belonging to another application simply matches nothing. |
placedByApplication? | boolean | true for the member calls your UI placed (<ConnectCallButton>) instead of the calls on your channels. Those leave from the workspace's own number, so the default list never contains them - this is the list to reconcile a timeline from when a webhook went to another deployment. |
workspaceId? | string | With placedByApplication, one workspace's calls. |
cursor? | string | From a previous page's nextCursor. |
limit? | number | 1-100, default 50. |
Interface: ListChannelsInput
Properties
| Property | Type | Description |
|---|---|---|
workspaceId? | string | Narrow to one workspace. Omit to list every channel assigned to your application. |
cursor? | string | From a previous page's nextCursor. |
limit? | number | 1-100, default 50. |
Interface: LogsResource
Push feature events into the Console's audit log, filterable to your application.
Methods
write()
write(input: WriteLogInput): Promise<WriteLogResponse>;Never throws. A logging call that takes down the code path it was observing is worse than a lost line, so a failure here is swallowed and reported through the return value.
Pass sessionToken to attribute the event to the end user it belongs to.
Both scopes on the stored entry come from something verified - the application
from your API key, the user from that token, which Connect checks was
minted for your application before attributing anything. attributed in the
result says whether it resolved; a false there still means the line was
stored, just without a user on it.
Parameters
| Parameter | Type |
|---|---|
input | WriteLogInput |
Returns
Promise<WriteLogResponse>
Interface: LookupContactInput
Properties
| Property | Type | Description |
|---|---|---|
workspaceId | string | - |
phone | string | Any reasonable spelling. +919000000001, 919000000001 and +91 90000 00001 all resolve to the same contact - the number is normalized to E.164 before the lookup, because comparing unnormalized is how a contact that exists reads as absent. |
asUser? | string | See ActingAsOption on the scheduling resource - same contract. |
Interface: OtpResource
WhatsApp/SMS phone verification.
Methods
send()
send(input: SendOtpInput, options?: {
idempotencyKey?: string;
}): Promise<SendOtpResponse>;Send a code. As with billing.consumeAddon, an Idempotency-Key always
goes along - a retried send would otherwise deliver a second message and
invalidate the first code the user is looking at.
Parameters
| Parameter | Type |
|---|---|
input | SendOtpInput |
options | { idempotencyKey?: string; } |
options.idempotencyKey? | string |
Returns
Promise<SendOtpResponse>
verify()
verify(input: VerifyOtpInput): Promise<VerifyOtpResponse>;Check a 6-digit code against a verificationId.
Parameters
| Parameter | Type |
|---|---|
input | VerifyOtpInput |
Returns
Promise<VerifyOtpResponse>
Interface: PlatformResource
Facts about the platform itself, rather than about a workspace or a user.
One method today, and the resource exists anyway rather than hanging
getCompanyProfile off identity or workspaces: neither of those is
what it is about. This answers "who operates the thing my application runs
on", which is a different subject from every other resource here, and a
misfiled method is harder to find than an obvious one-method namespace.
Methods
getCompanyProfile()
getCompanyProfile(): Promise<CompanyProfile>;The operating entity's legal identity and its DPDP grievance channel.
Takes no workspaceId and takes no asUser - the answer is the same for
every tenant and every caller, because it is a fact about the platform.
Returns the inner companyProfile rather than the envelope, matching
apps.list() and billing.listPlans(): the wrapper is a wire detail, and
every caller would destructure it on the next line.
Do not copy the result into your own configuration. Cache it if you
render it on a hot path - Connect already sends
s-maxage=3600, stale-while-revalidate=86400 - but the reason this is an
endpoint at all is that a grievance officer copied into .env goes stale
without anyone finding out.
Returns
Promise<CompanyProfile>
Interface: ProxyIntegrationRequestInput
Properties
| Property | Type | Description |
|---|---|---|
toolkitSlug | string | - |
method | IntegrationProxyMethod | - |
endpoint | string | A path relative to the provider's API base - /salesorders, not a URL. Connect refuses anything with a scheme, a host, a ? or a dot segment, because whoever picks the host picks where the workspace's credential goes. Parameters belong in query. |
query? | Record<string, string> | - |
body? | unknown | - |
onBehalfOfUserId? | string | null | Whose connection to act through - omitted or null for the workspace's shared one. |
Interface: RescheduleBookingInput
Extends
ActingAsOption
Properties
| Property | Type | Description | Inherited from |
|---|---|---|---|
asUser? | string | The signed-in member's opaque Connect session token. | ActingAsOption.asUser |
workspaceId | string | - | - |
bookingId | string | - | - |
startAt | string | Date | The new time, from a findSlots result, passed back unchanged. | - |
notes? | string | null | Replaces the original note when given; it carries over otherwise. | - |
Interface: SchedulingResource
A subscribing workspace's own calendar.
There is one calendar, not one per application. A booking made here
appears on the workspace's own screen immediately, and a slot their
receptionist just took is gone from your next findSlots. That is the
reason to read it through Connect rather than keeping a copy: two calendars
double-book, and nothing reconciles them afterwards.
Every method needs a live subscription to your application in the workspace you name. The key says which application is calling; the subscription is what makes you welcome in a given tenant.
Methods
listEventTypes()
listEventTypes(workspaceId: string, options?: ActingAsOption): Promise<BookableEventType[]>;What this workspace offers as bookable. Active appointment types only.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
options | ActingAsOption |
Returns
Promise<BookableEventType[]>
listSchedules()
listSchedules(workspaceId: string, options?: ActingAsOption): Promise<AvailabilitySchedule[]>;The working-hours templates an appointment type can point at.
Read this before offering to create one - a workspace that already has hours set does not need a second "Working hours" from your integration.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
options | ActingAsOption |
Returns
Promise<AvailabilitySchedule[]>
createSchedule()
createSchedule(input: CreateScheduleInput): Promise<AvailabilitySchedule>;Gives a workspace its first working hours.
Narrower than what the workspace's own Calendar settings can do - no date
overrides, and never owned by one person (ownerMemberId stays null
server-side). Anything beyond that is a Console edit, not a second write
path for your integration to maintain.
Parameters
| Parameter | Type |
|---|---|
input | CreateScheduleInput |
Returns
Promise<AvailabilitySchedule>
createEventType()
createEventType(input: CreateEventTypeInput): Promise<ManageableEventType>;The first thing a workspace can be booked for.
Needs a schedule - create or pick one with listSchedules/createSchedule
first. The slug for a future public booking link is derived from name
server-side; you cannot choose one.
Answers the richer manageable shape, not BookableEventType - you just
created this row and are about to manage it, not book against it.
Parameters
| Parameter | Type |
|---|---|
input | CreateEventTypeInput |
Returns
Promise<ManageableEventType>
listManageableEventTypes()
listManageableEventTypes(workspaceId: string, options?: ActingAsOption): Promise<ManageableEventType[]>;The richer read createEventType/updateEventType's edit mode needs -
everything listEventTypes's booking catalogue deliberately withholds,
because this is the opposite audience: a member managing their own
calendar, not a widget rendering someone else's. Includes inactive rows,
since an edit screen is exactly where one gets reactivated.
Parameters
| Parameter | Type |
|---|---|
workspaceId | string |
options | ActingAsOption |
Returns
Promise<ManageableEventType[]>
updateSchedule()
updateSchedule(input: UpdateScheduleInput): Promise<AvailabilitySchedule>;Edits an existing availability schedule.
Narrower than what the workspace's own Calendar settings can do, the
same way createSchedule is: ownerMemberId and date overrides stay
untouched, since this surface never sets either.
Parameters
| Parameter | Type |
|---|---|
input | UpdateScheduleInput |
Returns
Promise<AvailabilitySchedule>
updateEventType()
updateEventType(input: UpdateEventTypeInput): Promise<ManageableEventType>;Edits an existing appointment type.
hostMemberId omitted leaves the host untouched, including a group
host this surface cannot represent - so an edit that says nothing about
the host can never accidentally overwrite a team assignment made in
Connect.
Parameters
| Parameter | Type |
|---|---|
input | UpdateEventTypeInput |
Returns
Promise<ManageableEventType>
findSlots()
findSlots(input: FindSlotsInput): Promise<BookingSlots>;Free times for one appointment type over one window.
Advisory by construction. Between this answer and createBooking,
anyone else on that calendar can take any of these - a member, a voice
agent mid-call, someone on the workspace's public booking link. That is
why createBooking can answer 409, and why that is an ordinary outcome
rather than a fault.
The window is clamped silently to now and to the appointment type's own booking horizon, so asking for a year of a sixty-day type returns sixty days rather than an error.
Parameters
| Parameter | Type |
|---|---|
input | FindSlotsInput |
Returns
Promise<BookingSlots>
getBooking()
getBooking(input: ActingAsOption & {
workspaceId: string;
bookingId: string;
}): Promise<Booking>;One appointment, by the id you stored.
This is the read to make when you kept a booking id on a record of your
own - a meeting on a deal, an appointment on a case. listBookings
cannot serve it: that is a window over startAt, and the startAt you
cached is exactly what is wrong once someone has moved the appointment on
the workspace's own calendar, so the window you build from it comes back
without the row.
A retired booking still resolves. status: "rescheduled" answers with
rescheduledToId, so an id stored months ago leads to the appointment
that exists today; cancelled answers with its reason. not_found means
this workspace never had that booking.
Parameters
| Parameter | Type |
|---|---|
input | ActingAsOption & { workspaceId: string; bookingId: string; } |
Returns
Promise<Booking>
listBookings()
listBookings(input: ListBookingsInput): Promise<ListBookingsPage>;One page of the workspace's diary.
Parameters
| Parameter | Type |
|---|---|
input | ListBookingsInput |
Returns
Promise<ListBookingsPage>
createBooking()
createBooking(input: CreateBookingInput): Promise<Booking>;Takes one of the times findSlots offered.
Holds and confirms in one call - you already have the attendee, so there
is no half-finished state for your integration to remember. The slot is
re-checked inside a transaction, so a time taken since you read it throws
a ConnectError with code: "conflict" rather than double-booking.
Parameters
| Parameter | Type |
|---|---|
input | CreateBookingInput |
Returns
Promise<Booking>
holdSlot()
holdSlot(input: HoldSlotInput): Promise<Booking>;Takes a slot now, for a booking whose attendee you will learn afterwards -
a voice agent reads three times aloud, the caller picks one, then gives a
name. If you already have the attendee, createBooking does both at once.
The answer is a real booking with status: "hold": from this moment the
slot is gone from everyone else's findSlots, including the workspace's
own public booking page. A time taken since you read it throws a
ConnectError with code: "conflict".
It lapses at holdExpiresAt and frees the slot the moment it does, so an
abandoned call holds nothing. Only your application can confirm it, with
confirmHold. Say nothing to the caller about being booked until that
has returned.
Parameters
| Parameter | Type |
|---|---|
input | HoldSlotInput |
Returns
Promise<Booking>
confirmHold()
confirmHold(input: ConfirmHoldInput): Promise<Booking>;Turns a hold from holdSlot into a booking, and tells the attendee.
Idempotent: confirming a booking that is already confirmed returns it
unchanged and notifies nobody, so retrying after a dropped response is
safe. A hold that lapsed throws conflict - hold a slot again. An id your
application did not hold throws not_found.
Parameters
| Parameter | Type |
|---|---|
input | ConfirmHoldInput |
Returns
Promise<Booking>
rescheduleBooking()
rescheduleBooking(input: RescheduleBookingInput): Promise<RescheduledBooking>;Moves an appointment to a different time.
The original is retired, not edited. It stays on the diary as
rescheduled with rescheduledToId pointing at the replacement, and both
come back here - so a booking id you cached still resolves, and still says
where the appointment went.
A refused move changes nothing: the new slot is claimed before the old one
is released, so a conflict leaves the customer with the appointment they
already had. Re-read the slots (with excludeBookingId) and offer another.
Parameters
| Parameter | Type |
|---|---|
input | RescheduleBookingInput |
Returns
Promise<RescheduledBooking>
cancelBooking()
cancelBooking(input: ActingAsOption & {
workspaceId: string;
bookingId: string;
reason?: string | null;
}): Promise<Booking>;Frees the slot and records why.
Not a deletion: the booking stays on the diary as cancelled with the
reason, which is what makes a late cancellation something anyone can look
up afterwards.
Parameters
| Parameter | Type |
|---|---|
input | ActingAsOption & { workspaceId: string; bookingId: string; reason?: string | null; } |
Returns
Promise<Booking>
recordAttendance()
recordAttendance(input: ActingAsOption & {
workspaceId: string;
bookingId: string;
attended: boolean;
}): Promise<Booking>;Records whether the attendee turned up: attended: false marks the
booking no_show, true takes a mistaken no-show back to confirmed.
There is no "held" to set - a confirmed booking whose time has passed
already is one. Answers 409 for a booking that is not confirmed or
no_show, or has not started yet. The attendee is not notified.
Parameters
| Parameter | Type |
|---|---|
input | ActingAsOption & { workspaceId: string; bookingId: string; attended: boolean; } |
Returns
Promise<Booking>
Interface: SessionsResource
This Connect account's own login sessions - device, approximate location, timestamps - the same list Connect's own account settings page shows the user themselves.
Account-wide, not scoped to this application. A Connect session is one
browser's login to Connect, and it fans out to every partner application's own
sessionToken - Connect has no per-application session concept to filter by.
list() returns every session on the account, including ones that
originated from a completely different, unrelated application. Requires the
sessions:manage scope on your API key - unlike identity's base sso
capability, this is never granted to a zero-scope legacy key by default.
Authenticated like identity.* - client_id+client_secret, never a
Bearer key - because every call here is really "resolve identity from a
sessionToken", the same shape identity.getUser/identity.logout are.
Methods
list()
list(sessionToken: string): Promise<ConnectUserSession[]>;Every active session on the account behind sessionToken. isCurrent marks the one it was minted from.
Parameters
| Parameter | Type |
|---|---|
sessionToken | string |
Returns
Promise<ConnectUserSession[]>
revoke()
revoke(sessionToken: string, sessionId: string): Promise<void>;Ends one specific session by id - not necessarily the one sessionToken
itself was minted from. Cascades to every partner-application sign-in that
originated from that same browser, same as Connect's own "sign out this
device".
Parameters
| Parameter | Type |
|---|---|
sessionToken | string |
sessionId | string |
Returns
Promise<void>
revokeOthers()
revokeOthers(sessionToken: string): Promise<RevokeOtherSessionsResponse>;"Sign out everywhere else" - keeps the session sessionToken was minted from, ends every other one.
Parameters
| Parameter | Type |
|---|---|
sessionToken | string |
Returns
Promise<RevokeOtherSessionsResponse>
Interface: TasksResource
Background work, delivered back to your own endpoint by Google Cloud Tasks.
Declare a queue in Console - its URL, its rate, its retries - then enqueue
here. Cloud Tasks POSTs { workspaceId, queue, payload } to the queue's
URL with a Google-signed OIDC token; verify it with verifyConnectTask (or
let createConnectTaskHandler do it) before trusting a byte of the body.
Deliveries are at least once. Make each step safe to run twice, and
give each enqueue a name derived from the step so a retried enqueue is a
duplicate rather than a second task.
Methods
enqueue()
enqueue(input: EnqueueTaskInput): Promise<EnqueueTaskResponse>;Parameters
| Parameter | Type |
|---|---|
input | EnqueueTaskInput |
Returns
Promise<EnqueueTaskResponse>
Interface: TelephonyResource
Calls on the numbers your application handles, for an application that answers its own calls.
Connect stays the carrier. It holds the carrier account, the number
inventory and the rental; your application owns everything that happens on
a call. Every carrier callback for a number you handle reaches your
telephony webhook as a signed TelephonyCarrierEvent (verify it with
verifyTelephonyEvent), and for answer/fallback the XML you respond
with is executed on the call. These methods are the other direction - acts
on Connect's carrier account, each checked against the number being handled
by your application in the workspace you name.
Methods
dial()
dial(input: TelephonyDialInput): Promise<TelephonyDialResult>;Rings to from a number you handle. The answer arrives at your webhook as
an answer event whose params.RequestUUID is the providerRequestId
returned here. Refused with code: "do_not_call" for a number on the
workspace's do-not-call list.
Parameters
| Parameter | Type |
|---|---|
input | TelephonyDialInput |
Returns
Promise<TelephonyDialResult>
hangup()
hangup(providerCallUuid: string, input: {
workspaceId: string;
}): Promise<void>;Ends a live call you own. code: "not_found" once it has already ended.
Parameters
| Parameter | Type |
|---|---|
providerCallUuid | string |
input | { workspaceId: string; } |
input.workspaceId | string |
Returns
Promise<void>
browserToken()
browserToken(input: BrowserTokenInput): Promise<BrowserTokenResult>;A carrier credential a browser places calls with. Calls it places are
answered by your webhook - channel: null, and params.From carrying
the namespaced subject.
Parameters
| Parameter | Type |
|---|---|
input | BrowserTokenInput |
Returns
Promise<BrowserTokenResult>
archiveRecording()
archiveRecording(recordingId: string, input: ArchiveRecordingInput): Promise<ArchivedRecording>;Moves a carrier recording into the workspace's Drive, as a file your
application uploaded, and deletes it from the carrier. Idempotent on
recordingId - a retry answers the first archive. Pass the fileId to
connect.ai.analyzeCall for a transcript.
Parameters
| Parameter | Type |
|---|---|
recordingId | string |
input | ArchiveRecordingInput |
Returns
Promise<ArchivedRecording>
Interface: UploadInput
Properties
| Property | Type | Description |
|---|---|---|
workspaceId | string | - |
file | Blob | Any Blob/File. In Node, new Blob([buffer]) or fs.openAsBlob(path). |
fileName? | string | Defaults to the Blob's own name when it has one. |
parentId? | string | Target folder. Omit for the Drive root. |
Interface: VerifyConnectTaskOptions
Extended by
Properties
| Property | Type | Description |
|---|---|---|
invoker? | string | The invoker service account's email - the one the platform gives you with your API key. Defaults to CONNECT_TASK_INVOKER. |
audience? | string | Your own origin, which Connect sets as the token's audience. Defaults to CONNECT_TASK_AUDIENCE, then to the request URL's origin. |
fetch? | FetchLike | Injectable for tests. Defaults to the global fetch. |
now? | () => number | Injectable for tests. Milliseconds since the epoch. |
Interface: WorkspacesResource
Methods
get()
get(input: {
workspaceId: string;
}): Promise<ConnectWorkspaceProfile>;One subscribing workspace's identity - name, legal name, account type, and its owner's display name and email - for your own server, with nobody signed in. What an agent greets a caller with; where a note to the owner goes.
Needs workspace-profile:read and your application installed in that
workspace. Cache it - a name changes about never.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId: string; } |
input.workspaceId | string |
Returns
Promise<ConnectWorkspaceProfile>
list()
list(options: {
asUser: string;
}): Promise<UserWorkspace[]>;The workspaces this user could switch to.
Rows carry inAudience and subscribed so a switcher can show a
workspace it cannot select, with the reason. Hiding it instead reads as
data loss to the person who owns it.
Parameters
| Parameter | Type |
|---|---|
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<UserWorkspace[]>
create()
create(input: CreateUserWorkspaceInput, options: {
asUser: string;
}): Promise<UserWorkspace>;Opens another workspace under the tenant the signed-in user already owns - a branch office, a second business unit.
Every guard CreateWorkspaceUseCase enforces still applies: only the
tenant's owner may call this, type: "business" still needs a non-free
email domain, and the tenant's plan still decides whether it may hold
another workspace at all - a PreconditionError surfaces as a thrown
ConnectError with a message written to be shown ("Your current plan
covers a single workspace...", "You've used all 3 of the workspaces
your plans include..."). There is no tenantId parameter: the caller
names no tenant, same as every other method on this resource names no
workspace.
isActive/subscribed on the result are always false - this does not
switch the session, and the new workspace has no subscription yet. Chain
switch({workspaceId: result.workspaceId}) to move into it, but only
when result.inAudience - a workspace outside this application's
audience is refused by switch the same way selectable() in
<ConnectWorkspaceSwitcher> already treats it.
Parameters
| Parameter | Type |
|---|---|
input | CreateUserWorkspaceInput |
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<UserWorkspace>
switch()
switch(input: {
workspaceId: string;
}, options: {
asUser: string;
}): Promise<WorkspaceSwitchResult>;Moves this session to another workspace, returning a new session token. The one you passed is revoked.
Persist the result before navigating - dropping it signs the user out, because the old token no longer works.
Throws with status 404 when the workspace does not exist or the user is not a member (deliberately indistinguishable), and 403 when it is outside your audience, requires an MFA factor they have not enrolled, or is the workspace they are already on.
Parameters
| Parameter | Type |
|---|---|
input | { workspaceId: string; } |
input.workspaceId | string |
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<WorkspaceSwitchResult>
billingProfile()
billingProfile(options: {
asUser: string;
}): Promise<WorkspaceBillingProfile>;The billing identity of the workspace this session is on - its legal name, GSTIN, registered address and entity form.
For prefilling your own settings, once. See
WorkspaceBillingProfile, which carries the whole argument: this is the
Bill To side, yours is the Bill From side, and they are the same facts
for most customers without being the same field. There is no write form.
// app/settings/page.tsx - offer "Use my billing details"
const token = await getConnectSessionToken();
const billing = token ? await connect.workspaces.billingProfile({ asUser: token }) : null;Needs two things, not one. workspaces:read on your key, and
billing:read on the signed-in member - a tax identity belongs to
whoever can already see it in Connect's billing screens, so an ordinary
member gets a 403 here where they get a roster from members(). That
cannot be granted through a role on your application, deliberately.
So treat the 403 as a normal branch, not an error: offer the prefill to whoever can use it and let everyone else type their own details. A workspace admin is the person who sets this up anyway.
Parameters
| Parameter | Type |
|---|---|
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<WorkspaceBillingProfile>
members()
members(options: {
asUser: string;
includeUnassigned?: boolean;
}): Promise<ConnectWorkspaceMember[]>;The team of the workspace this session is on.
Members carry permissions already resolved for your application, not raw Connect roles - so you never reimplement Connect's role resolution, and you are told nothing about the customer's Connect account beyond what your application needs.
Seat holders only, by default. A workspace member with no seat on your
application cannot sign in to it, so they are not part of your team and
do not belong in an owner picker or an assignee list. Pass
includeUnassigned: true from the screen where an admin gives a seat -
it needs the people who do not have one yet. That form needs the signed-in
member to be able to change access (a 403 otherwise), and every row
carries assigned so the screen can tell the two apart.
Parameters
| Parameter | Type |
|---|---|
options | { asUser: string; includeUnassigned?: boolean; } |
options.asUser | string |
options.includeUnassigned? | boolean |
Returns
Promise<ConnectWorkspaceMember[]>
access()
access(options: {
asUser: string;
}): Promise<ConnectMemberAccess>;Seats, what the caller may do with them, and the invitations already holding one - everything a team screen needs to decide what to draw.
Behind members:manage rather than workspaces:read, and gated on the
caller's own member:invite: it names people who are not members yet.
Parameters
| Parameter | Type |
|---|---|
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<ConnectMemberAccess>
invite()
invite(input: {
inviteeEmail: string;
roleKey?: string | null;
}, options: {
asUser: string;
}): Promise<ConnectInvitation>;Invite someone into the workspace, with access to your application.
roleKey names a role from your own catalog (or the reserved "admin");
omit it to add someone to the workspace with no access to your
application. You cannot send a Connect role - the grant is built server
side as [member, app:<yourKey>:<roleKey>], so nothing this method can
express reaches outside your application.
Spends a member seat when the invitation is created, not when it is
accepted. Throws ConnectError with status 409 and a message written to
be shown when there is no seat, and 403 when the caller is inviting
someone with access they do not hold themselves.
Parameters
| Parameter | Type |
|---|---|
input | { inviteeEmail: string; roleKey?: string | null; } |
input.inviteeEmail | string |
input.roleKey? | string | null |
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<ConnectInvitation>
setAccess()
setAccess(input: {
userId: string;
roleKey: string | null;
}, options: {
asUser: string;
}): Promise<ConnectMemberAccessResult>;Grant or revoke your application's role for an existing member.
roleKey: null revokes it.
Moves only your own grant: Connect recomputes the member's roles as what they hold now, minus any grant naming your application, plus the one you asked for. Their workspace role and every other application they use are untouched, and there is no form of this that removes them from the workspace.
Re-sending the role they already have is a no-op rather than an error, so a form that submits whatever is on screen is safe.
Parameters
| Parameter | Type |
|---|---|
input | { userId: string; roleKey: string | null; } |
input.userId | string |
input.roleKey | string | null |
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<ConnectMemberAccessResult>
revokeInvitation()
revokeInvitation(input: {
invitationId: string;
}, options: {
asUser: string;
}): Promise<void>;Withdraw a pending invitation your application sent, freeing its seat.
Only yours - an invitation carrying another application's grant is a 404, deliberately indistinguishable from one that does not exist.
Parameters
| Parameter | Type |
|---|---|
input | { invitationId: string; } |
input.invitationId | string |
options | { asUser: string; } |
options.asUser | string |
Returns
Promise<void>
Type Aliases
Type Alias: ConnectWebhookHandlers
type ConnectWebhookHandlers = { [K in ConnectWebhookEventType]?: (data: ConnectWebhookEventMap[K], envelope: ConnectWebhookEnvelope<K>) => void | Promise<void> };One handler per event type. Every one is optional; unhandled types are acknowledged and ignored.
Type Alias: FetchLike
type FetchLike = (input: string, init?: RequestInit) => Promise<Response>;Parameters
| Parameter | Type |
|---|---|
input | string |
init? | RequestInit |
Returns
Promise<Response>
Variables
Variable: DEFAULT_BASE_URL
const DEFAULT_BASE_URL: "https://connect.cosmoops.com" = "https://connect.cosmoops.com";Server-only half of @cosmoops/connect-sdk.
Everything here handles the API key, so none of it may run in a browser.
The browser half is @cosmoops/connect-sdk/client, which talks to your own
origin and reaches Connect through createConnectHandler below.
Variable: SDK_VERSION
const SDK_VERSION: "0.56.0" = "0.56.0";Kept in step with package.json by sdk.test.ts.
Functions
Function: createConnectHandler()
function createConnectHandler(options: ConnectHandlerOptions): (request: Request) => Promise<Response>;Parameters
| Parameter | Type |
|---|---|
options | ConnectHandlerOptions |
Returns
(request: Request) => Promise<Response>
Function: createConnectTaskHandler()
function createConnectTaskHandler<TPayload>(options: ConnectTaskHandlerOptions<TPayload>): (request: Request) => Promise<Response>;A ready-made POST route for a queue's URL:
// app/api/tasks/campaign/route.ts
export const POST = createConnectTaskHandler<{ campaignId: string }>({
handle: async ({ workspaceId, payload }) => runCampaignPage(workspaceId, payload.campaignId),
});401 for anything that does not verify, 500 when handle throws (or
Google's keys cannot be fetched), 200 otherwise.
Type Parameters
| Type Parameter | Default type |
|---|---|
TPayload | unknown |
Parameters
| Parameter | Type |
|---|---|
options | ConnectTaskHandlerOptions<TPayload> |
Returns
(request: Request) => Promise<Response>
Function: createConnectWebhookHandler()
function createConnectWebhookHandler(options: ConnectWebhookHandlerOptions): (request: Request) => Promise<Response>;A ready-made POST route for Connect's webhooks.
export const POST = createConnectWebhookHandler({
secret: process.env.CONNECT_WEBHOOK_SECRET!,
on: {
"workspace.deletion_scheduled": ({ workspaceId, purgeAfter }) =>
freeze(workspaceId, purgeAfter),
"workspace.deletion_cancelled": ({ workspaceId }) => unfreeze(workspaceId),
"workspace.deleted": ({ workspaceId }) => purge(workspaceId),
},
})Three behaviours worth knowing, because each of them is a decision rather than an accident:
An unverified request gets a 401 and nothing else happens. No parsing, no logging of the body, no handler. The endpoint is a public URL, so anything reachable before the signature check is reachable by anyone.
An unhandled event type is a 200. Connect adds event types over time, and answering 4xx to one you have not written code for would dead-letter it immediately and, after enough of them, disable your endpoint. Silence is the correct answer to "we do not care about this one".
A throwing handler produces a 500, on purpose. That is what makes Connect retry. Swallowing the error would acknowledge an event you failed to process, and the retry ladder is the only thing standing between a transient failure and a permanently missed deletion.
Delivery is at-least-once and unordered. Dedupe on envelope.id, and
reconcile against the readiness endpoint rather than trusting arrival
order - the pull channel is authoritative, this one is fast.
Parameters
| Parameter | Type |
|---|---|
options | ConnectWebhookHandlerOptions |
Returns
(request: Request) => Promise<Response>
Function: createState()
function createState(byteLength?: number): string;A URL-safe random string, suitable for state or a PKCE verifier.
Parameters
| Parameter | Type | Default value |
|---|---|---|
byteLength | number | 32 |
Returns
string
Function: verifyConnectTask()
function verifyConnectTask<TPayload>(request: Request, options?: VerifyConnectTaskOptions): Promise<ConnectTaskDelivery<TPayload>>;Verify a delivery and return it, or throw ConnectTaskVerificationError.
Reads the body, so call it once and use what it returns.
Type Parameters
| Type Parameter | Default type |
|---|---|
TPayload | unknown |
Parameters
| Parameter | Type |
|---|---|
request | Request |
options | VerifyConnectTaskOptions |
Returns
Promise<ConnectTaskDelivery<TPayload>>
Function: verifyConnectWebhook()
function verifyConnectWebhook(
rawBody: string,
signatureHeader: string | null | undefined,
secret: string,
now?: Date): boolean;Verifies a Connect-Signature header against the raw request body.
Exported on its own for anyone wiring this into a framework
createConnectWebhookHandler does not cover. The raw body matters:
verify before parsing, and never re-serialise - JSON.stringify(await req.json()) produces a different string whenever key order or number
formatting differs, and the signature will not match.
Returns a boolean and never explains which check failed. Reporting "bad timestamp" separately from "bad signature" tells whoever is probing which half of a forgery to keep working on.
Parameters
| Parameter | Type |
|---|---|
rawBody | string |
signatureHeader | string | null | undefined |
secret | string |
now | Date |
Returns
boolean
Function: verifyTelephonyEvent()
function verifyTelephonyEvent(
rawBody: string,
signatureHeader: string | null | undefined,
secret: string,
now?: Date): TelephonyCarrierEvent;Verifies and parses a carrier event Connect relayed to your telephony webhook.
secret is your application's webhook endpoint secret - the one
verifyConnectWebhook takes. Connect signs a relayed event with every
active endpoint secret your application holds (one v1= each), so this
accepts the header if any signature matches the secret you hold.
Pass the raw body, read as text before parsing. Throws ConnectError
(status: 401, code: "invalid_signature") for a signature that does not
verify or a stale timestamp, and code: "malformed_event" for a verified
body that is not a carrier event - never saying which half of a forgery
failed.
export async function POST(request: Request) {
const event = verifyTelephonyEvent(await request.text(), request.headers.get("connect-signature"), secret);
if (event.event === "answer") return new Response(answerXml(event), { headers: { "content-type": "application/xml" } });
return new Response(null, { status: 200 });
}Parameters
| Parameter | Type |
|---|---|
rawBody | string |
signatureHeader | string | null | undefined |
secret | string |
now | Date |