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'
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>>
}
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)
}
For idempotent reads. Pass query parameters as the second argument - they're serialised onto the URL automatically (null/undefined values are skipped).
const result = await integration.get('finnhub/market-news', {
category: 'crypto',
minId: 100,
})
For idempotent updates against REST-style endpoints.
const result = await integration.put('notion/update-page', {
pageId: 'abc123',
properties: { Status: 'Done' },
})
For destructive operations. The platform proxy is signed and rate-limited per integration; OAuth scopes apply if the integration is OAuth-backed.
const result = await integration.delete('notion/delete-block', {
blockId: 'xyz789',
})
IntegrationResponse<T>#
type IntegrationResponse<T> =
| { success: true; data: T }
| { success: false; error: string; issues?: ValidationIssue[] }
type ValidationIssue = {
path?: string[]
message: string
code?: string
}
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
}
const r = await integration.post('exa/search', body, {
timeoutMs: 30_000,
headers: { 'X-Custom': 'value' },
})
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'
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
}
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
}
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>
usePlatform()#
function usePlatform(): {
platformFetch: (path: string, init?: RequestInit) => Promise<Response>
inbox: InboxEntry[]
/** Activate the inbox WebSocket. Returns an unsubscribe function. */
subscribeInbox: () => () => void
}
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
}
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
}
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.
| Endpoint | Method | Purpose |
|---|---|---|
/api/integrations/status | GET | Per-scope connection flags for all OAuth providers |
/api/integrations/oauth/:provider/disconnect | DELETE | Revokes 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
// }
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
}
}
Detection (result.data?.requiresOAuth, never success === false), the unwrap pattern, and the recovery flow live in the requiresOAuth contract.
See also#
- External APIs guide - patterns, billing routing, and discovery.
- Google OAuth guide - the per-user consent contract for
google/*. - LiveKit rooms guide - room lifecycle and reserve-then-settle billing.
- CLI integrations command -
list/info/invoke. - AI chat guide - streamed LLM responses with tool use (uses a different pipeline).