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

# Integrations API

> Call any connected provider's raw REST API through your agent's integrations

## Overview

`Integrations.passthrough` gives your code direct access to a provider's **raw REST API** through the integration your agent is already connected to. The platform relays the call server-side over the agent's own bound connection — your code never sees OAuth tokens or provider credentials, and what the call is allowed to do is exactly what the connection's OAuth grant allows.

Connected integrations already expose curated MCP tools to your agent. Passthrough is for everything those tools don't cover: any endpoint the provider documents, with your own query parameters, headers, and JSON bodies.

```typescript theme={null}
import { Integrations } from 'lua-cli';

// Any GitHub REST endpoint, through the agent's GitHub connection
const res = await Integrations.passthrough('github', {
  method: 'GET',
  path: 'repos/acme/app/pulls/42/files',
});

if (res.status === 200) {
  console.log(res.data); // parsed JSON: the PR's files, each with its patch
}
```

It works everywhere your code runs — tools, jobs, webhooks, and pre/post processors — and every connected integration also auto-attaches a matching agent tool (see [The auto-attached agent tool](#the-auto-attached-agent-tool) below), so the agent itself can make raw provider calls too.

<Note>
  **Scope-gated by the OAuth grant.** The provider enforces its own OAuth scopes on every raw call. A call outside the connection's granted scopes comes back as the provider's own `401`/`403` **inside the response envelope** — never as a thrown error. Reconnect the integration with the needed scopes to widen access.
</Note>

## Import

```typescript theme={null}
import { Integrations } from 'lua-cli';
```

The wire types are exported too, if you want to name them explicitly:

```typescript theme={null}
import type {
  IntegrationPassthroughRequest,
  IntegrationPassthroughResponse,
  IntegrationPassthroughMethod,
} from 'lua-cli';
```

## Method

### Integrations.passthrough(integrationType, request)

Make one raw provider API call through the agent's connected integration.

<ParamField path="integrationType" type="string" required>
  The connected integration to call through, e.g. `'github'`, `'microsoft'`, `'linear'`. Must match an integration the agent is connected to (see [`lua integrations`](/cli/integrations-command)).
</ParamField>

<ParamField path="request" type="IntegrationPassthroughRequest" required>
  The provider call to relay — see the fields below.
</ParamField>

**Returns:** `Promise<IntegrationPassthroughResponse>` — the raw provider response envelope.

### Request fields (IntegrationPassthroughRequest)

<ParamField path="method" type="'GET' | 'POST' | 'PUT' | 'PATCH' | 'DELETE' | 'HEAD'" required>
  HTTP method of the provider call.
</ParamField>

<ParamField path="path" type="string" required>
  Provider API path **after the provider's base URL**, e.g. `'repos/{owner}/{repo}/pulls/42/files'` for GitHub or `'v1.0/me'` for Microsoft Graph. A leading `/` is tolerated. Relative traversal segments (`..`) are rejected.
</ParamField>

<ParamField path="query" type="Record<string, string | number | boolean>">
  Query-string parameters (pagination etc.). Forwarded to the provider as-is. A query string embedded in `path` is merged in too.
</ParamField>

<ParamField path="data" type="unknown">
  Request body. Objects and arrays are sent as JSON (with `Content-Type: application/json` set for you); a string is sent verbatim — set your own `Content-Type` header for non-JSON payloads. Ignored on `GET`/`HEAD`.
</ParamField>

<ParamField path="headers" type="Record<string, string>">
  Extra request headers forwarded to the provider. `Authorization` and other auth/hop-by-hop headers are managed server-side and cannot be overridden. See the [limitation on `Accept` media-type overrides](#limitation-accept-header-media-type-overrides) below.
</ParamField>

### Response envelope (IntegrationPassthroughResponse)

| Field     | Type                     | Description                                                                                                                                                       |
| --------- | ------------------------ | ----------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `status`  | `number`                 | The provider's HTTP status code, relayed faithfully — including provider errors like `403` or `404`                                                               |
| `headers` | `Record<string, string>` | Provider response headers (lower-cased names; auth-related headers stripped)                                                                                      |
| `data`    | `unknown`                | Provider response body: **parsed JSON** when the provider responded with JSON, the **raw string** otherwise (e.g. `text/html` or `text/plain` round-trips intact) |

## Error model: envelope vs thrown

There are two distinct kinds of failure, and they surface differently on purpose:

**Provider errors come back in the envelope.** If the provider itself rejects the call — missing OAuth scope (`403`), not found (`404`), provider-side validation (`422`) — the envelope relays the provider's own status and body so you can see exactly what the provider said. **Nothing is thrown.** Always branch on `status`:

```typescript theme={null}
const res = await Integrations.passthrough('github', {
  method: 'GET',
  path: 'repos/acme/private-repo/pulls/42/files',
});

if (res.status === 403) {
  // The GitHub connection lacks a scope for this call — the body is
  // GitHub's own error message. Reconnect with wider scopes to fix.
  return { success: false, error: 'GitHub denied the call', detail: res.data };
}
```

**Route-level failures are thrown.** If the call never reaches the provider, `Integrations.passthrough` throws a plain `Error` with a human-readable message. The relay rejects such calls with one of these typed reasons:

| Code                          | HTTP status | When it fires                                                                                            | Remedy                                                                               |
| ----------------------------- | ----------- | -------------------------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------ |
| `passthrough_invalid_request` | 400         | Missing/invalid `method` or `path` (e.g. a method outside the allowed six, or a path with `..` segments) | Fix the request shape                                                                |
| `passthrough_disabled`        | 403         | An admin has switched passthrough **off** for this integration                                           | Ask a workspace admin to re-enable it, or use the integration's curated tools        |
| `passthrough_no_connection`   | 404         | The agent has no bound connection for `integrationType`                                                  | Connect the integration with [`lua integrations connect`](/cli/integrations-command) |
| `passthrough_rate_limited`    | 429         | The per-agent passthrough rate limit was exceeded                                                        | Back off and retry shortly; batch or cache calls                                     |
| `passthrough_upstream_error`  | 502         | The relay could not reach the upstream integration layer (transport failure)                             | Transient — retry with backoff                                                       |
| `passthrough_not_configured`  | 503         | The platform's server-side integration relay isn't configured for this workspace                         | Contact support                                                                      |

<Note>
  The **Code** column is the relay's typed rejection code. The `Error` thrown in sandbox code carries the corresponding human-readable message — the code token itself is not embedded in the message — so treat any throw as "the provider was never called" rather than string-matching on codes. The [auto-attached agent tool](#the-auto-attached-agent-tool) does surface the typed `code` directly on route-level failures.
</Note>

```typescript theme={null}
try {
  const res = await Integrations.passthrough('github', { method: 'GET', path: 'user' });
  return { status: res.status, user: res.data };
} catch (error) {
  // Route-level only: disabled, no connection, rate limited, transport, …
  return { success: false, error: error instanceof Error ? error.message : 'passthrough failed' };
}
```

## Guardrails

<CardGroup cols={3}>
  <Card title="Admin enable switch" icon="toggle-on">
    Passthrough is enabled per integration by default, and workspace admins can switch it off for any integration. A disabled integration rejects with `passthrough_disabled`.
  </Card>

  <Card title="Every call audited" icon="clipboard-list">
    Every passthrough call — including denied ones — is audit-logged with identifiers, status, and latency. Request and response bodies are never logged.
  </Card>

  <Card title="Per-agent rate limit" icon="gauge-high">
    Calls are rate-limited per agent (default 120 calls per minute). Exceeding it rejects with `passthrough_rate_limited` — back off and retry.
  </Card>
</CardGroup>

## Limitation: Accept-header media-type overrides

<Warning>
  Custom request headers are forwarded, but **`Accept` media-type overrides do not change what the provider returns** — the relay normalizes content negotiation. For example, requesting a GitHub pull request with `Accept: application/vnd.github.diff` returns the standard JSON representation, not a unified diff.

  Use the provider's **JSON-native equivalent** instead: GitHub's `GET repos/{owner}/{repo}/pulls/{n}/files` returns each changed file with its `patch` — the per-file diff — as plain JSON. Non-JSON *response bodies* are unaffected: endpoints that natively return text or HTML (e.g. GitHub's markdown renderer) round-trip intact as strings in `data`.
</Warning>

## Examples

### Microsoft Graph: profile and files

The generic Microsoft connector exposes relatively few curated tools — passthrough opens up the whole of Microsoft Graph through it.

```typescript theme={null}
import { LuaTool, Integrations } from 'lua-cli';
import { z } from 'zod';

export default class OneDriveRecentTool implements LuaTool {
  name = 'onedrive_recent';
  description = "List the connected user's OneDrive root folder";

  inputSchema = z.object({});

  async execute() {
    // Who is connected?
    const me = await Integrations.passthrough('microsoft', {
      method: 'GET',
      path: 'v1.0/me',
    });
    if (me.status !== 200) {
      return { success: false, status: me.status, error: me.data };
    }

    // List files in the OneDrive root
    const files = await Integrations.passthrough('microsoft', {
      method: 'GET',
      path: 'v1.0/me/drive/root/children',
      query: { $top: 25, $orderby: 'lastModifiedDateTime desc' },
    });
    if (files.status !== 200) {
      return { success: false, status: files.status, error: files.data };
    }

    const items = (files.data as any).value ?? [];
    return {
      success: true,
      user: (me.data as any).displayName,
      files: items.map((f: any) => ({ name: f.name, modified: f.lastModifiedDateTime })),
    };
  }
}
```

### GitHub: review a pull request

List a PR's changed files — each carries its own `patch` (the per-file diff) as JSON — then post a review with a JSON body.

```typescript theme={null}
import { LuaTool, Integrations } from 'lua-cli';
import { z } from 'zod';

export default class ReviewPrTool implements LuaTool {
  name = 'review_pr';
  description = 'Read a pull request diff and post a review';

  inputSchema = z.object({
    owner: z.string(),
    repo: z.string(),
    pullNumber: z.number(),
    comment: z.string(),
    approve: z.boolean().default(false),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const base = `repos/${input.owner}/${input.repo}/pulls/${input.pullNumber}`;

    // 1. The diff, JSON-natively: each file entry includes its `patch`
    const files = await Integrations.passthrough('github', {
      method: 'GET',
      path: `${base}/files`,
      query: { per_page: 100 },
    });
    if (files.status !== 200) {
      // Provider error relayed in the envelope — e.g. 403 on a missing scope
      return { success: false, status: files.status, error: files.data };
    }

    const patches = (files.data as any[]).map((f) => ({
      filename: f.filename,
      additions: f.additions,
      deletions: f.deletions,
      patch: f.patch, // per-file unified diff
    }));

    // 2. Post the review (JSON body POSTs through as-is)
    const review = await Integrations.passthrough('github', {
      method: 'POST',
      path: `${base}/reviews`,
      data: {
        event: input.approve ? 'APPROVE' : 'COMMENT',
        body: input.comment,
      },
    });

    return {
      success: review.status === 200,
      reviewStatus: review.status,
      filesReviewed: patches.length,
      patches,
    };
  }
}
```

## The auto-attached agent tool

Every connected integration also attaches one synthetic tool to the agent — named `{integrationType}_passthrough`, e.g. `github_passthrough` — alongside that integration's curated tools. It takes an equivalent `method` / `path` / `query` / `body` / `headers` input (note: the tool's body field is named `body`, where the SDK's is `data`) and returns the same `{ status, headers, data }` envelope, so the agent can reach any provider endpoint its connection allows without you writing a tool for it. The same guardrails (admin switch, audit log, rate limit) apply identically.

If you'd rather the agent *not* have raw API access to an integration, a workspace admin can turn the integration's passthrough switch off — that disables both the agent tool and `Integrations.passthrough` calls for it.

## Related APIs

<CardGroup cols={2}>
  <Card title="Integrations Command" href="/cli/integrations-command" icon="plug">
    Connect integrations, manage scopes, and set up triggers
  </Card>

  <Card title="LuaTrigger" href="/api/luatrigger" icon="bolt">
    Wake the agent when events fire in a connected integration
  </Card>

  <Card title="LuaWebhook" href="/api/luawebhook" icon="webhook">
    Receive external events with full control of the HTTP response
  </Card>

  <Card title="AI API" href="/api/ai" icon="wand-magic-sparkles">
    Generate AI responses from within your tools
  </Card>
</CardGroup>

## See Also

* [LuaTool](/api/luatool) - Creating tools
* [Jobs API](/api/jobs) - Schedule recurring provider calls
* [Environment API](/api/environment) - Configuration for your skill code
