Skip to main content
Docs
Worker & server

External APIs

Call third-party APIs through the platform's integration proxy.

DeepSpace fronts 215+ third-party API endpoints - LLMs (Claude, GPT, Cerebras), search (Exa, Tavily), media (LiveKit, Resend), finance (Finnhub, Alpha Vantage), social (Discord, Slack), Google Workspace, weather, and more - through a single signed proxy. You don't store API keys, configure webhooks, or build per-vendor SDKs. You call integration.post(...), the platform handles billing, rate-limiting, and provider routing.

Calling an integration#

import { integration } from 'deepspace'

const result = await integration.post('openweathermap/geocoding', { q: 'Brooklyn' })

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

Endpoint names are always two segments: <integration>/<endpoint>. The response is a discriminated envelope:

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

When the api-worker's Zod validator rejects a body, issues carries field-level errors:

if (!result.success && result.issues) {
  for (const issue of result.issues) {
    console.log(issue.path, issue.message)
  }
}

Discover endpoints - deepspace integrations#

The CLI exposes the full catalog and lets you invoke endpoints interactively. Discovery is free; calls are billed - list and info work without authentication, so you can scope integration work before deciding whether to log in.

No login required.

# Human-readable
npx deepspace integrations list

# Machine-readable (JSON)
npx deepspace integrations list --json
Returns all 215+ endpoints grouped by integration with one-line descriptions.

No login required.

npx deepspace integrations info openai/chat-completion
npx deepspace integrations info openai/chat-completion --json
Prints the input schema (Zod), output schema, and an example body. Use this before guessing field names - the schemas are the source of truth.

Login required, billed to the logged-in user.

# Body inline
npx deepspace invoke openai/chat-completion --body '{
  "model": "claude-sonnet-4-6",
  "messages": [{"role": "user", "content": "Hello"}]
}'

# Body from file or stdin
npx deepspace invoke openai/chat-completion --body-file request.json
cat request.json | npx deepspace invoke openai/chat-completion --body-file -
Useful for verifying a body shape end-to-end before wiring the call into your app.

See the CLI reference for the full flag list.

Billing - developer vs user#

Every integration has a billing setting in src/integrations.ts:

// src/integrations.ts
export const integrations: Record<string, { billing: 'developer' | 'user' }> = {
  google: { billing: 'user' },        // already in scaffold (OAuth requires user-pays)
  openai: { billing: 'developer' },   // owner pays
  exa: { billing: 'developer' },
}
Setting Who pays Anonymous callers
'developer' (default) The app owner via APP_OWNER_JWT Allowed
'user' The signed-in caller Blocked with 401

Response shapes#

data shape varies by endpoint. Common patterns:

// List endpoint
const r = await integration.post('exa/search', { query: 'climate change papers' })
if (r.success) {
  for (const result of r.data.results) {
    console.log(result.title, result.url)
  }
}

// Detail endpoint
const r = await integration.post('finnhub/stock-quote', { symbol: 'AAPL' })
if (r.success) {
  console.log(r.data.c, r.data.h, r.data.l)  // current, high, low
}

Calling from your worker#

Inside server actions and cron tasks, use tools.integration or ctx.integrations.call:

// Server action
export const summarizeDay: ActionHandler<Env> = async ({ tools }) => {
  const r = await tools.integration('openai/chat-completion', {
    model: 'gpt-5.4-mini',
    messages: [{ role: 'user', content: 'Summarize today\'s activity' }],
  })
  // ...
}

// Cron task
export async function runTask(name: string, env: Env) {
  const ctx = buildCronContext(env, env.OWNER_USER_ID, `app:${env.APP_NAME}`)
  const r = await ctx.integrations.call('resend/send-email', { to, subject, text })
}

Both routes go through the api-worker proxy. Billing follows src/integrations.ts - when called from a server action, 'user' integrations bill the caller and 'developer' ones bill the owner.

Request options#

The post, get, put, and delete methods accept an options object:

const r = await integration.post('exa/search', body, {
  timeoutMs: 30_000,                          // default 120s
  headers: { 'X-Custom': 'value' },
})

OAuth integrations#

A handful of integrations require per-user OAuth (Google Workspace, Gmail, Calendar, Drive). These are always billed as 'user'. The scaffold ships Google with billing: 'user'.

When a user calls an OAuth endpoint without a connected account, the response includes a requiresOAuth flag:

const r = await integration.post('google/gmail-send', { to, subject, body })

if (!r.success && r.error === 'requiresOAuth') {
  // Redirect to the OAuth connect URL
  window.location.href = `/api/integrations/oauth/google/connect?return=${encodeURIComponent(location.pathname)}`
}

After connecting, retry the original call. Disconnect via:

await fetch('/api/integrations/oauth/google/disconnect', {
  method: 'DELETE',
  headers: { Authorization: `Bearer ${await getAuthToken()}` },
})

Check connection status:

const r = await fetch('/api/integrations/status', {
  headers: { Authorization: `Bearer ${await getAuthToken()}` },
})
const { google } = await r.json()
// { connected: boolean, scopes: string[] }

Testing integrations#

Integration calls hit real third-party services and cost real money. Keep integration assertions minimal:

  • One integration.post(...) per endpoint per test run, not a matrix
  • Never put integration calls inside for loops or retry-until-success polls
  • Skip 'user'-billed endpoint calls in api.spec.ts - test accounts have no credits and will 402

For the integration call itself, assert the envelope shape - not the upstream provider's exact response:

test('weather lookup returns coords', async ({ request }) => {
  const token = await signInAndGetToken(request)
  const r = await request.post('/api/integrations/openweathermap/geocoding', {
    headers: { Authorization: `Bearer ${token}` },
    data: { q: 'Brooklyn' },
  })
  const body = await r.json()
  expect(body.success).toBe(true)
  expect(body.data[0]).toMatchObject({ lat: expect.any(Number), lon: expect.any(Number) })
})

Tips#

  • Run info before guessing a body shape. The Zod schemas the api-worker validates against are the source of truth - npx deepspace integrations info <endpoint> prints them with an example body.
  • Use tools.integration from server actions for owner-pays endpoints. Keeps the JWT scope correct and centralizes routing.
  • For LLM streaming, use the AI chat pipeline (see AI chat) rather than integration.post. The proxy returns the full response; the AI helpers stream it.

Next steps#