Skip to main content
Documentation

Integrations reference

The `integration` client and OAuth helpers.

On this page

The integration object fronts 215+ third-party API endpoints through the platform's signed proxy. For discovery, billing, and the full workflow see the external APIs guide; for the CLI catalog, see deepspace integrations.

import { integration } from 'deepspace'
import type { IntegrationResponse, RequestOptions } from 'deepspace'
ts

integration#

The integration client exposes four HTTP verbs. All return a typed envelope.

const integration: {
  get<T>:    (endpoint: string, params?: Record<string, string | number | boolean | null | undefined>, options?: RequestOptions) => Promise<IntegrationResponse<T>>
  post<T>:   (endpoint: string, data?: unknown, options?: RequestOptions) => Promise<IntegrationResponse<T>>
  put<T>:    (endpoint: string, data?: unknown, options?: RequestOptions) => Promise<IntegrationResponse<T>>
  delete<T>: (endpoint: string, data?: unknown, options?: RequestOptions) => Promise<IntegrationResponse<T>>
}
ts

get takes a query-parameter object as its middle argument; the others take an optional JSON body. delete also accepts a body - the api-worker dispatches DELETE with a JSON payload when one is provided.

Endpoint names are two segments: <integration>/<endpoint> (e.g. openai/chat-completion).

The most common verb. Use for actions, completions, lookups, and anything with a request body.

const result = await integration.post('openai/chat-completion', {
  model: 'claude-sonnet-5',
  messages: [{ role: 'user', content: 'Hello' }],
})

if (result.success) {
  console.log(result.data)
} else {
  console.error(result.error, result.issues)
}
ts

IntegrationResponse<T>#

type IntegrationResponse<T> =
  | { success: true; data: T }
  | { success: false; error: string; issues?: ValidationIssue[] }

type ValidationIssue = {
  path?: string[]
  message: string
  code?: string
}
ts

issues appears when the api-worker's Zod validator rejects the body shape. Read it instead of guessing field names - or run deepspace integrations info <endpoint> to print the schema before you call.

RequestOptions#

type RequestOptions = {
  headers?: Record<string, string>
  timeoutMs?: number     // default 120000 (120s)
  signal?: AbortSignal   // cancel the request from the caller
}
ts
const r = await integration.post('exa/search', body, {
  timeoutMs: 30_000,
  headers: { 'X-Custom': 'value' },
})
ts

signal aborts the in-flight request and resolves the envelope as { success: false, error: 'Request cancelled' }. Pass the AbortSignal that useAsyncResource hands your fetcher so unmounts and dependency changes cancel cleanly.

Async resource hooks#

Two general-purpose hooks turn any async fetch - most commonly an integration call - into render-ready UI state: loading, error with a local retry, empty, and success. Use them instead of hand-rolled useEffect fetch state, and keep failures in place: a failed resource re-fires via reload()/retry(), never by reloading the page. Usage patterns live in the external APIs guide.

import { useAsyncResource, usePagedResource } from 'deepspace'
ts

useAsyncResource#

One-shot fetch keyed on a dependency array - a lookup, a single completion, a status check.

function useAsyncResource<T>(
  fetcher: (signal: AbortSignal) => Promise<T>,
  deps: readonly unknown[],
  options?: UseAsyncResourceOptions<T>,
): AsyncResourceState<T> & { reload: () => void }

type AsyncResourceState<T> = {
  status: 'idle' | 'loading' | 'ready' | 'error'
  data: T | null
  error: string | null
  isRefreshing: boolean   // a re-fetch is in flight while previous data stays visible
  isSlow: boolean         // the in-flight request has exceeded slowAfterMs
  retryCount: number
}

type UseAsyncResourceOptions<T> = {
  enabled?: boolean          // default true; false parks the hook at 'idle'
  initialData?: T | null
  keepPreviousData?: boolean // default true - previous data stays visible during re-fetch
  retry?: number             // automatic retries after failure; default 0
  retryDelayMs?: number      // default 2000
  slowAfterMs?: number       // default 10000; 0 disables the isSlow signal
}
ts

The fetcher must throw on failure so the hook can capture the error - for integration envelopes, if (!r.success) throw new Error(r.error). The fetch re-runs when deps change; reload() re-fires it manually. Forward the provided AbortSignal (e.g. as RequestOptions.signal) so unmounts and dependency changes cancel in-flight work.

usePagedResource#

Bounded, append-on-demand pagination for feeds. It fetches page 1 automatically, appends on loadMore(), and clamps oversized pages so a feed stays bounded instead of pulling an entire upstream dataset.

function usePagedResource<T>(
  fetchPage: (args: { page: number; pageSize: number; signal: AbortSignal }) => Promise<{ items: T[]; hasMore?: boolean }>,
  deps: readonly unknown[],
  options?: UsePagedResourceOptions<T>,
): PagedResourceState<T> & {
  loadMore: () => void   // fetch the next page and append
  retry: () => void      // re-fire the failed page
  refresh: () => void    // restart from page 1
}

type PagedResourceState<T> = {
  status: 'idle' | 'loading' | 'ready' | 'error'
  items: T[]
  error: string | null
  warning: string | null   // set when an oversized page was clamped
  hasMore: boolean
  isLoadingInitial: boolean
  isLoadingMore: boolean
  isRefreshing: boolean
}

type UsePagedResourceOptions<T> = {
  enabled?: boolean
  initialItems?: T[]
  pageSize?: number          // default 20
  maxItemsPerPage?: number   // default pageSize; larger API pages are clamped with a warning
  keepPreviousData?: boolean // default true
  autoRetryOnError?: boolean // default false - failures wait for retry() with backoff otherwise off
  retryDelayMs?: number      // default 2000 (backoff base)
  maxRetryDelayMs?: number   // default 30000
}
ts

hasMore comes from the page result (hasMore ?? items.length >= pageSize). When a page fails after items have already loaded, status stays 'ready' and error is set - render an inline retry next to the intact list rather than replacing it with an error screen.

Cross-app platform context#

These exports support a small set of cross-app surfaces (inbox, platform fetch). The scaffold does not include <PlatformProvider> - wrap manually if you need this surface.

import { PlatformProvider, usePlatform, useInbox, usePlatformWS } from 'deepspace'

<PlatformProvider>{/* tree */}</PlatformProvider>
ts

usePlatform()#

function usePlatform(): {
  platformFetch: (path: string, init?: RequestInit) => Promise<Response>
  inbox: InboxEntry[]
  /** Activate the inbox WebSocket. Returns an unsubscribe function. */
  subscribeInbox: () => () => void
}
ts

platformFetch prepends /platform and adds the auth header automatically. Throws if no <PlatformProvider> is mounted. subscribeInbox is the primitive useInbox() calls under the hood - apps rarely call it directly.

useInbox()#

function useInbox(): InboxEntry[]

type InboxEntry = {
  conversationId: string
  scope: { type: string; participants?: string[]; appId?: string; contentRef?: string; ticketNumber?: string }
  displayName: string
  muted: boolean
  joinedAt: string
  lastMessageAt: string | null
  lastMessagePreview: string | null
  lastMessageAuthor: string | null
  unreadCount: number
}
ts

The inbox WebSocket activates only when at least one component subscribes via useInbox().

usePlatformWS<S>(options)#

Generic platform WebSocket subscription for custom platform-side streams. S is the state shape; the constraint requires a status: ConnectionStatus field so the hook can drive reconnect UI.

function usePlatformWS<S extends { status: ConnectionStatus }>(
  options: PlatformWSOptions<S>,
): PlatformWSResult<S>

type PlatformWSOptions<S> = {
  /** DO path segment - e.g. 'orders' for /platform/ws/orders/{scopeId} */
  path: string
  /** Scope ID - e.g. 'app:app_01HZXYABCDEFGHJKMNPQRSTVWX'. Pass undefined to skip the connection. */
  scopeId: string | undefined
  initialState: S
  onMessage: (msg: Record<string, unknown>, prev: S) => S
}

type PlatformWSResult<S> = {
  state: S
  send: (msg: Record<string, unknown>) => void
}
ts

OAuth endpoints#

Google (google/*) is the OAuth-backed integration surface. Two REST endpoints manage per-user connections - call them via fetch with the session token. There is no connect endpoint: authorization URLs come only from the requiresOAuth response, so consent always starts from a real endpoint call. The Google OAuth guide carries the full contract - billing, scope gating, and test mocks.

EndpointMethodPurpose
/api/integrations/statusGETPer-scope connection flags for all OAuth providers
/api/integrations/oauth/:provider/disconnectDELETERevokes the current user's stored tokens
const r = await fetch('/api/integrations/status', {
  headers: { Authorization: `Bearer ${await getAuthToken()}` },
})
const { google } = await r.json()
// {
//   connected: boolean,
//   gmailSend: boolean, gmailRead: boolean, gmailModify: boolean,
//   calendar: boolean, drive: boolean, contacts: boolean,
//   gmail: boolean,     // aggregate: gmailSend || gmailRead
//   email?: string      // connected account email, when known
// }
ts

Broader scopes imply narrower ones, never the reverse - gate each feature on its own flag (implication rules).

requiresOAuth response#

Calls lacking tokens or a required scope return the OAuth-required payload as a normal success result - HTTP 200, nested under data:

{
  success: true,
  data: {
    requiresOAuth: true,
    provider: 'google',
    scopes: string[],
    authUrl: string
  }
}
ts

Detection (result.data?.requiresOAuth, never success === false), the unwrap pattern, and the recovery flow live in the requiresOAuth contract.

See also#