Skip to main content

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
Just want to use ULPI with Claude/Cursor? See Getting Started for MCP setup instead.Building something custom? This guide is for you.

Why Use the API?

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

Custom Search UIs

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

Slack/Discord Bots

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

CLI Tools

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

Custom AI Agents

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
Plus: Dashboards, IDE plugins, mobile apps, workflow automation, and more.

Quick Start: Your First API Call

Get up and running in 2 minutes:
1

Get Your API Key

Generate an API key:
  1. Go to 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_...)
Save your API key immediately! It’s only shown once.Store in password manager or environment variable.
2

Make Your First Request

Test with cURL:
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.
3

View Results

You’ll get JSON response:
{
  "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.
4

Integrate Into Your App

Choose your language:Or: Use the REST API directly
Total time: 2 minutes from API key to first results

Real-World Integration Examples

See how teams use the ULPI API:
Use case: Engineers ask /docs <question> in Slack, bot returns relevant docsBenefits:
  • No context switching (stay in Slack)
  • Share results with team instantly
  • Track most-asked questions
  • Reduce Slack interruptions (β€œWhere’s the deploy doc?”)
Implementation:
  • Bolt.js (JavaScript)
  • Python (slack_sdk)
// 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
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
Use case: Company-branded docs website with ULPI searchBenefits:
  • Centralized documentation access
  • Advanced search with filters
  • Usage analytics (what’s searched most)
  • SSO integration
Implementation (Next.js):
  • API Route
  • Search Component
  • Result Display
// 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);
}
Features to add:
  • Repository filter dropdown
  • Branch selector
  • Search history
  • Bookmarks
  • Analytics dashboard
Example: docs.acmecorp.com - internal portal with 1,000+ daily searches
Use case: Quick doc access in admin dashboardBenefits:
  • Context-aware quick links
  • Common runbooks one click away
  • New hire onboarding helper
  • Embedded in existing tools
Implementation (React):
// 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)
Use case: Customer support chatbot that searches internal docsBenefits:
  • Accurate answers from YOUR documentation
  • Reduces support ticket volume
  • 24/7 availability
  • Cites sources (links to docs)
Implementation (LangChain + OpenAI):
# 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

API Reference

Complete API endpoint documentation:

Authentication

All requests require API key in Authorization header:
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

POST /api/v1/documentation/search

Search across indexed documentation Base URL: https://api.ulpi.io/api/v1/documentation/search Request Body:
{
  "query": "How do I deploy to production?",
  "repository": "backend-api",
  "branch": "main",
  "limit": 10
}
Parameters:
ParameterTypeRequiredDescriptionDefault
querystringβœ… YesNatural language search query-
repositorystring❌ NoFilter to specific repositoryAll repos
branchstring❌ NoSearch specific branchDefault branch
limitinteger❌ NoMax 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:
{
  "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:
FieldTypeDescription
results[]arrayArray of search results (sorted by relevance)
results[].contentstringDocumentation excerpt (most relevant section)
results[].filestringFile path relative to repository root
results[].repositorystringRepository name
results[].branchstringGit branch
results[].urlstringDirect link to file on GitHub/GitLab
results[].scorefloatRelevance score (0.0-1.0, higher = better match)
results[].last_updatedstringISO 8601 timestamp of last file update
meta.totalintegerTotal results found (may be more than returned)
meta.query_time_msintegerQuery execution time in milliseconds
meta.cachedbooleanWhether results were served from cache

Code Examples by Language

  • JavaScript / TypeScript
  • Python
  • PHP
  • Go
Node.js implementation:
// 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`);
});

Rate Limits & Quotas

API limits by plan:
PlanRequests/MinRequests/HourRequests/DayBurst
Starter601,00010,000100
Professional1205,00050,000200
Enterprise30015,000Unlimited500
Rate limit headers (included in every response):
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 45
X-RateLimit-Reset: 1640000000
429 Too Many Requests response:
{
  "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:
  • 400 Bad Request
  • 401 Unauthorized
  • 403 Forbidden
  • 404 Not Found
  • 429 Too Many Requests
  • 500 Internal Server Error
Cause: Invalid request parameters
{
  "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
Robust error handling:
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

Cache search results to reduce API calls:
// 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
NEVER expose API keys in client-side code:
// ❌ 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
Retry failed requests with increasing delays:
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)
Track API usage to avoid rate limits:
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
Write better queries for better results:
// ❌ 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

Webhooks (Optional)

Get notified when documentation updates:
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 β†’

Next Steps


Need help with API integration?Average response time: Under 2 hours during business hoursLooking for client libraries? We have official SDKs for JavaScript, Python, PHP, and Go.Want to contribute? We accept community SDKs for other languages!