API Documentation
TypoSuggest provides instant, typo-tolerant search suggestions across product titles, marketplace gigs, articles, and catalog databases. Matching takes under a millisecond, keeping your primary database free from keystroke traffic.
Authentication & Dual Keys
Every account receives two roles of API keys designed for separate environments:
ac_pub_...)
Safe to embed directly in browser JavaScript, React components, or mobile applications. Permitted
exclusively on GET /v1/suggest. Cannot write, modify, or delete data.
ac_sec_...)
Restricted to your backend server. Grants full administrative permissions to create datasets, ingest rows, and delete records. Keep private.
X-API-Key: ac_pub_your_key_here
Vibe Coder / AI Agent Setup (MCP & Prompt)
TypoSuggest provides a native Model Context Protocol (MCP) server so coding agents in Cursor, Claude Code, Windsurf, or Antigravity can inspect your datasets, add phrases, and test suggestions autonomously. You can also copy a pre-filled, zero-hallucination prompt directly into your AI chat.
mcp.json):{
"mcpServers": {
"typosuggest": {
"url": "https://typosuggest.pial.digital/mcp",
"headers": {
"X-API-Key": "ac_sec_your_secret_key_here"
}
}
}
}
How Ranking Works
TypoSuggest handles relevance for you—no manual tuning needed. Each query is checked in four stages, and phrases you have uploaded more often rank higher:
- Stage 1 (Starts With The Query): Suggests phrases that begin with the words you typed.
Ranked by how often the phrase appears in your data (returned as
frequency). - Stage 2 (Contains The Query): Suggests phrases that contain the words you typed anywhere inside the title.
- Stage 3 (Typos Allowed): Corrects spelling mistakes (e.g.,
macbok → macbook,wrodpres → wordpress) and matches them in under a millisecond. - Stage 4 (Partial Words): Suggests individual words when longer phrases are not found.
/v1/suggest
Query suggestions from a specific dataset. Allowed with either public or secret key.
dataset_id(required): The UUID or string ID of the target dataset.q(required): The search prefix or query text (e.g.macbok).key(optional): Public search key (ac_pub_...). Passing?key=...eliminates browser CORSOPTIONSpreflight requests completely!limit(optional, default 8): Number of suggestions to return (max 20).typo(optional, default true): Turn typo tolerance on or off.
You can authenticate GET /v1/suggest using either the X-API-Key: ac_pub_... header OR the ?key=ac_pub_... URL query parameter. Passing ?key=... makes the call a CORS simple request, so the browser skips the OPTIONS preflight round-trip.
Security Note: Query parameters can be recorded in proxy/CDN logs, browser history, and monitoring systems. Only pass public, read-only keys (ac_pub_...) in URLs—never pass secret keys (ac_sec_...) or session tokens in query strings.
GET https://typosuggest.pial.digital/v1/suggest?dataset_id=YOUR_DATASET_ID&q=macbook&limit=5
X-API-Key: ac_pub_YOUR_KEY
{
"query": "macbook",
"took_ms": 0.08,
"suggestions": [
{ "text": "Apple MacBook Pro 16 inch M3 Max", "score": 24.5, "frequency": 2, "group": "laptop" },
{ "text": "MacBook Air M2 13 inch", "score": 18.2, "frequency": 1, "group": "laptop" }
]
}
/widget.js):
<!-- Option A: WordPress, WooCommerce & Shopify (paste in site header — auto-attaches to input[name="s"] & input[name="q"]) -->
<script
src="https://typosuggest.pial.digital/widget.js"
data-key="ac_pub_YOUR_KEY"
data-dataset="YOUR_DATASET_ID"
defer
></script>
<!-- Option B: Any Custom HTML <input> -->
<script src="https://typosuggest.pial.digital/widget.js" defer></script>
<input
type="search"
data-typosuggest-key="ac_pub_YOUR_KEY"
data-typosuggest-dataset="YOUR_DATASET_ID"
placeholder="Search products..."
/>
/v1/datasets/{id}/items:add
Ingest raw texts or structured items. Requires secret key or Bearer token.
{
"items": [
{
"id": "item_901",
"text": "I will build a modern WordPress website for your business",
"group": "web-development",
"count": 10
},
{
"id": "item_902",
"text": "Apple MacBook Pro 16 inch M3 Max",
"group": "product",
"count": 25
},
{
"id": "item_903",
"text": "Responsive React Tailwind Dashboard",
"group": "web-development",
"count": 5
}
]
}
{
"added": 3,
"skipped_duplicates": 0,
"active_phrases": 28,
"took_ms": 1.18
}
/v1/datasets/{id}/items:delete
Delete records by passing the matching item id and text. Sub-phrases shared with other
records remain intact with decremented frequency.
{
"ids": ["item_902"],
"texts": ["Apple MacBook Pro 16 inch M3 Max"]
}
{
"deleted_docs": 2,
"purged_phrases": 5,
"active_phrases": 23,
"took_ms": 0.31
}
Measured Performance
These numbers come from the internal engine benchmark suite in this repository (internal/engine/benchmark_test.go), run against an in-memory stress-test dataset of 100,000 raw ingested items (which deduplicate into ~38,000–45,000 unique indexed phrases and n-grams) across 5,000 varied queries (prefix, multi-word, typo, and non-Latin script). Note that while the self-serve Beta plan caps customer accounts at 2 datasets and 500 rows, this stress test exercises the underlying engine directly to verify capacity for high-volume custom deployments. Re-run it yourself with
go test ./internal/engine/ -run TestStress100kDatasetMetrics -v.
| Metric | Value | Notes |
|---|---|---|
| Median match time | 0.031 ms | Time to match one query against the index. |
| p95 match time | 0.104 ms | 95% of queries match faster than this. |
| p99 match time | 0.139 ms | Worst-case tail. |
| Ingestion throughput | ~63,000 rows/sec | Items indexed and deduplicated per second. |
| Memory per row | ~0.31 KB | Heap in use after ingesting 100,000 rows. |
| Snapshot load time | ~250 ms | Cold load of a compressed 3.9 MB snapshot. |
took_ms field so you can measure the real number on your own
data.
Errors & Troubleshooting
Every error response uses the same shape. The error field explains what
went wrong in plain language.
{
"error": "Dataset not found"
}
| Status | Meaning | What to do |
|---|---|---|
| 400 | The request is missing a required field or the payload is malformed. | Check that dataset_id and q
are both present and URL-encoded. |
| 401 | The API key is missing, malformed, or revoked. | Send a valid key in the X-API-Key header. If you
revoked it, issue a new one from the console. |
| 403 | The key is valid but not allowed to perform this action. | Public keys can only read suggestions. Use your secret key for uploads and deletes. |
| 404 | The dataset does not exist for this account. | Copy the dataset ID from the console. Datasets are scoped to the account that owns them. |
| 429 | Too many requests in a short window. | Add a debounce of 100–200ms and send one request per keystroke burst, not one per keystroke. |
| 402 | The monthly suggestion quota for this account is used up. | Wait for the monthly reset, or contact us to raise your limit. |
Common issues
- Suggestions come back empty. The most common cause is a typo that is more than two
edits away, or a query shorter than two characters. Confirm the dataset has items with
GET /v1/datasets/{id}/stats. - Requests fail in the browser but work in curl. You are probably sending the secret key from frontend code. Use the public key in the browser and keep the secret key on your server.
- Every keystroke sends two requests. The browser is issuing a CORS preflight. Pass the
public key as a
?key=query parameter instead of a header to make it a simple request. - Items upload but never appear in suggestions. The index is rebuilt asynchronously. Wait a second and retry, and check the dataset has an active phrase count above zero.
Copy-Paste Code Examples
Pre-filled with your live server host, active keys, and dataset.
WordPress, WooCommerce, Shopify & Webflow (1-Line Drop-In Widget)
No coding required. Automatically upgrades built-in WordPress (input[name="s"]), Shopify (input[name="q"]), and Webflow (input[name="query"]) search bars.
<script
src="https://typosuggest.pial.digital/widget.js"
data-key="ac_pub_YOUR_KEY"
data-dataset="YOUR_DATASET_ID"
defer
></script>
- Open WP Admin → WPCode (Headers & Footers) or Elementor → Custom Code.
- Paste into the Header box and click Save.
- Automatically upgrades every
input[name="s"]search bar.
- Open Online Store → Themes → Edit code.
- In
layout/theme.liquid, paste right above</head>& Save. - Automatically upgrades
input[name="q"]search bars.
- Open Site Settings → Custom Code → Head Code.
- Paste the script & click Publish.
- Upgrades
input[name="query"]or anytype="search"input.
By default, widget.js automatically inherits your search input’s font and border radius. Below is how the built-in dropdown renders in Light Mode (default) and Dark Mode (data-theme="dark"), plus a live input you can type into right now:
- WordPress website
- Modern WordPress website development
- Custom WordPress website with Elementor
- Apple MacBook Pro 16-inch M3 Max
- MacBook Air 15-inch M3 Chip
- USB-C Hub for MacBook Pro
widget.js dropdown right here:
Type wrodpres, macbok, or react to test the real widget in action.
<!-- Option A: Dark Mode or Custom Selector -->
<script src="https://typosuggest.pial.digital/widget.js" data-key="ac_pub_YOUR_KEY" data-dataset="YOUR_DATASET_ID" data-theme="dark" defer></script>
<!-- Option B: Headless Mode (Your Own Custom UI Renders the Results) -->
<script src="https://typosuggest.pial.digital/widget.js" data-key="ac_pub_YOUR_KEY" data-dataset="YOUR_DATASET_ID" data-headless="true" defer></script>
<script>
document.addEventListener("typosuggest:results", function (e) {
console.log("Suggestions for", e.detail.query, e.detail.suggestions);
// Render e.detail.suggestions into your own theme modal or dropdown!
});
</script>
import React, { useState, useEffect, useRef } from 'react';
const DATASET_ID = 'YOUR_DATASET_ID';
const PUBLIC_KEY = 'ac_pub_YOUR_KEY'; // Safe for browsers: read-only scope
const API_BASE = 'https://typosuggest.pial.digital';
export default function SearchAutocomplete() {
const [query, setQuery] = useState('');
const [results, setResults] = useState([]);
const [error, setError] = useState(null);
const requestSeq = useRef(0);
useEffect(() => {
const trimmed = query.trim();
if (!trimmed) {
setResults([]);
setError(null);
return;
}
const seq = ++requestSeq.current;
const controller = new AbortController();
const timer = setTimeout(async () => {
try {
// Passing ?key= in URL avoids browser CORS OPTIONS preflight
const url = `${API_BASE}/v1/suggest?dataset_id=${encodeURIComponent(DATASET_ID)}&q=${encodeURIComponent(trimmed)}&limit=6&key=${encodeURIComponent(PUBLIC_KEY)}`;
const res = await fetch(url, { signal: controller.signal });
if (!res.ok) {
const errBody = await res.json().catch(() => ({}));
throw new Error(errBody.error || `HTTP ${res.status}`);
}
const data = await res.json();
if (seq === requestSeq.current) {
setResults(Array.isArray(data.suggestions) ? data.suggestions : []);
setError(null);
}
} catch (err) {
if (err.name === 'AbortError') return;
if (seq === requestSeq.current) {
console.error('Suggest error:', err);
setError(err.message);
}
}
}, 120);
return () => {
clearTimeout(timer);
controller.abort();
};
}, [query]);
return (
<div style={{ position: 'relative', width: '360px' }}>
<input
type="text"
value={query}
onChange={(e) => setQuery(e.target.value)}
placeholder="Search products, services, news..."
style={{ width: '100%', padding: '10px 14px', border: '1px solid #E5E2DC', borderRadius: '8px', outline: 'none' }}
/>
{error && <div style={{ color: '#b91c1c', fontSize: '12px', marginTop: '4px' }}>{error}</div>}
{results.length > 0 && (
<ul style={{ position: 'absolute', top: '100%', left: 0, right: 0, background: '#fff', border: '1px solid #E5E2DC', borderRadius: '8px', listStyle: 'none', margin: '4px 0 0 0', padding: '4px 0', boxShadow: '0 4px 12px rgba(0,0,0,0.05)', zIndex: 50 }}>
{results.map((item) => (
<li key={item.text} style={{ padding: '8px 14px', cursor: 'pointer', borderBottom: '1px solid #F4F3EE', fontSize: '13px' }}>
{item.text}
</li>
))}
</ul>
)}
</div>
);
}
const API_BASE = 'https://typosuggest.pial.digital';
const DATASET_ID = 'YOUR_DATASET_ID';
const PUBLIC_KEY = 'ac_pub_YOUR_KEY';
let activeController = null;
async function queryAutocomplete(keyword) {
const trimmed = keyword.trim();
if (!trimmed) return [];
if (activeController) activeController.abort();
activeController = new AbortController();
const url = `${API_BASE}/v1/suggest?dataset_id=${encodeURIComponent(DATASET_ID)}&q=${encodeURIComponent(trimmed)}&limit=5&key=${encodeURIComponent(PUBLIC_KEY)}`;
const res = await fetch(url, { signal: activeController.signal });
if (!res.ok) {
const errBody = await res.json().catch(() => ({}));
throw new Error(errBody.error || `HTTP ${res.status}`);
}
const data = await res.json();
return Array.isArray(data.suggestions) ? data.suggestions : [];
}
// Server-side only: keep your ac_sec_... key in environment variables
async function indexProduct(product) {
const API_BASE = 'https://typosuggest.pial.digital';
const datasetId = process.env.TYPOSUGGEST_DATASET_ID;
const secretKey = process.env.TYPOSUGGEST_SECRET_KEY; // ac_sec_...
const res = await fetch(`${API_BASE}/v1/datasets/${encodeURIComponent(datasetId)}/items:add`, {
method: 'POST',
headers: {
'X-API-Key': secretKey,
'Content-Type': 'application/json'
},
body: JSON.stringify({
items: [
{ id: String(product.id), text: product.title, group: product.category || 'product', count: 1 }
]
}),
signal: AbortSignal.timeout(10000)
});
const payload = await res.json().catch(() => ({}));
if (!res.ok) {
throw new Error(`TypoSuggest ingest failed (${res.status}): ${payload.error || res.statusText}`);
}
return payload; // { added, skipped_duplicates, active_phrases, took_ms }
}
import os
import requests
API_BASE = "https://typosuggest.pial.digital"
DATASET_ID = "YOUR_DATASET_ID"
SECRET_KEY = os.environ.get("TYPOSUGGEST_SECRET_KEY", "ac_sec_YOUR_KEY")
res = requests.post(
f"{API_BASE}/v1/datasets/{DATASET_ID}/items:add",
headers={"X-API-Key": SECRET_KEY, "Content-Type": "application/json"},
json={
"items": [
{"id": "sku-1", "text": "Apple MacBook Pro 16 inch M3 Max", "group": "product", "count": 25},
{"id": "sku-2", "text": "Modern WordPress Website Development", "group": "gig", "count": 10},
]
},
timeout=5.0,
)
if not res.ok:
raise RuntimeError(f"Ingest failed ({res.status_code}): {res.text}")
payload = res.json()
print(f"Added {payload.get('added', 0)} items ({payload.get('active_phrases', 0)} active phrases)")
package main
import (
"context"
"encoding/json"
"fmt"
"net/http"
"net/url"
"time"
)
type Suggestion struct {
Text string `json:"text"`
Group string `json:"group"`
Score float64 `json:"score"`
Frequency uint32 `json:"frequency"`
}
type SuggestResponse struct {
Query string `json:"query"`
TookMs float64 `json:"took_ms"`
Suggestions []Suggestion `json:"suggestions"`
}
func Suggest(ctx context.Context, query string) (*SuggestResponse, error) {
client := &http.Client{Timeout: 5 * time.Second}
u, _ := url.Parse("https://typosuggest.pial.digital/v1/suggest")
q := u.Query()
q.Set("dataset_id", "YOUR_DATASET_ID")
q.Set("q", query)
q.Set("limit", "8")
u.RawQuery = q.Encode()
req, err := http.NewRequestWithContext(ctx, http.MethodGet, u.String(), nil)
if err != nil {
return nil, err
}
req.Header.Set("X-API-Key", "ac_pub_YOUR_KEY")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
if resp.StatusCode != http.StatusOK {
var apiErr struct {
Error string `json:"error"`
}
_ = json.NewDecoder(resp.Body).Decode(&apiErr)
return nil, fmt.Errorf("typosuggest HTTP %d: %s", resp.StatusCode, apiErr.Error)
}
var out SuggestResponse
if err := json.NewDecoder(resp.Body).Decode(&out); err != nil {
return nil, err
}
return &out, nil
}
curl "https://typosuggest.pial.digital/v1/suggest?dataset_id=YOUR_DATASET_ID&q=macbook&limit=5" \
-H "X-API-Key: ac_pub_YOUR_KEY"