API documentation
Snyf API Reference
Use these endpoints to manage API keys, upload catalogs, poll ingestion jobs, and search enriched collections.
Authentication
Catalog upload, job polling, and search use the customer API key in the x-api-key header.
x-api-key: $API_KEY
/collections/{collection_id}/upload
Uploads a catalog CSV to the collection named in the URL and queues accepted rows for enrichment.
Required for every upload. Send only the file name, not an S3 URI.
curl -sS -X POST "$API_URL/collections/coll_ss25/upload" \
-H "x-api-key: $API_KEY" \
-F "csv_file=@catalog.csv" \
-F "attribute_list_id=fashion-v1"
import os
import requests
API_URL = os.environ["API_URL"]
API_KEY = os.environ["API_KEY"]
COLLECTION_ID = os.environ["COLLECTION_ID"]
with open("catalog.csv", "rb") as catalog:
response = requests.post(
f"{API_URL}/collections/{COLLECTION_ID}/upload",
headers={"x-api-key": API_KEY},
files={"csv_file": catalog},
data={"attribute_list_id": "fashion-v1"},
timeout=30,
)
response.raise_for_status()
print(response.json())
const API_URL = "https://xydj4rm9eg.execute-api.us-east-1.amazonaws.com";
const API_KEY = "YOUR_API_KEY";
const COLLECTION_ID = "coll_ss25";
async function uploadCatalog(file) {
const formData = new FormData();
formData.append("csv_file", file);
formData.append("attribute_list_id", "fashion-v1");
const response = await fetch(`${API_URL}/collections/${COLLECTION_ID}/upload`, {
method: "POST",
headers: { "x-api-key": API_KEY },
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
return response.json();
}
type UploadResponse = {
job_id: string;
accepted_rows: number;
rejected_rows: number;
status: string;
};
const API_URL = "https://xydj4rm9eg.execute-api.us-east-1.amazonaws.com";
const API_KEY = "YOUR_API_KEY";
const COLLECTION_ID = "coll_ss25";
export async function uploadCatalog(file: File): Promise<UploadResponse> {
const formData = new FormData();
formData.append("csv_file", file);
formData.append("attribute_list_id", "fashion-v1");
const response = await fetch(`${API_URL}/collections/${COLLECTION_ID}/upload`, {
method: "POST",
headers: { "x-api-key": API_KEY },
body: formData
});
if (!response.ok) {
throw new Error(`Upload failed: ${response.status}`);
}
return response.json() as Promise<UploadResponse>;
}
package main
import (
"bytes"
"fmt"
"io"
"mime/multipart"
"net/http"
"os"
)
func main() {
apiURL := os.Getenv("API_URL")
apiKey := os.Getenv("API_KEY")
collectionID := os.Getenv("COLLECTION_ID")
var body bytes.Buffer
writer := multipart.NewWriter(&body)
file, err := os.Open("catalog.csv")
if err != nil {
panic(err)
}
defer file.Close()
part, err := writer.CreateFormFile("csv_file", "catalog.csv")
if err != nil {
panic(err)
}
if _, err := io.Copy(part, file); err != nil {
panic(err)
}
writer.WriteField("attribute_list_id", "fashion-v1")
writer.Close()
req, err := http.NewRequest("POST", apiURL+"/collections/"+collectionID+"/upload", &body)
if err != nil {
panic(err)
}
req.Header.Set("x-api-key", apiKey)
req.Header.Set("Content-Type", writer.FormDataContentType())
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
panic(fmt.Sprintf("upload failed: %s", resp.Status))
}
fmt.Println(resp.Status)
}
| Input | Required | Notes |
|---|---|---|
| csv_file | Yes | CSV file with product rows. |
| attribute_list_id | Yes | Attribute-list file name, not an S3 URI. |
Required CSV columns: product_id, name, image_urls. Do not include collection_id in the CSV; the upload URL supplies it.
Optional CSV columns: category, description, price, currency, brand, _deleted. Use attribute_list_id in the upload form/API instead of an attributes CSV column.
Response
{
"job_id": "job_coll_ss25_a1b2c3d4e5f6",
"accepted_rows": 3,
"rejected_rows": 0,
"warnings": 0,
"status": "QUEUED",
"issues": []
}
/ingestion-jobs/{job_id}
Polls an upload job until products become searchable.
curl -sS "$API_URL/ingestion-jobs/$JOB_ID" \
-H "x-api-key: $API_KEY"
Response
{
"job_id": "job_coll_ss25_a1b2c3d4e5f6",
"status": "READY",
"accepted_rows": 3,
"queued_rows": 0,
"processing_rows": 0,
"enriched_rows": 3,
"failed_rows": 0,
"deleted_rows": 0,
"products": [
{
"collection_id": "coll_ss25",
"product_id": "prod_001",
"name": "Linen Wrap Dress",
"status": "ENRICHED"
}
]
}
/ingestion-jobs/{job_id}/errors
curl -sS "$API_URL/ingestion-jobs/$JOB_ID/errors" \
-H "x-api-key: $API_KEY"
/search
Searches one collection and returns ranked product results with image URLs.
curl -sS -X POST "$API_URL/search" \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-d '{
"collection_id": "coll_ss25",
"query": "flowy beach wedding look",
"top_k": 5
}'
import os
import requests
API_URL = os.environ["API_URL"]
API_KEY = os.environ["API_KEY"]
response = requests.post(
f"{API_URL}/search",
headers={"Content-Type": "application/json", "x-api-key": API_KEY},
json={
"collection_id": "coll_ss25",
"query": "flowy beach wedding look",
"top_k": 5,
},
timeout=30,
)
response.raise_for_status()
print(response.json())
const API_URL = "https://xydj4rm9eg.execute-api.us-east-1.amazonaws.com";
const API_KEY = "YOUR_API_KEY";
async function searchProducts(query) {
const response = await fetch(`${API_URL}/search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY
},
body: JSON.stringify({
collection_id: "coll_ss25",
query,
top_k: 5
})
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status}`);
}
return response.json();
}
searchProducts("flowy beach wedding look").then(console.log);
type SearchResult = {
product_id: string;
name: string;
image_urls: string[];
score: number;
};
type SearchResponse = {
collection_id: string;
query: string;
results: SearchResult[];
};
const API_URL = "https://xydj4rm9eg.execute-api.us-east-1.amazonaws.com";
const API_KEY = "YOUR_API_KEY";
export async function searchProducts(query: string, topK = 5): Promise<SearchResponse> {
const response = await fetch(`${API_URL}/search`, {
method: "POST",
headers: {
"Content-Type": "application/json",
"x-api-key": API_KEY
},
body: JSON.stringify({
collection_id: "coll_ss25",
query,
top_k: topK
})
});
if (!response.ok) {
throw new Error(`Search failed: ${response.status}`);
}
return response.json() as Promise<SearchResponse>;
}
package main
import (
"bytes"
"encoding/json"
"fmt"
"net/http"
"os"
)
func main() {
apiURL := os.Getenv("API_URL")
apiKey := os.Getenv("API_KEY")
body, _ := json.Marshal(map[string]any{
"collection_id": "coll_ss25",
"query": "flowy beach wedding look",
"top_k": 5,
})
req, err := http.NewRequest("POST", apiURL+"/search", bytes.NewReader(body))
if err != nil {
panic(err)
}
req.Header.Set("Content-Type", "application/json")
req.Header.Set("x-api-key", apiKey)
resp, err := http.DefaultClient.Do(req)
if err != nil {
panic(err)
}
defer resp.Body.Close()
if resp.StatusCode >= 400 {
panic(fmt.Sprintf("search failed: %s", resp.Status))
}
var result map[string]any
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
panic(err)
}
fmt.Printf("%+v\n", result)
}
| Field | Required | Notes |
|---|---|---|
| collection_id | Yes | Must belong to the API-key customer. |
| query | Yes | Natural-language product search. |
| top_k | No | Defaults to 20. Maximum is 100. |
Response
{
"collection_id": "coll_ss25",
"query": "flowy beach wedding look",
"results": [
{
"product_id": "prod_001",
"name": "Linen Wrap Dress",
"image_urls": ["https://cdn.example.com/prod_001.jpg"],
"category": "dress",
"brand": "Example",
"price": 148,
"currency": "USD",
"score": 0.84,
"feature_scores": {
"occasion": 0.91,
"silhouette": 0.78
},
"enriched_attrs": {
"occasion": "beach wedding",
"silhouette": "flowy"
}
}
],
"metrics": {
"api_total_ms": 823.41,
"search": {
"total_ms": 790.0,
"steps_ms": {
"embed_query": 96.31,
"extract_query_attributes": 488.17,
"vector_retrieve_documents": 42.89,
"embed_query_rerank_attributes": 126.48,
"rerank_candidates": 0.42
},
"embedding_calls": 2,
"embedding_cache_hits": 4
}
}
}
Search uses OpenSearch kNN retrieval for candidate products. Query embeddings, query-attribute embeddings, and parsed query attributes are cached first in the running service and then in a shared DynamoDB cache, so repeated searches can skip model calls.
/search/keyword
Runs a lexical baseline over product names, descriptions, categories, brands, enriched text, and extracted attributes. Request and response shape match /search.
curl -sS -X POST "$API_URL/search/keyword" \
-H "Content-Type: application/json" \
-H "x-api-key: $API_KEY" \
-d '{
"collection_id": "coll_ss25",
"query": "linen dress",
"top_k": 5
}'
/search/demo
Public website demo search. It does not take an API key or collection ID; the backend locks it to the demo collection so no secret is exposed in browser code. Public demo requests are still rate limited.
curl -sS -X POST "$API_URL/search/demo" \
-H "Content-Type: application/json" \
-d '{
"query": "flowy beach wedding look"
}'
/search/demo/keyword
Public keyword baseline for the same demo collection. Customer apps should use the protected endpoints above.
Errors And Limits
| Plan | Searches / month | Ingestion rows / month | Active API keys |
|---|---|---|---|
| FREE | 100 | 1,000 | 1 |
| PRO | 10,000 | 100,000 | 5 |
| ENTERPRISE | 1,000,000 | 10,000,000 | 25 |
CSV headers are not counted toward ingestion row limits. API keys can be regenerated, but each plan caps the number of active keys.
Error Shape
{
"detail": "Invalid email or password."
}
Plan Limit Error
{
"detail": "search_requests_per_month exceeded for FREE plan. Please upgrade your plan."
}