Skip to main content

Creator Autocomplete Best Practices

The Creator Autocomplete API provides real-time search suggestions as users type. Follow these best practices to build fast, cost-effective autocomplete experiences.

Endpoint Overview

GET /v1/creators/autocomplete?q={query}&limit={limit}&platform={platform}
Pricing: 0.001 credits per request (extremely cost-effective) Use Case: Real-time search suggestions in UI components

Critical: Implement Debouncing

Always implement client-side debouncing to prevent excessive API calls. Without debouncing, autocomplete can generate hundreds of unnecessary requests.
SettingValueReason
Debounce delay300msBalances responsiveness with API efficiency
Minimum query length2 charactersReduces noise from single-letter searches
Maximum requests/sec10Stays well within rate limits

Implementation Examples

Vanilla JavaScript

function createAutocomplete(inputElement, onResults) {
  let debounceTimer;
  let abortController = null;

  inputElement.addEventListener('input', async (e) => {
    const query = e.target.value.trim();

    // Clear previous timer and cancel in-flight request
    clearTimeout(debounceTimer);
    if (abortController) {
      abortController.abort();
    }

    // Don't search for queries shorter than 2 characters
    if (query.length < 2) {
      onResults([]);
      return;
    }

    // Debounce the API call
    debounceTimer = setTimeout(async () => {
      try {
        abortController = new AbortController();
        
        const response = await fetch(
          `https://api.influship.com/v1/creators/autocomplete?q=${encodeURIComponent(query)}&limit=5`,
          {
            headers: {
              'X-API-Key': 'your-api-key-here',
            },
            signal: abortController.signal
          }
        );

        if (!response.ok) {
          throw new Error(`API error: ${response.status}`);
        }

        const data = await response.json();
        onResults(data.results || []);
      } catch (error) {
        if (error.name === 'AbortError') {
          // Request was cancelled, ignore
          return;
        }
        console.error('Autocomplete error:', error);
        onResults([]);
      }
    }, 300); // 300ms delay
  });
}

// Usage
const searchInput = document.querySelector('#creator-search');
const resultsContainer = document.querySelector('#results');

createAutocomplete(searchInput, (results) => {
  resultsContainer.innerHTML = results
    .map(creator => `
      <div class="result">
        <img src="${creator.avatar_url}" alt="${creator.name}">
        <div>
          <strong>${creator.name}</strong>
          ${creator.platforms.map(p => 
            `<span>${p.platform}: @${p.username}</span>`
          ).join('')}
        </div>
      </div>
    `)
    .join('');
});

React with Hooks

import { useState, useEffect, useCallback, useRef } from 'react';

function CreatorAutocomplete() {
  const [query, setQuery] = useState('');
  const [results, setResults] = useState([]);
  const [loading, setLoading] = useState(false);
  const abortControllerRef = useRef(null);

  const searchCreators = useCallback(async (searchQuery) => {
    if (searchQuery.length < 2) {
      setResults([]);
      return;
    }

    // Cancel previous request
    if (abortControllerRef.current) {
      abortControllerRef.current.abort();
    }

    abortControllerRef.current = new AbortController();
    setLoading(true);

    try {
      const response = await fetch(
        `https://api.influship.com/v1/creators/autocomplete?q=${encodeURIComponent(searchQuery)}&limit=5`,
        {
          headers: {
            'X-API-Key': process.env.REACT_APP_INFLUSHIP_API_KEY,
          },
          signal: abortControllerRef.current.signal
        }
      );

      if (!response.ok) {
        throw new Error(`API error: ${response.status}`);
      }

      const data = await response.json();
      setResults(data.results || []);
    } catch (error) {
      if (error.name === 'AbortError') {
        // Request was cancelled, ignore
        return;
      }
      console.error('Autocomplete error:', error);
      setResults([]);
    } finally {
      setLoading(false);
    }
  }, []);

  useEffect(() => {
    const debounceTimer = setTimeout(() => {
      searchCreators(query);
    }, 300);

    return () => {
      clearTimeout(debounceTimer);
      if (abortControllerRef.current) {
        abortControllerRef.current.abort();
      }
    };
  }, [query, searchCreators]);

  return (
    <div className="autocomplete">
      <input
        type="text"
        value={query}
        onChange={(e) => setQuery(e.target.value)}
        placeholder="Search creators..."
        aria-label="Search creators"
        aria-autocomplete="list"
        aria-controls="autocomplete-results"
      />

      {loading && <div className="loading">Searching...</div>}

      <ul id="autocomplete-results" role="listbox">
        {results.map((creator) => (
          <li key={creator.id} role="option">
            <img src={creator.avatar_url} alt="" />
            <div>
              <strong>{creator.name}</strong>
              {creator.platforms.map((platform) => (
                <div key={platform.platform}>
                  {platform.platform}: @{platform.username}
                  {platform.match_type === 'username' && ' (username match)'}
                  {platform.match_type === 'display_name' && ' (name match)'}
                </div>
              ))}
            </div>
          </li>
        ))}
      </ul>
    </div>
  );
}

Vue 3 Composition API

<template>
  <div class="autocomplete">
    <input
      v-model="query"
      type="text"
      placeholder="Search creators..."
      @input="handleInput"
    />
    
    <div v-if="loading">Searching...</div>
    
    <ul v-if="results.length">
      <li v-for="creator in results" :key="creator.id">
        <img :src="creator.avatar_url" :alt="creator.name" />
        <div>
          <strong>{{ creator.name }}</strong>
          <div v-for="platform in creator.platforms" :key="platform.platform">
            {{ platform.platform }}: @{{ platform.username }}
          </div>
        </div>
      </li>
    </ul>
  </div>
</template>

<script setup>
import { ref } from 'vue';
import { useDebounceFn } from '@vueuse/core'; // or implement your own

const query = ref('');
const results = ref([]);
const loading = ref(false);
let abortController = null;

const searchCreators = async (searchQuery) => {
  if (searchQuery.length < 2) {
    results.value = [];
    return;
  }

  // Cancel previous request
  if (abortController) {
    abortController.abort();
  }

  abortController = new AbortController();
  loading.value = true;

  try {
    const response = await fetch(
      `https://api.influship.com/v1/creators/autocomplete?q=${encodeURIComponent(searchQuery)}&limit=5`,
      {
        headers: {
          'X-API-Key': import.meta.env.VITE_INFLUSHIP_API_KEY,
        },
        signal: abortController.signal
      }
    );

    const data = await response.json();
    results.value = data.results || [];
  } catch (error) {
    if (error.name !== 'AbortError') {
      console.error('Autocomplete error:', error);
      results.value = [];
    }
  } finally {
    loading.value = false;
  }
};

const handleInput = useDebounceFn((e) => {
  searchCreators(e.target.value);
}, 300);
</script>

Advanced: Client-Side Caching

Reduce API calls by caching recent searches:
class AutocompleteCache {
  constructor(maxSize = 100, ttl = 300000) { // 5 minutes TTL
    this.cache = new Map();
    this.maxSize = maxSize;
    this.ttl = ttl;
  }

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

    // Check if expired
    if (Date.now() - item.timestamp > this.ttl) {
      this.cache.delete(key);
      return null;
    }

    return item.data;
  }

  set(key, data) {
    // Implement LRU eviction
    if (this.cache.size >= this.maxSize) {
      const firstKey = this.cache.keys().next().value;
      this.cache.delete(firstKey);
    }

    this.cache.set(key, {
      data,
      timestamp: Date.now(),
    });
  }

  clear() {
    this.cache.clear();
  }
}

const cache = new AutocompleteCache();

async function searchWithCache(query) {
  // Check cache first
  const cached = cache.get(query.toLowerCase());
  if (cached) {
    console.log('Cache hit for:', query);
    return cached;
  }

  // Fetch from API
  const results = await searchCreators(query);
  
  // Store in cache
  cache.set(query.toLowerCase(), results);
  
  return results;
}

Platform Filtering

Narrow results by platform when the target platform is known:
// Search only Instagram creators
const response = await fetch(
  `https://api.influship.com/v1/creators/autocomplete?q=${query}&platform=instagram&limit=5`
);

// Search only TikTok creators
const response = await fetch(
  `https://api.influship.com/v1/creators/autocomplete?q=${query}&platform=tiktok&limit=5`
);

Match Highlighting

The API returns match_type to help you highlight relevant matches:
function highlightMatch(text, query) {
  const regex = new RegExp(`(${escapeRegex(query)})`, 'gi');
  return text.replace(regex, '<mark>$1</mark>');
}

function escapeRegex(string) {
  return string.replace(/[.*+?^${}()|[\]\\]/g, '\\$&');
}

// Render with highlighted matches
results.forEach((creator) => {
  creator.platforms.forEach((platform) => {
    if (platform.match_type === 'username') {
      // Highlight in username
      const highlighted = highlightMatch(platform.username, query);
      console.log(`@${highlighted}`);
    } else if (platform.match_type === 'display_name') {
      // Highlight in display name
      const highlighted = highlightMatch(platform.display_name || creator.name, query);
      console.log(highlighted);
    }
  });
});

Error Handling

Gracefully handle errors without breaking the UX:
async function safeAutocomplete(query) {
  try {
    const response = await fetch(url, options);
    
    if (response.status === 429) {
      // Rate limited - show user-friendly message
      console.warn('Rate limited. Please slow down your search.');
      return { results: [], error: 'rate_limited' };
    }
    
    if (!response.ok) {
      throw new Error(`API error: ${response.status}`);
    }
    
    return await response.json();
  } catch (error) {
    console.error('Autocomplete error:', error);
    
    // Return empty results instead of breaking the UI
    return { results: [], error: 'network_error' };
  }
}

Performance Tips

Debounce Aggressively

Use 300ms+ delays to reduce unnecessary requests

Cancel In-Flight Requests

Use AbortController to cancel outdated requests

Cache Aggressively

Cache results for 5-10 minutes to reduce API calls

Limit Results

Request only 5-10 results for optimal UX and cost

Platform Filter

Use platform filtering when the target is known

Minimum Query Length

Require 2-3 characters before searching

Cost Optimization

Autocomplete is extremely cost-effective:
  • Cost: 0.001 credits per request
  • Free tier: 1,000 credits/month = 1,000,000 autocomplete requests
  • Pro tier: 10,000 credits/month = 10,000,000 autocomplete requests
With proper debouncing (300ms) and minimum query length (2 chars), typical usage:
  • Heavy user typing “fitness”: ~3-4 requests = 0.003-0.004 credits
  • 1,000 searches/day: ~3,000 requests/day = 3 credits/day = 90 credits/month
Even with thousands of daily searches, autocomplete costs remain negligible. Focus on UX optimization rather than cost concerns.

Monitoring Usage

Track autocomplete performance and costs:
class AutocompleteMetrics {
  constructor() {
    this.totalRequests = 0;
    this.cacheHits = 0;
    this.errors = 0;
    this.totalLatency = 0;
  }

  recordRequest(latency, cached = false, error = false) {
    this.totalRequests++;
    
    if (cached) {
      this.cacheHits++;
    }
    
    if (error) {
      this.errors++;
    } else {
      this.totalLatency += latency;
    }
  }

  getStats() {
    const avgLatency = this.totalLatency / (this.totalRequests - this.cacheHits);
    const cacheHitRate = (this.cacheHits / this.totalRequests * 100).toFixed(1);
    const errorRate = (this.errors / this.totalRequests * 100).toFixed(1);

    return {
      totalRequests: this.totalRequests,
      cacheHitRate: `${cacheHitRate}%`,
      errorRate: `${errorRate}%`,
      avgLatency: `${avgLatency.toFixed(0)}ms`,
      estimatedCredits: (this.totalRequests * 0.001).toFixed(3)
    };
  }
}

const metrics = new AutocompleteMetrics();

async function trackedAutocomplete(query) {
  const start = Date.now();
  
  try {
    const results = await searchWithCache(query);
    const latency = Date.now() - start;
    
    metrics.recordRequest(latency, results.cached);
    return results;
  } catch (error) {
    metrics.recordRequest(Date.now() - start, false, true);
    throw error;
  }
}

// Log stats periodically
setInterval(() => {
  console.log('Autocomplete Stats:', metrics.getStats());
}, 60000); // Every minute

Accessibility

Make autocomplete accessible to all users:
<div role="combobox" aria-expanded={results.length > 0} aria-haspopup="listbox">
  <input
    type="text"
    role="searchbox"
    aria-autocomplete="list"
    aria-controls="autocomplete-results"
    aria-activedescendant={activeItemId}
  />
  
  <ul id="autocomplete-results" role="listbox">
    {results.map((creator, index) => (
      <li
        key={creator.id}
        role="option"
        id={`result-${index}`}
        aria-selected={index === activeIndex}
      >
        {creator.name}
      </li>
    ))}
  </ul>
</div>

Next Steps