Plain REST. Three scopes. v1 stable.
Bearer-token auth, JSON in and out, stable SK-* error codes with incident IDs, dual response shape (Skryx-native + legacy data-wrapper). No SDK lock-in — use whatever HTTP client your team already has.
// Search in 3 lines const r = await fetch("https://api.skryx.io/v1/indexes/products/query", { method: "POST", headers: { Authorization: "Bearer sk_live_…" }, body: JSON.stringify({ q: "sneakers" }) }); const { hits, found, search_mode } = await r.json();
Three scopes. No more.
Every API key carries one of three scopes. Keys are stored as SHA-256 hashes (full plaintext shown once at create time); a 12-char prefix is kept for display so you can tell keys apart in the UI. Rotate any key without breaking the others.
Read · write · admin.
Admin implies the rest.
index:read covers search + autocomplete only — safe to
ship in the browser. index:write adds document mutations
and data-source syncs (ingestion services). index:admin is
the umbrella for schema / synonyms / ranking rules / curator / lifecycle
endpoints.
- Key formats:
sk_live_{32}for search/write,sk_admin_{32}for admin - Scope checked by
CheckApiScopemiddleware on every protected route - 403 with
insufficient_scopeand a message listing the required scope
// API key shapes sk_live_a1b2c3d4e5f6g7h8… // search · write sk_admin_x9y8z7w6v5u4t3s2… // admin // Required scopes per resource GET /v1/indexes/:n/query none POST /v1/indexes/:n/documents index:write POST /v1/indexes/:n/synonyms index:admin DELETE /v1/indexes/:n index:admin
Eleven resources. Standard verbs.
Two paths, same body shape.
POST /v1/indexes/:name/query (or the /search
alias). Accepts both Skryx-native input (q +
filters{} + facets[] + sort)
and the legacy engine-style shape (query_by,
filter_by, etc.) — keeps existing integrations working
without translation.
- Optional
search_mode:auto / keyword / semantic / hybrid - Per-query
num_typos,prefix,fields/exclude_fields POST /v1/indexes/:name/suggestfor autocomplete (1–20 results)
POST /v1/indexes/products/query
{
"q": "wireless headphones",
"filters": { "brand": ["Bose", "Sony"] },
"facets": ["brand", "category"],
"sort": "price:asc",
"per_page": 20,
"search_mode": "auto"
}
Single & batch upsert. Partial PATCH.
POST single or /batch (no documented batch
size cap — validation requires min 1, in practice we recommend
batches of 500). Partial updates via PATCH. Successful
batch returns 200; mixed results return 207 with per-row errors so you
know exactly which documents failed and why.
POST /v1/indexes/:n/documents· singlePOST /v1/indexes/:n/documents/batch· array, 200 / 207PATCH /v1/indexes/:n/documents/:id· merge fields, re-embed if neededDELETE /v1/indexes/:n/documents/:id
POST /v1/indexes/products/documents/batch
{ "documents": [/* … */] }
// 207 partial response
{
"data": {
"imported": 498,
"failed": 2,
"errors": [
{ "id": "sku-x",
"reason": "missing field: title" }
],
"records_count": 26509
}
}
Build a new collection. Swap atomically.
Index Lifecycle exposes two endpoints. copy-settings-from
clones synonyms, ranking rules, and search settings from one index to
another (each set is opt-in). swap-with performs an atomic
alias swap so traffic moves to the new collection instantly, with the
option to drop the old one.
POST /v1/indexes/:n/copy-settings-from· clone synonyms / rules / settingsPOST /v1/indexes/:n/swap-with· atomic alias swap, optional drop- Zero downtime, no inconsistent reads, no warming step needed
POST /v1/indexes/products_v2/copy-settings-from
{
"source_index": "products",
"copy": ["synonyms", "ranking_rules"]
}
POST /v1/indexes/products/swap-with
{
"target_index": "products_v2",
"drop_source_after": true
}
Pin · replace · hide.
With A/B + scheduling.
Full CRUD on curated-result rules: triggers (exact / contains /
contains_all / regex), modes (pin / replace / hide), schedule windows,
A/B split percentage. Plus duplicate, toggle,
per-rule analytics, and a preview endpoint
that returns organic vs curated hits before you save anything.
- Multiple triggers per rule (exact / contains / contains_all / regex)
- A/B split percentage between curated and organic variants
- Per-rule analytics: impressions, clicks, CTR, last fired
POST /v1/indexes/products/curated-results
{
"name": "Holiday push",
"mode": "pin",
"pinned_product_ids": ["sku-1", "sku-2"],
"triggers": [
{ "type": "contains", "value": "sneakers" }
],
"scheduled_start": "2026-12-01",
"scheduled_end": "2026-12-31",
"ab_test_enabled": true,
"ab_test_split": 0.5
}
Enable, status, reindex, backfill.
Six endpoints to manage embeddings end-to-end. Pick a Skryx embedding
model at enable time, poll /status for live progress,
re-trigger a full pass with /reindex, or backfill missing
vectors after a schema bump with /embeddings/backfill.
GET / POSTsemantic-search/{status, enable, disable, reindex}GETembeddings/status· coverage % + countsPOSTembeddings/backfill· idempotent full pass
GET /v1/indexes/products/embeddings/status
{ "data": {
"enabled": true,
"model": "voyage-3-lite",
"dimensions": 512,
"total_documents": 26011,
"documents_with_embeddings": 26011,
"documents_pending": 0,
"documents_failed": 0,
"coverage_percentage": 100
}}
Full CRUD + test-connection + sync-now.
Manage data sources entirely through the API. Test a feed before saving it. Trigger an out-of-cycle sync. Walk the last 50 runs with per-run detail (products fetched / added / updated / deleted / failed, duration, error trace).
- Full CRUD on
/v1/indexes/:n/data-sources POST /data-sources/:id/sync-now· 202 + run IDGET /data-sources/:id/runs· last 50 with per-run drilldown
POST /v1/indexes/products/data-sources/
test-connection
{
"url": "https://shop…/feed.xml",
"format": "auto"
}
// → format_detected, total_products,
// preview (3 docs), default_mapping
One envelope, two doors.
Search responses ship in two shapes simultaneously: the Skryx-native
shape at the top level (the documented one for new integrations) and a
legacy data wrapper underneath that mirrors the older
engine-style payload. Existing integrations keep reading
data.hits; new ones use the cleaner top-level fields.
{
// Native shape — preferred for new code
"hits": [ /* … */ ],
"found": 142,
"page": 1,
"per_page": 20,
"total_pages": 8,
"facets": { "brand": { "Sony": 42, "Bose": 38 } },
"search_time_ms": 18,
"search_mode": "hybrid",
"ai_enhanced": true,
"ai_context": {
"original_query": "laptop for vid edting",
"rewritten_query": "laptop for video editing",
"intent": "compare",
"alternative_queries": ["4k editing laptop", "creator laptop"]
},
"suggestions": ["video editing laptop"],
"related_hits": [ /* secondary tier · similarity 0.50–0.65 */ ],
"curated_applied": false,
// Legacy data wrapper — keeps older integrations working
"data": { "hits": […], "out_of": 26011, "facet_counts": […], "embed_time_ms": 3 }
}
Stable codes. Incident IDs.
Same code = same root cause. Forever.
Every error response carries a stable SK-* code so your
error-handling code can match on it without parsing English. Each one
also includes an incident_id you can quote when emailing
support so we look up the exact request in our logs.
SK-AI-{503, 429, 422, 504}— AI / embedding pipelineSK-SE-{503, 400, 504, 404}— search engineSK-DS-{503, 422, 504, 401}— data sourcesSK-SYS-{500, 503, 429}— platform
{
"error": "SK-SE-400",
"message": "Invalid filter_by expression",
"incident_id": "INC-2026-05-25-a8f2",
"details": {
"field": "filter_by",
"reason": "unbalanced parentheses"
}
}
Two layers. Both observable from headers.
Per-IP burst (60 req / sec, soft cap) returns 429 with
X-RateLimit-Limit / Remaining / Retry-After. Per-tenant
monthly quota returns 429 with a structured body — current_usage,
quota, reset_at, upgrade_url —
or, on overage-enabled plans, bills instead of failing.
HTTP/1.1 429 Too Many Requests
X-RateLimit-Limit: 60
X-RateLimit-Remaining: 0
Retry-After: 0
{
"error": "quota_exceeded",
"current_usage": 100242,
"quota": 100000,
"reset_at": "2026-06-01T00:00:00Z",
"upgrade_url": "https://app.skryx.io/billing"
}
If it speaks HTTP, it speaks Skryx.
There are no published SDKs yet — language-specific clients for
JavaScript, Python, PHP, Go, Ruby are on the roadmap. In the meantime, the
contract is plain JSON over HTTPS: every example in the docs is a
fetch / requests / Http /
net/http snippet that runs without any dependency install.
// JavaScript / TypeScript const r = await fetch(API + "/v1/indexes/products/query", { method: "POST", headers: { Authorization: "Bearer " + key }, body: JSON.stringify({ q }) });
# Python import requests r = requests.post( f"{API}/v1/indexes/products/query", headers={"Authorization": f"Bearer {key}"}, json={"q": q}, ).json()
// PHP / Laravel $hits = Http::withToken($key) ->post("$API/v1/indexes/products/query", ['q' => $q]) ->json('hits');
// Go body, _ := json.Marshal(map[string]any{ "q": q, }) req, _ := http.NewRequest("POST", API+"/v1/indexes/products/query", bytes.NewReader(body)) req.Header.Set("Authorization", "Bearer "+key)
v1. Honest about what's missing.
api.skryx.io/v1 · response shape carries native + legacy doors