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

# API Integration

> Build custom documentation search into your apps, bots, and workflows with the ULPI REST API

# Build Documentation Search Into Your Applications

**Your team needs documentation search in places ULPI doesn't reach:**

* Internal documentation portals
* Slack bots for instant doc lookups
* IDE extensions
* CLI tools
* Admin dashboards
* Custom AI agents

**The ULPI REST API lets you integrate semantic documentation search anywhere.**

**This guide shows you:**

* 🔌 How to authenticate and make your first API call (2 minutes)
* 💡 Real-world integration examples (Slack bots, portals, CLI tools)
* 🛠️ Production-ready code in JavaScript, Python, PHP, Go
* ⚡ Best practices for caching, rate limits, and error handling
* 📊 Performance optimization strategies

<Info>
  **Just want to use ULPI with Claude/Cursor?** See [Getting Started](/documentation/getting-started) for MCP setup instead.

  **Building something custom?** This guide is for you.
</Info>

***

## Why Use the API?

**The MCP server is great for AI assistants, but the API unlocks custom integrations:**

<CardGroup cols={2}>
  <Card title="Custom Search UIs" icon="window" color="#10b981">
    **Build internal documentation portals:**

    * Company-branded search interface
    * Advanced filtering and faceting
    * Usage analytics and tracking
    * Integration with SSO/auth

    **Example:** Acme Corp's internal dev portal with ULPI search
  </Card>

  <Card title="Slack/Discord Bots" icon="robot" color="#3b82f6">
    **Instant doc lookups in chat:**

    * `/docs deploy to production` → Get deployment guide
    * No context switching
    * Share results with team
    * Track common questions

    **Example:** Engineering Slack bot with 500+ daily searches
  </Card>

  <Card title="CLI Tools" icon="terminal" color="#8b5cf6">
    **Terminal-based documentation search:**

    * `docs auth` → Find auth docs
    * Integrate with shell scripts
    * CI/CD pipeline helpers
    * Developer productivity boost

    **Example:** `company-docs` CLI tool for all repos
  </Card>

  <Card title="Custom AI Agents" icon="brain" color="#f59e0b">
    **Build specialized AI assistants:**

    * Customer support chatbots
    * Onboarding assistants
    * Technical troubleshooting bots
    * Integration with your AI stack

    **Example:** Support bot that searches internal docs first
  </Card>
</CardGroup>

**Plus:** Dashboards, IDE plugins, mobile apps, workflow automation, and more.

***

## Quick Start: Your First API Call

**Get up and running in 2 minutes:**

<Steps>
  <Step title="Get Your API Key">
    **Generate an API key:**

    1. Go to [app.ulpi.io/api-keys](https://app.ulpi.io/api-keys)
    2. Click **Create API Key**
    3. Name: "API Integration Test"
    4. Environment: `live` (production)
    5. Copy the key (starts with `ulpi_live_sk_...`)

    <Warning>
      **Save your API key immediately!** It's only shown once.

      Store in password manager or environment variable.
    </Warning>
  </Step>

  <Step title="Make Your First Request">
    **Test with cURL:**

    ```bash theme={null}
    curl -X POST https://api.ulpi.io/api/v1/documentation/search \
      -H "Authorization: Bearer ulpi_live_YOUR_KEY_HERE" \
      -H "Content-Type: application/json" \
      -d '{
        "query": "How do I deploy to production?",
        "limit": 3
      }'
    ```

    **Replace `ulpi_live_YOUR_KEY_HERE`** with your actual API key.
  </Step>

  <Step title="View Results">
    **You'll get JSON response:**

    ```json theme={null}
    {
      "results": [
        {
          "content": "To deploy to production:\n\n1. Run tests...",
          "file": "docs/deployment.md",
          "repository": "backend-api",
          "branch": "main",
          "url": "https://github.com/org/repo/blob/main/docs/deployment.md#L42",
          "score": 0.94,
          "last_updated": "2025-01-10T14:23:00Z"
        }
      ],
      "meta": {
        "total": 12,
        "query_time_ms": 45,
        "cached": false
      }
    }
    ```

    **Success!** You just searched your documentation via API.
  </Step>

  <Step title="Integrate Into Your App">
    **Choose your language:**

    * [JavaScript/TypeScript](#javascript-sdk)
    * [Python](#python-sdk)
    * [PHP](#php-sdk)
    * [Go](#go-sdk)

    **Or:** Use the [REST API directly](#api-reference)
  </Step>
</Steps>

**Total time:** 2 minutes from API key to first results

***

## Real-World Integration Examples

**See how teams use the ULPI API:**

<AccordionGroup>
  <Accordion title="🤖 Slack Bot for Documentation Lookup" icon="slack">
    **Use case:** Engineers ask `/docs <question>` in Slack, bot returns relevant docs

    **Benefits:**

    * No context switching (stay in Slack)
    * Share results with team instantly
    * Track most-asked questions
    * Reduce Slack interruptions ("Where's the deploy doc?")

    **Implementation:**

    <Tabs>
      <Tab title="Bolt.js (JavaScript)">
        ```javascript theme={null}
        // slack-bot.js
        const { App } = require('@slack/bolt');

        const app = new App({
          token: process.env.SLACK_BOT_TOKEN,
          signingSecret: process.env.SLACK_SIGNING_SECRET
        });

        // /docs command
        app.command('/docs', async ({ command, ack, respond }) => {
          await ack();

          // Search ULPI
          const results = await fetch('https://api.ulpi.io/api/v1/documentation/search', {
            method: 'POST',
            headers: {
              'Authorization': `Bearer ${process.env.ULPI_API_KEY}`,
              'Content-Type': 'application/json'
            },
            body: JSON.stringify({
              query: command.text,
              limit: 3
            })
          }).then(r => r.json());

          // Format for Slack
          await respond({
            text: `Found ${results.meta.total} results for "${command.text}":`,
            blocks: results.results.map(r => ({
              type: 'section',
              text: {
                type: 'mrkdwn',
                text: `*${r.file}* (${r.repository})\n${r.content.substring(0, 300)}...\n<${r.url}|View on GitHub>`
              }
            }))
          });
        });

        app.start(3000);
        ```

        **Usage in Slack:**

        ```
        /docs how to deploy to production
        /docs authentication with JWT
        /docs troubleshoot 500 errors
        ```
      </Tab>

      <Tab title="Python (slack_sdk)">
        ```python theme={null}
        # slack_bot.py
        from slack_bolt import App
        from slack_bolt.adapter.socket_mode import SocketModeHandler
        import requests
        import os

        app = App(token=os.environ["SLACK_BOT_TOKEN"])

        @app.command("/docs")
        def handle_docs_command(ack, command, respond):
            ack()

            query = command['text']

            # Search ULPI
            response = requests.post(
                'https://api.ulpi.io/api/v1/documentation/search',
                headers={
                    'Authorization': f'Bearer {os.environ["ULPI_API_KEY"]}',
                    'Content-Type': 'application/json'
                },
                json={'query': query, 'limit': 3}
            )

            results = response.json()

            # Format response
            blocks = [{
                'type': 'section',
                'text': {
                    'type': 'mrkdwn',
                    'text': f"*{r['file']}* ({r['repository']})\n{r['content'][:300]}...\n<{r['url']}|View on GitHub>"
                }
            } for r in results['results']]

            respond(
                text=f"Found {results['meta']['total']} results for \"{query}\":",
                blocks=blocks
            )

        if __name__ == "__main__":
            handler = SocketModeHandler(app, os.environ["SLACK_APP_TOKEN"])
            handler.start()
        ```
      </Tab>
    </Tabs>

    **Deployment:**

    * Deploy to Heroku, AWS Lambda, or any Node/Python host
    * Set environment variables: `SLACK_BOT_TOKEN`, `ULPI_API_KEY`
    * Invite bot to Slack channels

    **Result:** Instant documentation lookup without leaving Slack
  </Accordion>

  <Accordion title="🌐 Internal Documentation Portal" icon="window">
    **Use case:** Company-branded docs website with ULPI search

    **Benefits:**

    * Centralized documentation access
    * Advanced search with filters
    * Usage analytics (what's searched most)
    * SSO integration

    **Implementation (Next.js):**

    <Tabs>
      <Tab title="API Route">
        ```typescript theme={null}
        // pages/api/search.ts
        import type { NextApiRequest, NextApiResponse } from 'next';

        export default async function handler(
          req: NextApiRequest,
          res: NextApiResponse
        ) {
          if (req.method !== 'POST') {
            return res.status(405).json({ error: 'Method not allowed' });
          }

          const { query, repository, limit = 10 } = req.body;

          // Call ULPI API (server-side keeps API key secure)
          const response = await fetch(
            'https://api.ulpi.io/api/v1/documentation/search',
            {
              method: 'POST',
              headers: {
                'Authorization': `Bearer ${process.env.ULPI_API_KEY}`,
                'Content-Type': 'application/json'
              },
              body: JSON.stringify({ query, repository, limit })
            }
          );

          const data = await response.json();

          // Log for analytics
          await logSearch(query, data.meta.total);

          res.status(200).json(data);
        }
        ```
      </Tab>

      <Tab title="Search Component">
        ```typescript theme={null}
        // components/SearchBar.tsx
        import { useState } from 'react';

        export default function SearchBar() {
          const [query, setQuery] = useState('');
          const [results, setResults] = useState([]);
          const [loading, setLoading] = useState(false);

          const handleSearch = async (q: string) => {
            setLoading(true);

            const res = await fetch('/api/search', {
              method: 'POST',
              headers: { 'Content-Type': 'application/json' },
              body: JSON.stringify({ query: q })
            });

            const data = await res.json();
            setResults(data.results);
            setLoading(false);
          };

          return (
            <div className="search-container">
              <input
                type="text"
                placeholder="Search documentation..."
                value={query}
                onChange={(e) => {
                  setQuery(e.target.value);
                  if (e.target.value.length > 2) {
                    handleSearch(e.target.value);
                  }
                }}
              />

              {loading && <Spinner />}

              <div className="results">
                {results.map((result) => (
                  <SearchResult key={result.url} result={result} />
                ))}
              </div>
            </div>
          );
        }
        ```
      </Tab>

      <Tab title="Result Display">
        ```typescript theme={null}
        // components/SearchResult.tsx
        interface SearchResultProps {
          result: {
            file: string;
            repository: string;
            content: string;
            url: string;
            score: number;
            last_updated: string;
          };
        }

        export default function SearchResult({ result }: SearchResultProps) {
          return (
            <div className="result-card">
              <div className="result-header">
                <h3>{result.file}</h3>
                <span className="repo-badge">{result.repository}</span>
                <span className="score">
                  {Math.round(result.score * 100)}% match
                </span>
              </div>

              <p className="result-content">
                {result.content.substring(0, 300)}...
              </p>

              <div className="result-footer">
                <a href={result.url} target="_blank" rel="noopener">
                  View on GitHub →
                </a>
                <span className="last-updated">
                  Updated {new Date(result.last_updated).toLocaleDateString()}
                </span>
              </div>
            </div>
          );
        }
        ```
      </Tab>
    </Tabs>

    **Features to add:**

    * Repository filter dropdown
    * Branch selector
    * Search history
    * Bookmarks
    * Analytics dashboard

    **Example:** `docs.acmecorp.com` - internal portal with 1,000+ daily searches
  </Accordion>

  <Accordion title="🖥️ CLI Tool for Terminal Search" icon="terminal">
    **Use case:** Search docs from terminal without opening browser

    **Benefits:**

    * Stay in terminal workflow
    * Script-friendly (use in automation)
    * Fast lookups during coding
    * Pipe output to other commands

    **Implementation (Python Click):**

    ```python theme={null}
    # company_docs.py
    import click
    import requests
    import os
    from rich.console import Console
    from rich.markdown import Markdown

    console = Console()
    ULPI_API_KEY = os.getenv('ULPI_API_KEY')

    def search_docs(query, repo=None, limit=5):
        """Search ULPI documentation."""
        response = requests.post(
            'https://api.ulpi.io/api/v1/documentation/search',
            headers={
                'Authorization': f'Bearer {ULPI_API_KEY}',
                'Content-Type': 'application/json'
            },
            json={
                'query': query,
                'repository': repo,
                'limit': limit
            }
        )
        response.raise_for_status()
        return response.json()

    @click.group()
    def cli():
        """Company documentation search CLI."""
        pass

    @cli.command()
    @click.argument('query')
    @click.option('--repo', '-r', help='Filter by repository')
    @click.option('--limit', '-n', default=5, help='Number of results')
    @click.option('--open', '-o', is_flag=True, help='Open first result in browser')
    def search(query, repo, limit, open):
        """Search documentation."""
        console.print(f"[bold]Searching for:[/bold] {query}\n")

        results = search_docs(query, repo, limit)

        console.print(f"[green]Found {results['meta']['total']} results[/green] "
                     f"(showing {len(results['results'])})\n")

        for i, result in enumerate(results['results'], 1):
            console.print(f"[bold cyan]{i}. {result['file']}[/bold cyan] "
                         f"({result['repository']})")
            console.print(f"   {result['content'][:200]}...")
            console.print(f"   [dim]{result['url']}[/dim]\n")

        if open and results['results']:
            import webbrowser
            webbrowser.open(results['results'][0]['url'])

    @cli.command()
    def interactive():
        """Interactive search mode."""
        while True:
            query = click.prompt('\nSearch (or "q" to quit)')
            if query.lower() == 'q':
                break

            results = search_docs(query, limit=3)

            for i, result in enumerate(results['results'], 1):
                console.print(f"\n{i}. [bold]{result['file']}[/bold]")
                console.print(Markdown(result['content'][:300]))

    if __name__ == '__main__':
        cli()
    ```

    **Installation:**

    ```bash theme={null}
    # Install as global command
    pip install -e .

    # Or create alias
    alias docs="python /path/to/company_docs.py"
    ```

    **Usage:**

    ```bash theme={null}
    # Search
    docs search "deploy to production"

    # Filter by repo
    docs search "authentication" --repo backend-api

    # Open first result in browser
    docs search "troubleshoot" --open

    # Interactive mode
    docs interactive
    ```

    **Result:** Documentation lookup without leaving terminal
  </Accordion>

  <Accordion title="📊 Dashboard Widget" icon="chart-line">
    **Use case:** Quick doc access in admin dashboard

    **Benefits:**

    * Context-aware quick links
    * Common runbooks one click away
    * New hire onboarding helper
    * Embedded in existing tools

    **Implementation (React):**

    ```typescript theme={null}
    // DocsWidget.tsx
    import { useState, useEffect } from 'react';

    const quickTopics = [
      { label: 'Deploy to Prod', query: 'deployment production' },
      { label: 'API Auth', query: 'API authentication' },
      { label: 'Troubleshoot 500s', query: 'troubleshoot 500 errors' },
      { label: 'Local Setup', query: 'local development setup' }
    ];

    export function DocsWidget() {
      const [selectedDoc, setSelectedDoc] = useState(null);

      const quickSearch = async (query) => {
        const res = await fetch('/api/search', {
          method: 'POST',
          headers: { 'Content-Type': 'application/json' },
          body: JSON.stringify({ query, limit: 1 })
        });

        const data = await res.json();
        setSelectedDoc(data.results[0]);
      };

      return (
        <div className="docs-widget">
          <h3>Quick Docs</h3>

          <div className="quick-links">
            {quickTopics.map(topic => (
              <button
                key={topic.query}
                onClick={() => quickSearch(topic.query)}
                className="quick-link-btn"
              >
                {topic.label}
              </button>
            ))}
          </div>

          {selectedDoc && (
            <div className="doc-preview">
              <h4>{selectedDoc.file}</h4>
              <p>{selectedDoc.content.substring(0, 200)}...</p>
              <a href={selectedDoc.url} target="_blank">
                View full doc →
              </a>
            </div>
          )}
        </div>
      );
    }
    ```

    **Use in dashboard:**

    * Employee onboarding page
    * Developer tools section
    * Help/support sidebar
    * Context-sensitive (show relevant docs per page)
  </Accordion>

  <Accordion title="🤖 Custom AI Assistant" icon="brain">
    **Use case:** Customer support chatbot that searches internal docs

    **Benefits:**

    * Accurate answers from YOUR documentation
    * Reduces support ticket volume
    * 24/7 availability
    * Cites sources (links to docs)

    **Implementation (LangChain + OpenAI):**

    ```python theme={null}
    # support_bot.py
    from langchain.chat_models import ChatOpenAI
    from langchain.agents import Tool, initialize_agent
    import requests
    import os

    def search_documentation(query: str) -> str:
        """Search company documentation."""
        response = requests.post(
            'https://api.ulpi.io/api/v1/documentation/search',
            headers={
                'Authorization': f'Bearer {os.environ["ULPI_API_KEY"]}',
                'Content-Type': 'application/json'
            },
            json={'query': query, 'limit': 3}
        )

        results = response.json()['results']

        # Format for LLM
        docs_text = "\n\n".join([
            f"From {r['file']}:\n{r['content']}\nSource: {r['url']}"
            for r in results
        ])

        return docs_text

    # Create tool for LangChain
    doc_search_tool = Tool(
        name="SearchDocumentation",
        func=search_documentation,
        description="Search company documentation for technical information, "
                    "deployment guides, troubleshooting steps, etc."
    )

    # Initialize agent
    llm = ChatOpenAI(temperature=0, model="gpt-4")
    agent = initialize_agent(
        tools=[doc_search_tool],
        llm=llm,
        agent="zero-shot-react-description",
        verbose=True
    )

    # Use agent
    response = agent.run(
        "How do I deploy our backend API to production?"
    )

    print(response)
    # Agent automatically calls search_documentation tool,
    # gets relevant docs, and formulates answer with citations
    ```

    **Deploy as:**

    * Slack bot
    * Website chat widget
    * Support ticket assistant
    * Email autoresponder
  </Accordion>
</AccordionGroup>

***

## API Reference

**Complete API endpoint documentation:**

### Authentication

**All requests require API key in Authorization header:**

```bash theme={null}
Authorization: Bearer ulpi_live_sk_abc123...
```

**API key formats:**

* `ulpi_live_sk_...` - Production environment
* `ulpi_test_sk_...` - Testing environment

**Generate keys:** [app.ulpi.io/api-keys](https://app.ulpi.io/api-keys)

***

### POST /api/v1/documentation/search

**Search across indexed documentation**

**Base URL:** `https://api.ulpi.io/api/v1/documentation/search`

**Request Body:**

```json theme={null}
{
  "query": "How do I deploy to production?",
  "repository": "backend-api",
  "branch": "main",
  "limit": 10
}
```

**Parameters:**

| Parameter    | Type    | Required | Description                   | Default        |
| ------------ | ------- | -------- | ----------------------------- | -------------- |
| `query`      | string  | ✅ Yes    | Natural language search query | -              |
| `repository` | string  | ❌ No     | Filter to specific repository | All repos      |
| `branch`     | string  | ❌ No     | Search specific branch        | Default branch |
| `limit`      | integer | ❌ No     | Max results (1-20)            | 10             |

**Query Guidelines:**

* **Max length:** 500 characters
* **Natural language:** "How do I..." works better than keywords
* **Be specific:** "deploy to AWS production" better than "deploy"

**Response:**

```json theme={null}
{
  "results": [
    {
      "content": "Full text excerpt from documentation...",
      "file": "docs/deployment.md",
      "repository": "backend-api",
      "branch": "main",
      "url": "https://github.com/org/repo/blob/main/docs/deployment.md#L42",
      "score": 0.94,
      "last_updated": "2025-01-15T10:30:00Z"
    }
  ],
  "meta": {
    "total": 12,
    "query_time_ms": 45,
    "cached": false
  }
}
```

**Response Fields:**

| Field                    | Type    | Description                                      |
| ------------------------ | ------- | ------------------------------------------------ |
| `results[]`              | array   | Array of search results (sorted by relevance)    |
| `results[].content`      | string  | Documentation excerpt (most relevant section)    |
| `results[].file`         | string  | File path relative to repository root            |
| `results[].repository`   | string  | Repository name                                  |
| `results[].branch`       | string  | Git branch                                       |
| `results[].url`          | string  | Direct link to file on GitHub/GitLab             |
| `results[].score`        | float   | Relevance score (0.0-1.0, higher = better match) |
| `results[].last_updated` | string  | ISO 8601 timestamp of last file update           |
| `meta.total`             | integer | Total results found (may be more than returned)  |
| `meta.query_time_ms`     | integer | Query execution time in milliseconds             |
| `meta.cached`            | boolean | Whether results were served from cache           |

***

## Code Examples by Language

<Tabs>
  <Tab title="JavaScript / TypeScript">
    **Node.js implementation:**

    ```typescript theme={null}
    // ulpi-client.ts
    interface SearchOptions {
      repository?: string;
      branch?: string;
      limit?: number;
    }

    interface SearchResult {
      content: string;
      file: string;
      repository: string;
      branch: string;
      url: string;
      score: number;
      last_updated: string;
    }

    interface SearchResponse {
      results: SearchResult[];
      meta: {
        total: number;
        query_time_ms: number;
        cached: boolean;
      };
    }

    class ULPIClient {
      private apiKey: string;
      private baseUrl = 'https://api.ulpi.io/api/v1';

      constructor(apiKey: string) {
        this.apiKey = apiKey;
      }

      async searchDocumentation(
        query: string,
        options: SearchOptions = {}
      ): Promise<SearchResponse> {
        const response = await fetch(`${this.baseUrl}/documentation/search`, {
          method: 'POST',
          headers: {
            'Authorization': `Bearer ${this.apiKey}`,
            'Content-Type': 'application/json',
          },
          body: JSON.stringify({
            query,
            repository: options.repository,
            branch: options.branch,
            limit: options.limit || 10,
          }),
        });

        if (!response.ok) {
          const error = await response.json();
          throw new Error(`ULPI API error: ${error.message}`);
        }

        return response.json();
      }
    }

    // Usage
    const ulpi = new ULPIClient(process.env.ULPI_API_KEY!);

    const results = await ulpi.searchDocumentation(
      'How to deploy to production?',
      { repository: 'backend-api', limit: 5 }
    );

    results.results.forEach(result => {
      console.log(`${result.file}: ${result.content.substring(0, 100)}...`);
      console.log(`Score: ${result.score}, URL: ${result.url}\n`);
    });
    ```
  </Tab>

  <Tab title="Python">
    **Python implementation with requests:**

    ```python theme={null}
    # ulpi_client.py
    import requests
    from typing import Optional, List, Dict
    import os

    class ULPIClient:
        """Client for ULPI Documentation API."""

        def __init__(self, api_key: str):
            self.api_key = api_key
            self.base_url = 'https://api.ulpi.io/api/v1'

        def search_documentation(
            self,
            query: str,
            repository: Optional[str] = None,
            branch: Optional[str] = None,
            limit: int = 10
        ) -> Dict:
            """
            Search documentation across repositories.

            Args:
                query: Natural language search query
                repository: Optional repository filter
                branch: Optional branch filter
                limit: Max results (1-20)

            Returns:
                Dict with 'results' and 'meta' keys

            Raises:
                requests.HTTPError: If API request fails
            """
            response = requests.post(
                f'{self.base_url}/documentation/search',
                headers={
                    'Authorization': f'Bearer {self.api_key}',
                    'Content-Type': 'application/json'
                },
                json={
                    'query': query,
                    'repository': repository,
                    'branch': branch,
                    'limit': limit
                }
            )

            response.raise_for_status()
            return response.json()

    # Usage
    ulpi = ULPIClient(os.getenv('ULPI_API_KEY'))

    results = ulpi.search_documentation(
        'How to deploy to production?',
        repository='backend-api',
        limit=5
    )

    for result in results['results']:
        print(f"{result['file']} (score: {result['score']:.2f})")
        print(f"{result['content'][:150]}...")
        print(f"{result['url']}\n")

    print(f"Found {results['meta']['total']} results in {results['meta']['query_time_ms']}ms")
    ```
  </Tab>

  <Tab title="PHP">
    **PHP implementation with Laravel HTTP:**

    ```php theme={null}
    <?php
    // app/Services/UlpiClient.php

    namespace App\Services;

    use Illuminate\Support\Facades\Http;
    use Illuminate\Support\Facades\Cache;

    class UlpiClient
    {
        private string $apiKey;
        private string $baseUrl = 'https://api.ulpi.io/api/v1';

        public function __construct(string $apiKey)
        {
            $this->apiKey = $apiKey;
        }

        /**
         * Search documentation
         */
        public function searchDocumentation(
            string $query,
            ?string $repository = null,
            ?string $branch = null,
            int $limit = 10
        ): array {
            // Cache key
            $cacheKey = "ulpi:search:" . md5(json_encode([
                'query' => $query,
                'repository' => $repository,
                'branch' => $branch,
                'limit' => $limit
            ]));

            // Check cache (5 minutes)
            return Cache::remember($cacheKey, 300, function () use (
                $query, $repository, $branch, $limit
            ) {
                $response = Http::withHeaders([
                    'Authorization' => "Bearer {$this->apiKey}",
                ])->post("{$this->baseUrl}/documentation/search", [
                    'query' => $query,
                    'repository' => $repository,
                    'branch' => $branch,
                    'limit' => $limit,
                ]);

                if ($response->failed()) {
                    throw new \Exception(
                        "ULPI API error: " . $response->json('message')
                    );
                }

                return $response->json();
            });
        }

        /**
         * Get first result for quick lookups
         */
        public function quickSearch(string $query): ?array
        {
            $results = $this->searchDocumentation($query, limit: 1);
            return $results['results'][0] ?? null;
        }
    }

    // Usage (in controller or service)
    $ulpi = new UlpiClient(config('services.ulpi.api_key'));

    $results = $ulpi->searchDocumentation(
        'How to deploy to production?',
        repository: 'backend-api',
        limit: 5
    );

    foreach ($results['results'] as $result) {
        echo "{$result['file']}: {$result['content']}\n";
        echo "{$result['url']}\n\n";
    }
    ```
  </Tab>

  <Tab title="Go">
    **Go implementation:**

    ```go theme={null}
    // ulpi/client.go
    package ulpi

    import (
        "bytes"
        "encoding/json"
        "fmt"
        "net/http"
        "time"
    )

    type Client struct {
        APIKey  string
        BaseURL string
        client  *http.Client
    }

    type SearchOptions struct {
        Repository string `json:"repository,omitempty"`
        Branch     string `json:"branch,omitempty"`
        Limit      int    `json:"limit,omitempty"`
    }

    type SearchResult struct {
        Content     string    `json:"content"`
        File        string    `json:"file"`
        Repository  string    `json:"repository"`
        Branch      string    `json:"branch"`
        URL         string    `json:"url"`
        Score       float64   `json:"score"`
        LastUpdated time.Time `json:"last_updated"`
    }

    type SearchResponse struct {
        Results []SearchResult `json:"results"`
        Meta    struct {
            Total       int  `json:"total"`
            QueryTimeMs int  `json:"query_time_ms"`
            Cached      bool `json:"cached"`
        } `json:"meta"`
    }

    func NewClient(apiKey string) *Client {
        return &Client{
            APIKey:  apiKey,
            BaseURL: "https://api.ulpi.io/api/v1",
            client:  &http.Client{Timeout: 30 * time.Second},
        }
    }

    func (c *Client) SearchDocumentation(
        query string,
        opts *SearchOptions,
    ) (*SearchResponse, error) {
        if opts == nil {
            opts = &SearchOptions{Limit: 10}
        }

        payload := map[string]interface{}{
            "query":      query,
            "repository": opts.Repository,
            "branch":     opts.Branch,
            "limit":      opts.Limit,
        }

        jsonData, err := json.Marshal(payload)
        if err != nil {
            return nil, err
        }

        req, err := http.NewRequest(
            "POST",
            fmt.Sprintf("%s/documentation/search", c.BaseURL),
            bytes.NewBuffer(jsonData),
        )
        if err != nil {
            return nil, err
        }

        req.Header.Set("Authorization", fmt.Sprintf("Bearer %s", c.APIKey))
        req.Header.Set("Content-Type", "application/json")

        resp, err := c.client.Do(req)
        if err != nil {
            return nil, err
        }
        defer resp.Body.Close()

        if resp.StatusCode != http.StatusOK {
            return nil, fmt.Errorf("API error: %s", resp.Status)
        }

        var searchResp SearchResponse
        if err := json.NewDecoder(resp.Body).Decode(&searchResp); err != nil {
            return nil, err
        }

        return &searchResp, nil
    }

    // Usage
    package main

    import (
        "fmt"
        "log"
        "os"
    )

    func main() {
        client := ulpi.NewClient(os.Getenv("ULPI_API_KEY"))

        results, err := client.SearchDocumentation(
            "How to deploy to production?",
            &ulpi.SearchOptions{
                Repository: "backend-api",
                Limit:      5,
            },
        )
        if err != nil {
            log.Fatal(err)
        }

        for _, result := range results.Results {
            fmt.Printf("%s (score: %.2f)\n", result.File, result.Score)
            fmt.Printf("%s\n", result.Content[:150])
            fmt.Printf("%s\n\n", result.URL)
        }

        fmt.Printf("Found %d results in %dms\n",
            results.Meta.Total,
            results.Meta.QueryTimeMs)
    }
    ```
  </Tab>
</Tabs>

***

## Rate Limits & Quotas

**API limits by plan:**

| Plan             | Requests/Min | Requests/Hour | Requests/Day | Burst |
| ---------------- | ------------ | ------------- | ------------ | ----- |
| **Starter**      | 60           | 1,000         | 10,000       | 100   |
| **Professional** | 120          | 5,000         | 50,000       | 200   |
| **Enterprise**   | 300          | 15,000        | Unlimited    | 500   |

**Rate limit headers** (included in every response):

```
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000
```

**429 Too Many Requests response:**

```json theme={null}
{
  "error": "Rate limit exceeded",
  "message": "You have exceeded your rate limit of 60 requests per minute",
  "retry_after": 30
}
```

**Best practices:**

* Implement exponential backoff
* Cache results (5-15 minutes)
* Monitor `X-RateLimit-Remaining`
* Use batch queries when possible

***

## Error Handling

**HTTP status codes:**

<Tabs>
  <Tab title="400 Bad Request">
    **Cause:** Invalid request parameters

    ```json theme={null}
    {
      "error": "Invalid request",
      "message": "Query parameter is required",
      "field": "query"
    }
    ```

    **Common issues:**

    * Missing `query` parameter
    * `limit` out of range (1-20)
    * `query` too long (>500 chars)

    **Fix:** Validate parameters before sending
  </Tab>

  <Tab title="401 Unauthorized">
    **Cause:** Invalid or missing API key

    ```json theme={null}
    {
      "error": "Unauthorized",
      "message": "Invalid API key"
    }
    ```

    **Common issues:**

    * API key not in Authorization header
    * Wrong key format
    * Expired or revoked key

    **Fix:** Verify API key and header format
  </Tab>

  <Tab title="403 Forbidden">
    **Cause:** API key lacks access to resource

    ```json theme={null}
    {
      "error": "Forbidden",
      "message": "API key does not have access to repository 'backend-api'"
    }
    ```

    **Fix:** Check API key scopes or connect repository
  </Tab>

  <Tab title="404 Not Found">
    **Cause:** Resource doesn't exist

    ```json theme={null}
    {
      "error": "Not found",
      "message": "Repository 'backend-api' not found"
    }
    ```

    **Fix:** Verify repository name or connect repository
  </Tab>

  <Tab title="429 Too Many Requests">
    **Cause:** Rate limit exceeded

    ```json theme={null}
    {
      "error": "Rate limit exceeded",
      "retry_after": 30
    }
    ```

    **Fix:** Implement backoff, cache results, or upgrade plan
  </Tab>

  <Tab title="500 Internal Server Error">
    **Cause:** Server-side issue

    ```json theme={null}
    {
      "error": "Internal server error",
      "message": "Search service temporarily unavailable"
    }
    ```

    **Fix:** Retry with exponential backoff. Contact support if persistent.
  </Tab>
</Tabs>

**Robust error handling:**

```typescript theme={null}
async function searchWithRetry(
  query: string,
  maxRetries = 3
): Promise<SearchResponse> {
  for (let attempt = 1; attempt <= maxRetries; attempt++) {
    try {
      return await ulpi.searchDocumentation(query);
    } catch (error) {
      // Don't retry client errors (4xx)
      if (error.status >= 400 && error.status < 500) {
        throw error;
      }

      // Last attempt - throw error
      if (attempt === maxRetries) {
        throw error;
      }

      // Exponential backoff: 1s, 2s, 4s
      const delay = Math.pow(2, attempt - 1) * 1000;
      await new Promise(resolve => setTimeout(resolve, delay));
    }
  }
}
```

***

## Best Practices

<AccordionGroup>
  <Accordion title="🗄️ Cache Results Aggressively" icon="database">
    **Cache search results to reduce API calls:**

    ```typescript theme={null}
    // Simple in-memory cache with TTL
    class SearchCache {
      private cache = new Map<string, { data: any; expires: number }>();

      set(key: string, data: any, ttl = 300000) { // 5 min default
        this.cache.set(key, {
          data,
          expires: Date.now() + ttl
        });
      }

      get(key: string): any | null {
        const item = this.cache.get(key);
        if (!item) return null;

        if (Date.now() > item.expires) {
          this.cache.delete(key);
          return null;
        }

        return item.data;
      }
    }

    const cache = new SearchCache();

    async function cachedSearch(query: string, options: any) {
      const cacheKey = JSON.stringify({ query, ...options });

      // Check cache first
      const cached = cache.get(cacheKey);
      if (cached) {
        return { ...cached, meta: { ...cached.meta, cached: true } };
      }

      // Cache miss - call API
      const results = await ulpi.searchDocumentation(query, options);
      cache.set(cacheKey, results);

      return results;
    }
    ```

    **Recommended TTL:**

    * User searches: 5-15 minutes
    * Programmatic queries: 1-5 minutes
    * Static content: 1 hour

    **Cache invalidation:**

    * After repository webhook (if you process webhooks)
    * Manual refresh button
    * Time-based expiry
  </Accordion>

  <Accordion title="🔒 Secure API Keys" icon="lock">
    **NEVER expose API keys in client-side code:**

    ```typescript theme={null}
    // ❌ BAD: Client-side code (key exposed in browser)
    // frontend/search.ts
    const results = await fetch('https://api.ulpi.io/api/v1/documentation/search', {
      headers: {
        'Authorization': 'Bearer ulpi_live_sk_abc123...' // EXPOSED!
      }
    });

    // ✅ GOOD: Proxy through your backend
    // frontend/search.ts
    const results = await fetch('/api/search', {
      method: 'POST',
      body: JSON.stringify({ query: 'authentication' })
    });

    // backend/api/search.ts
    export async function POST(req: Request) {
      const { query } = await req.json();

      // API key stays on server
      const results = await fetch('https://api.ulpi.io/api/v1/documentation/search', {
        headers: {
          'Authorization': `Bearer ${process.env.ULPI_API_KEY}` // SECURE
        },
        body: JSON.stringify({ query })
      });

      return results.json();
    }
    ```

    **Storage best practices:**

    * Environment variables (`.env` file, gitignored)
    * Secret managers (AWS Secrets Manager, HashiCorp Vault)
    * Never commit to version control
    * Rotate keys quarterly
  </Accordion>

  <Accordion title="⚡ Implement Exponential Backoff" icon="rotate">
    **Retry failed requests with increasing delays:**

    ```typescript theme={null}
    async function fetchWithBackoff<T>(
      fn: () => Promise<T>,
      maxRetries = 3
    ): Promise<T> {
      for (let attempt = 0; attempt < maxRetries; attempt++) {
        try {
          return await fn();
        } catch (error: any) {
          // Don't retry client errors (4xx)
          if (error.status >= 400 && error.status < 500) {
            throw error;
          }

          // Last attempt
          if (attempt === maxRetries - 1) {
            throw error;
          }

          // Exponential backoff: 1s, 2s, 4s
          const delay = Math.pow(2, attempt) * 1000;

          // Add jitter to prevent thundering herd
          const jitter = Math.random() * 1000;

          await new Promise(resolve => setTimeout(resolve, delay + jitter));
        }
      }

      throw new Error('Max retries exceeded');
    }

    // Usage
    const results = await fetchWithBackoff(() =>
      ulpi.searchDocumentation('deploy to production')
    );
    ```

    **When to retry:**

    * ✅ 500-level errors (server issues)
    * ✅ Network errors (timeouts, connection refused)
    * ✅ 429 (but respect `retry_after` header)
    * ❌ 400-level errors (client mistakes - fix your code)
  </Accordion>

  <Accordion title="📊 Monitor Usage & Performance" icon="chart-line">
    **Track API usage to avoid rate limits:**

    ```typescript theme={null}
    class ULPIMetrics {
      private requestCount = 0;
      private errors = 0;
      private latencies: number[] = [];

      async trackSearch<T>(fn: () => Promise<T>): Promise<T> {
        this.requestCount++;
        const start = Date.now();

        try {
          const result = await fn();
          const latency = Date.now() - start;
          this.latencies.push(latency);
          return result;
        } catch (error) {
          this.errors++;
          throw error;
        }
      }

      getStats() {
        const avgLatency = this.latencies.reduce((a, b) => a + b, 0) / this.latencies.length;

        return {
          requests: this.requestCount,
          errors: this.errors,
          errorRate: (this.errors / this.requestCount) * 100,
          avgLatency: Math.round(avgLatency),
          p95Latency: this.percentile(0.95),
          p99Latency: this.percentile(0.99)
        };
      }

      private percentile(p: number): number {
        const sorted = [...this.latencies].sort((a, b) => a - b);
        const index = Math.ceil(sorted.length * p) - 1;
        return sorted[index];
      }
    }

    const metrics = new ULPIMetrics();

    // Tracked search
    const results = await metrics.trackSearch(() =>
      ulpi.searchDocumentation('authentication')
    );

    // View stats
    console.log(metrics.getStats());
    // { requests: 150, errors: 2, errorRate: 1.33%, avgLatency: 67ms, p95: 142ms, p99: 234ms }
    ```

    **What to monitor:**

    * Request volume per hour
    * Error rate and types
    * Latency percentiles (p50, p95, p99)
    * Cache hit rate
    * Remaining rate limit quota
  </Accordion>

  <Accordion title="🔍 Optimize Search Queries" icon="magnifying-glass">
    **Write better queries for better results:**

    ```typescript theme={null}
    // ❌ Bad: Too generic
    await ulpi.searchDocumentation('database');
    // Returns 127 results, mostly irrelevant

    // ✅ Good: Specific question
    await ulpi.searchDocumentation('How to optimize slow PostgreSQL queries?');
    // Returns 3 highly relevant results

    // ✅ Better: Include context
    await ulpi.searchDocumentation(
      'How to configure Redis cache for API session storage?',
      { repository: 'backend-api' }
    );
    // Scoped to relevant repo, precise results

    // Helper function for query optimization
    function buildQuery(
      topic: string,
      context?: string,
      action?: 'how' | 'what' | 'where'
    ): string {
      const actionMap = {
        how: 'How do I',
        what: 'What is',
        where: 'Where is'
      };

      const prefix = action ? actionMap[action] : '';
      const suffix = context ? `in ${context}` : '';

      return `${prefix} ${topic} ${suffix}`.trim();
    }

    // Usage
    const query = buildQuery('deploy', 'production environment', 'how');
    // "How do I deploy in production environment"
    ```

    **Query best practices:**

    * Be specific, not generic
    * Use question format
    * Include technology names
    * Specify environment/context
    * Use repository filter when known
  </Accordion>
</AccordionGroup>

***

## Webhooks (Optional)

**Get notified when documentation updates:**

<Info>
  **Coming soon:** Webhook support for documentation changes.

  Subscribe to:

  * `documentation.indexed` - Repository finished indexing
  * `documentation.updated` - Documentation file changed
  * `documentation.deleted` - Documentation file removed

  [Request early access →](mailto:support@ulpi.io?subject=Webhook%20Access)
</Info>

***

## Next Steps

<CardGroup cols={2}>
  <Card title="Generate API Keys" icon="key" href="https://app.ulpi.io/api-keys">
    Create production API keys for your integration

    **Start building today**
  </Card>

  <Card title="Search Features" icon="magnifying-glass" href="/documentation/search-features">
    Learn about query syntax and advanced filters

    **Optimize your queries**
  </Card>

  <Card title="How It Works" icon="gear" href="/documentation/how-it-works">
    Understand semantic search architecture and performance

    **Deep dive into technology**
  </Card>

  <Card title="Repository Management" icon="folder" href="/documentation/repositories">
    Connect repositories to make them searchable via API

    **Expand your search scope**
  </Card>
</CardGroup>

***

<Note>
  **Need help with API integration?**

  * 📧 **Email:** [support@ulpi.io](mailto:support@ulpi.io)
  * 📚 **Docs:** [docs.ulpi.io](https://docs.ulpi.io)
  * 💬 **Slack:** [ulpi.io/slack](https://ulpi.io/slack)
  * 🐛 **Issues:** [github.com/ulpi-io/api-clients](https://github.com/ulpi-io/api-clients)

  **Average response time:** Under 2 hours during business hours

  **Looking for client libraries?** We have official SDKs for JavaScript, Python, PHP, and Go.

  **Want to contribute?** We accept community SDKs for other languages!
</Note>
