> ## 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

> Raw REST calls to a connected provider through the agent's integration connection

`Integrations.passthrough` relays one HTTP call to a provider's own REST API through the [integration](/concepts/integrations) the agent is connected to. The platform adds the connection's credentials, your code never sees them, and the provider enforces the OAuth scopes the connection was granted. Every connected integration also gives the model a `<type>_passthrough` tool with the same fields, except that its body field is named `body`. Available in tools, jobs, webhooks, triggers, processors, and workflow code steps.

*Verified against lua-cli 3.33.0.*

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

## Quick example

Provider errors come back in `status`; only a call that never reached the provider throws.

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

const files = await Integrations.passthrough('github', {
  method: 'GET',
  path: 'repos/acme/app/pulls/42/files',
  query: { per_page: 100 },
});

if (files.status !== 200) {
  throw new Error(`GitHub answered ${files.status}`);
}
```

## Methods

### passthrough(integrationType, request)

Makes one provider API call through the agent's connection for that integration type.

```ts theme={null}
Integrations.passthrough(integrationType: string, request: IntegrationPassthroughRequest): Promise<IntegrationPassthroughResponse>
```

<ParamField path="integrationType" type="string" required>
  The integration type as `lua integrations connect --integration <type>` names it, for example `github`, `linear`, or `googlemail`.
</ParamField>

<ParamField path="request.method" type="IntegrationPassthroughMethod" required>
  One of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`.
</ParamField>

<ParamField path="request.path" type="string" required>
  The provider path after the provider's base URL, for example `repos/acme/app/pulls/42/files` on GitHub or `v1.0/me` on Microsoft Graph. At most 2,048 characters. A leading `/` is tolerated, a `?query` inside the path is merged into `query`, and `.` or `..` segments are rejected.
</ParamField>

<ParamField path="request.query" type="Record<string, string | number | boolean>">
  Query-string parameters, forwarded as given.
</ParamField>

<ParamField path="request.data" type="unknown">
  Request body. Objects and arrays are sent as JSON with `Content-Type: application/json` unless you set that header; a string is sent verbatim. Ignored on `GET` and `HEAD`.
</ParamField>

<ParamField path="request.headers" type="Record<string, string>">
  Extra request headers, forwarded lower-cased. `Authorization`, `Cookie`, `Host`, and hop-by-hop headers are dropped; the platform manages authentication.
</ParamField>

**Returns**

<ResponseField name="response" type="IntegrationPassthroughResponse">
  <Expandable title="properties">
    <ResponseField name="status" type="number">
      The provider's HTTP status, relayed as is. A 401 or 403 means the connection lacks a scope for the call; a 404, 422, 429, or 5xx is the provider's own answer. None of these throw.
    </ResponseField>

    <ResponseField name="headers" type="Record<string, string>">
      The provider's response headers with lower-cased names; `set-cookie` and `authorization` are removed.
    </ResponseField>

    <ResponseField name="data" type="unknown">
      Parsed JSON when the provider's content type says JSON, otherwise the raw body as a string.
    </ResponseField>
  </Expandable>
</ResponseField>

**Example**

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

export default class ReviewPullRequestTool implements LuaTool {
  name = 'review_pull_request';
  description = 'Post a review comment on a GitHub pull request';
  inputSchema = z.object({
    owner: z.string(),
    repo: z.string(),
    pullNumber: z.number(),
    comment: z.string(),
  });

  async execute(input: z.infer<typeof this.inputSchema>) {
    const review = await Integrations.passthrough('github', {
      method: 'POST',
      path: `repos/${input.owner}/${input.repo}/pulls/${input.pullNumber}/reviews`,
      data: { event: 'COMMENT', body: input.comment },
    });
    return { posted: review.status === 200, status: review.status };
  }
}
```

**Errors** — the call throws only when it never reached the provider. The error's `name` is `IntegrationPassthroughError`; it carries `code`, `statusCode` (alias `status`; `0` in `lua test` when the platform itself could not be reached), and, when the platform's own request to the integration layer failed, `vendor`, `vendorStatus`, `requestId`, and `retryAfterSeconds`. `retryAfterSeconds` is present only when a blind retry is safe, that is for `GET` and `HEAD`.

| `code`                        | Status | When                                                                                               |
| ----------------------------- | ------ | -------------------------------------------------------------------------------------------------- |
| `passthrough_invalid_request` | 400    | A method outside the six, an empty path, or `.` and `..` segments                                  |
| `passthrough_disabled`        | 403    | An admin turned passthrough off: `Passthrough is disabled for the '<type>' integration`            |
| `passthrough_no_connection`   | 404    | `Agent has no bound '<type>' connection`                                                           |
| `passthrough_rate_limited`    | 429    | `Passthrough rate limit exceeded (120 calls/min per agent) — retry shortly`                        |
| `VENDOR_UNAVAILABLE`          | 503    | The integration layer gave no answer; `legacyCode` is `passthrough_upstream_error` for one release |
| `passthrough_not_configured`  | 503    | The relay isn't configured on the platform; contact [support@heylua.ai](mailto:support@heylua.ai)  |

An answered provider 5xx or 429 is not thrown; classify `status` on the envelope yourself. Deployed agents can also throw `passthrough_unavailable` (`Integrations.passthrough is not available in this runtime`) when the runtime has no relay.

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

export async function lookupProfile() {
  try {
    const me = await Integrations.passthrough('microsoft', { method: 'GET', path: 'v1.0/me' });
    return { status: me.status, user: me.data };
  } catch (error: unknown) {
    const e = error as { name?: string; code?: string; retryAfterSeconds?: number; message: string };
    if (e.name === 'IntegrationPassthroughError' && e.code === 'passthrough_no_connection') {
      return { error: 'Connect Microsoft first: lua integrations connect --integration microsoft' };
    }
    return { error: e.message, retryAfterSeconds: e.retryAfterSeconds };
  }
}
```

Which connection answers: one bound to the agent, either connected with `--scope agent` or a personal `--scope user` connection attached to it. In `lua test`, a personal connection is used only when it is yours. Paused and disconnected connections are skipped; when several qualify, an active one wins, then the most recently updated. Every call, including a refused one, is audit-logged with identifiers, status, and latency; bodies are never logged.

## Types

`IntegrationPassthroughRequest`, `IntegrationPassthroughResponse`, and `IntegrationPassthroughMethod` are exported from `lua-cli`. The thrown error class is not; branch on `name` and `code`.

## See also

* [Call an integration's API directly](/integrations/passthrough) — how-to with relay errors and a local test
* [Connect an integration](/integrations/connect) — `lua integrations connect`, `--scope`, and `--scopes`
* [About integrations](/concepts/integrations) — integrations, MCP tools, and passthrough
* [`lua integrations`](/reference/cli/integrations) — list, connect, and disconnect connections
