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

# Call an integration's API directly

> Use Integrations.passthrough in a tool to reach provider endpoints the integration's MCP tools don't cover

After this guide, a tool of yours calls a connected provider's REST or GraphQL API through the agent's own connection, with the platform adding the credential. Use it when the [integration's tools](/integrations/mcp) don't expose the endpoint or you need the raw body; for a system with no integration, write a tool with `fetch` and your own key (see [Call your API](/build/call-your-api)).

*Verified against lua-cli 3.33.0.*

**Before you begin**

* A connection of that type on the agent (see [Connect a Unified.to integration](/integrations/connect)).
* The provider's own API documentation: `path` is relative to the provider's API base URL and is relayed untouched.

<Steps>
  <Step title="Write the tool">
    `Integrations.passthrough(type, { method, path, query?, data?, headers? })` returns `{ status, headers, data }`, where `data` is parsed JSON when the provider answered JSON and the raw string otherwise. A provider error such as a 403 for a missing scope comes back in `status`, not as an exception.

    ```ts src/skills/tools/SearchLinearIssuesTool.ts theme={null}
    import type { LuaTool } from 'lua-cli';
    import { Integrations } from 'lua-cli';
    import { z } from 'zod';

    export default class SearchLinearIssuesTool implements LuaTool {
      name = 'search_linear_issues';
      description = 'Full-text search of Linear issues. Use when the user quotes words from an issue title.';
      inputSchema = z.object({
        term: z.string().min(2).describe('Words to search for in issue titles and descriptions'),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        const res = await Integrations.passthrough('linear', {
          method: 'POST',
          path: 'graphql',
          data: {
            query:
              'query Search($term: String!) { searchIssues(term: $term, first: 5) { nodes { identifier title state { name } } } }',
            variables: { term: input.term },
          },
        });
        // Provider errors arrive in `status`, not as exceptions; 403 = missing scope.
        if (res.status !== 200) throw new Error(`Linear answered ${res.status}`);
        return res.data;
      }
    }
    ```

    `method` is one of `GET`, `POST`, `PUT`, `PATCH`, `DELETE`, `HEAD`; an object `data` is sent as JSON, a string verbatim; `query` is appended to the URL; `headers` are forwarded except `Authorization`, `Cookie`, and hop-by-hop headers, which the relay owns.
  </Step>

  <Step title="Handle relay errors">
    The call throws only when the relay itself refuses: no bound connection, passthrough disabled for the type, the per-agent rate limit, an invalid request, or the vendor not answering. The error's `name` is `IntegrationPassthroughError` and `code` says why; the class isn't exported from `'lua-cli'`, so narrow on those two fields in the same tool.

    ```ts src/skills/tools/SearchLinearIssuesTool.ts highlight={5,15,27-39} theme={null}
    import type { LuaTool } from 'lua-cli';
    import { Integrations } from 'lua-cli';
    import { z } from 'zod';

    type PassthroughFailure = Error & { code?: string; retryAfterSeconds?: number; requestId?: string };

    export default class SearchLinearIssuesTool implements LuaTool {
      name = 'search_linear_issues';
      description = 'Full-text search of Linear issues. Use when the user quotes words from an issue title.';
      inputSchema = z.object({
        term: z.string().min(2).describe('Words to search for in issue titles and descriptions'),
      });

      async execute(input: z.infer<typeof this.inputSchema>) {
        try {
          const res = await Integrations.passthrough('linear', {
            method: 'POST',
            path: 'graphql',
            data: {
              query:
                'query Search($term: String!) { searchIssues(term: $term, first: 5) { nodes { identifier title state { name } } } }',
              variables: { term: input.term },
            },
          });
          if (res.status !== 200) throw new Error(`Linear answered ${res.status}`);
          return res.data;
        } catch (err) {
          const failure = err as PassthroughFailure;
          // The relay refused before reaching Linear; `code` says why.
          if (failure.name === 'IntegrationPassthroughError') {
            if (failure.code === 'passthrough_no_connection') {
              return { issues: [], reason: 'Linear is not connected to this agent' };
            }
            if (failure.code === 'VENDOR_UNAVAILABLE' && failure.retryAfterSeconds) {
              return { issues: [], reason: `Retry in ${failure.retryAfterSeconds}s` };
            }
          }
          throw err;
        }
      }
    }
    ```

    Codes: `passthrough_no_connection` (404), `passthrough_disabled` (403), `passthrough_rate_limited` (429), `passthrough_invalid_request` (400), and `VENDOR_UNAVAILABLE` (503, when Unified.to didn't answer; `retryAfterSeconds` is set only for a `GET` or `HEAD`, and `vendor`, `vendorStatus`, and `requestId` identify the fault). `statusCode` carries the relay's HTTP status. A provider that answered 5xx or 429 isn't a relay error: read `status` on the envelope.
  </Step>

  <Step title="Register and test locally">
    Add the tool to a skill on the agent (`integrations-samples` in this guide), then run it. `lua test` sends the call through the platform with your CLI credential, so the agent's real connection is used.

    ```bash theme={null}
    lua test skill --name search_linear_issues --input '{"term":"docs"}' --json
    ```

    ```json Output theme={null}
    {
      "data": {
        "searchIssues": {
          "nodes": [
            {
              "identifier": "IMP-404",
              "title": "Template: Doc Keeper",
              "state": {
                "name": "Backlog"
              }
            },
            {
              "identifier": "PRO-44",
              "title": "Write design system getting-started docs",
              "state": {
                "name": "Backlog"
              }
            },
    …
          ]
        }
      }
    }
    ```
  </Step>

  <Step title="Release">
    Push the skill, snapshot the agent, and promote the version (see [Releasing](/ship/releasing)).

    ```bash theme={null}
    lua push skill --name integrations-samples --set-version 1.0.0
    lua version create
    lua version promote <n>
    ```
  </Step>
</Steps>

## Options you may need

### Which connection answers

Under `lua test` the call carries your credential: it resolves the agent's own connections, and a personal connection only when you own it. Deployed code resolves any connection bound to the agent, a mounted personal connection included, on behalf of whichever end user is in the conversation. The per-end-user filter on [About Spaces](/concepts/spaces) applies to the tools a consulted member carries, the model's `<type>_passthrough` tool among them, which is pinned to the connection mounted for that turn; `Integrations.passthrough` from your code isn't filtered that way.

### Limits

* 120 calls per minute per agent, in a sliding window, counting calls from your code and from the model's `<type>_passthrough` tool alike; above that the call throws with code `passthrough_rate_limited` (HTTP 429) and no retry-after value. A provider's own 429 comes back in `status` instead.
* `Integrations.passthrough` takes an integration type, not a connection ID; with several connections of one type, the active, most recently updated one answers.
* Deployed, the relay abandons a call after 60 seconds end to end, so a provider that takes longer fails the call with a thrown error.
* `path` is at most 2,048 characters. A `?` inside it is merged with `query`, and `..` segments are rejected.
* Scopes are enforced by the provider: a call outside the connection's grant returns the provider's 401 or 403 in `status`. Change the grant with `lua integrations update --scopes`.
* The platform can switch passthrough off per integration type, which yields `passthrough_disabled`.
* Every call is logged by the platform for support and abuse review, with the integration, connection, method, path, status, and latency and never bodies or headers; the log isn't readable through the CLI or API.

## If it isn't working

<AccordionGroup>
  <Accordion title="Agent has no bound 'linear' connection">
    The agent has no connection of that type, or the type is spelled differently from `lua integrations available`. Connect it, and use the type in parentheses from that list.
  </Accordion>

  <Accordion title="Passthrough rate limit exceeded (120 calls/min per agent) — retry shortly">
    The tool loops or a job fans out too fast. Batch the calls or wait for the window to pass; the limit is per agent, not per tool, and the error carries no retry-after value.
  </Accordion>

  <Accordion title="status is 401 or 403 in the envelope">
    The provider refused the credential or the scope. Re-authorize with `lua integrations update --connection-id <id> --scopes all`, or add the scope the endpoint needs.
  </Accordion>
</AccordionGroup>

## Next steps

<Columns cols={2}>
  <Card title="Integrations reference" href="/reference/sdk/integrations">Request and response fields, and the error shape.</Card>
  <Card title="Manage integration MCP tools" href="/integrations/mcp">The provisioned tools, before you write your own.</Card>
  <Card title="Add a tool" href="/build/add-a-tool">Tool anatomy, input schemas, and registration.</Card>
  <Card title="About integrations" href="/concepts/integrations">What a connection provides and who owns it.</Card>
</Columns>
