> ## Documentation Index
> Fetch the complete documentation index at: https://docs.deep.space/llms.txt
> Use this file to discover all available pages before exploring further.

# 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

```ts theme={null}
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:

```ts theme={null}
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:

```ts theme={null}
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.

<Tabs>
  <Tab title="List catalog">
    No login required.

    ```bash theme={null}
    # 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.
  </Tab>

  <Tab title="Inspect one endpoint">
    No login required.

    ```bash theme={null}
    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.
  </Tab>

  <Tab title="Invoke (test call)">
    Login required, billed to the logged-in user.

    ```bash theme={null}
    # 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.
  </Tab>
</Tabs>

See the [CLI reference](/cli-reference/commands#integrations-and-invoke) for the full flag list.

## Billing - developer vs user

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

```ts theme={null}
// 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  |

<Warning>
  **Auth-gate any UI that calls `integration.post(...)`** for `'developer'`-billed endpoints. The api-worker accepts anonymous callers for developer-billed integrations, so a public endpoint silently bills the owner for every visitor (or bot) hit. Wrap calling components in `useAuth().isSignedIn`.
</Warning>

## Response shapes

`data` shape varies by endpoint. Common patterns:

```ts theme={null}
// 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
}
```

<Note>
  **Empty results are not errors.** Some endpoints return `success: true` with empty data when the upstream has no matches. Check for empty state explicitly. For example, `finnhub/stock-price` returns an all-zero quote for an invalid symbol - the call "succeeds" but the data is meaningless.
</Note>

## Calling from your worker

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

```ts theme={null}
// 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:

```ts theme={null}
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:

```ts theme={null}
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:

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

Check connection status:

```ts theme={null}
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:

```ts theme={null}
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](/guides/ai-chat)) rather than `integration.post`. The proxy returns the full response; the AI helpers stream it.

## Next steps

* [AI chat](/guides/ai-chat) - streamed LLM responses with tool use.
* [Server actions](/guides/server-actions) - call integrations from worker code.
* [Integrations reference](/sdk-reference/client/integrations) - full `integration` API.
