Saved Search Indexing API
Upload, update, and monitor the saved search alerts that power Unbxd's percolation matching. #
Overview
Shoppers save a search once and get notified when a new or restocked product matches it. Saved searches (also called claims) are uploaded to Unbxd as a JSONL file. The Feed API accepts the file, stores it, and triggers indexing. You then poll the status API until the job finishes.
This guide covers the three APIs used to load and monitor the saved-search index.
| Operation | HTTP method | Effect |
|---|---|---|
| Full upload | POST | Rebuilds the site's saved search index from this file. Replaces all prior queries. |
| Delta upload | PUT | Upserts claims from this file into the existing index. Keeps prior queries. Requires an existing index from a prior successful full upload. |
| Status | GET | Returns recent or single upload status for the saved search index. |
Workflow
- You upload a JSONL file of saved searches (full or delta) to the Feed API.
- The indexing pipeline validates claims, builds or updates the saved search index, then cleans up.
- You poll status until the job is
INDEXED(orFAILED/REJECTED). - Live listings are matched against the active index, and matches can be delivered through webhooks.
API versionUse
v2.1only. Older/v1.0/{site}/saved-search/uploadpaths are not the supported integration contract for this feature.
Base URL and path pattern
Base URL (example): https://<feed-api-host>
Path pattern: /v2.1/sites/{siteKey}/indexes/saved-search/...
Use the Feed API host provided for your environment (QA or production).
Authentication
All requests require the site API key.
| Header | Value |
|---|---|
Authorization | <API_KEY> |
Also accepted in some setups: x-unbxd-authkey, unbxd-api-key (same API key value). Prefer Authorization.
Feed File Format (Shared by Full and Delta)
Content type and transport
| Item | Requirement |
|---|---|
| HTTP body | multipart/form-data |
| Form field name | file (required) |
| File content | JSONL, one JSON object per line |
| Compression | Not supported (do not zip) |
| Encoding | UTF-8 |
Claim object schema
Each line is one saved search (claim).
| Field | Required | Type | Description |
|---|---|---|---|
id | Yes | string | Unique saved search / claim id. Used as the query id in the index. |
memberId | Yes | number or string | Owner / member identifier. |
name | Yes | string | Display name for the saved search. |
q | No | string or null | Keyword query. If missing, null, or blank, indexing treats it as filter only (q=*). |
filter | No | object | Structured filters (facet/equality, ranges, geo). Unknown catalog fields are rejected unless configured otherwise. |
Records missing id, memberId, or name are marked invalid. If no valid records remain after validation, the upload fails.
Examples
Text query plus filters:
{"id":"A1","memberId":1,"name":"RV humidifiers","q":"humidifier","filter":{"zip":"84655","price":{"From":50,"To":100}}}
Filter only, no keyword:
{"id":"B2","memberId":2,"name":"Cars in UT","q":null,"filter":{"category":["Cars"],"state":"UT"}}
Geo distance (circular radius):
{
"id": "ALERT-USER-3144509",
"memberId": 2496898,
"name": "Humidifier",
"q": "Humidifier",
"filter": {
"price": { "From": 50, "To": 100 },
"geo_distance": {
"latLon": "40.0562,-111.7546",
"distance_km": "25"
}
}
}
| geo_distance field | Meaning |
|---|---|
latLon | Center as "lat,lon" |
distance_km | Radius in kilometers |
Not the same as query time geo search
geo_distancehere is matched during percolation: it decides whether a newly indexed or updated catalog document falls inside a saved alert's radius. It is a different mechanism fromgeo.filter,near(), andin(), which run at live query time in the Search API. Don't use one syntax in place of the other.
Multiple records in one file:
{"id":"A1","memberId":1,"name":"Alert 1","q":"humidifier","filter":{"zip":"84655"}}
{"id":"A2","memberId":1,"name":"Alert 2","q":"generator","filter":{"state":"UT"}}
Do not wrap the file in a JSON array ([{...},{...}]). Use one object per line.
Full Upload API
Rebuilds the saved search index from the uploaded file.
Contract
| Item | Value |
|---|---|
| Method | POST |
| URL | /v2.1/sites/{siteKey}/indexes/saved-search/file |
| Auth | Authorization: <API_KEY> |
| Body | multipart/form-data with field file |
Path parameters
| Parameter | Description |
|---|---|
siteKey | Unbxd site key (e.g. ss-example-site) |
Example
curl -X POST \
"https://<feed-api-host>/v2.1/sites/<SITE_KEY>/indexes/saved-search/file" \
-H "Authorization: <API_KEY>" \
-F "file=@/path/to/saved-searches.jsonl"
Typical success response (HTTP 200)
The Feed API returns file metadata after accepting the upload (shape may include):
{
"id": "251cc103-ec2f-4d75-8828-d7bb873ef824",
"site": "<SITE_KEY>",
"source": "s3",
"file": {
"name": "<generated-file-name>.json",
"path": "<storage-path>",
"args": {}
}
}
| Field | Meaning |
|---|---|
id | Upload / feed id, used with the status API |
site | Site key |
file.name | Stored file name |
file.path | Storage path |
HTTP 200 means the file was accepted, not that indexing finished. Poll status untilINDEXEDorFAILED.
Concurrent uploads
Only one full saved search upload may run at a time per site. If another full upload is in progress, the new upload may be rejected, with status REJECTED and a message such as "multiple uploads not allowed." Wait for the prior job to reach a terminal status, then retry.
Delta Upload API
Applies incremental creates and updates to an existing saved search index.
Contract
| Item | Value |
|---|---|
| Method | PUT |
| URL | /v2.1/sites/{siteKey}/indexes/saved-search/file |
| Auth | Authorization: <API_KEY> |
| Body | Same as full upload: multipart/form-data, field file, JSONL content |
Prerequisites
- A successful full upload must have completed at least once for this site's saved search index.
- Same claim schema and JSONL rules as full upload.
Example
curl -X PUT \
"https://<feed-api-host>/v2.1/sites/<SITE_KEY>/indexes/saved-search/file" \
-H "Authorization: <API_KEY>" \
-F "file=@/path/to/saved-searches-delta.jsonl"
Response
Same acceptance pattern as full upload: HTTP 200 with an id to poll.
Concurrency notes
- While a full upload is running, deltas may be rejected or queued depending on lock state.
- Recommended order: finish the full upload, confirm it reaches
INDEXED, then send deltas.
Status API
List Recent Statuses
| Item | Value |
|---|---|
| Method | GET |
| URL | /v2.1/sites/{siteKey}/indexes/saved-search/status |
| Auth | Authorization: <API_KEY> |
Query parameters
| Parameter | Required | Default | Max | Description |
|---|---|---|---|---|
count | No | 20 | 100 | Number of status records to return |
Example
curl \
"https://<feed-api-host>/v2.1/sites/<SITE_KEY>/indexes/saved-search/status?count=5" \
-H "Authorization: <API_KEY>"
Single Upload Status
| Item | Value |
|---|---|
| Method | GET |
| URL | /v2.1/sites/{siteKey}/indexes/saved-search/status/{id} |
| Auth | Authorization: <API_KEY> |
Example
curl \
"https://<feed-api-host>/v2.1/sites/<SITE_KEY>/indexes/saved-search/status/<UPLOAD_ID>" \
-H "Authorization: <API_KEY>"
Status response fields
Returned as an array (list) or a single object:
{
"timestamp": 1785742108000,
"id": "251cc103-ec2f-4d75-8828-d7bb873ef824",
"fileName": "classified-saved-search-02.json",
"status": "INDEXED",
"origin": "saved-search-full-upload",
"message": "success",
"exec_code": 200,
"duration": "28 seconds",
"context": {
"site": "<SITE_KEY>",
"index": "saved-search",
"code": 14
},
"latest_stage": "cleanup",
"stages": [
{ "name": "analyzer", "timestamp": 1785742125870 },
{ "name": "indexer", "timestamp": 1785742153595 },
{ "name": "cleanup", "timestamp": 1785742177582 }
],
"error": []
}
| Field | Description |
|---|---|
id | Upload / feed id |
fileName | Stored file name |
status | High level job state (see below) |
origin | saved-search-full-upload or saved-search-delta-upload |
message | Human readable summary (success, failed, processing, etc.) |
exec_code | 200 for success, 400 or 500 on failure classes |
duration | Elapsed processing time, when available |
context.site | Site key |
context.index | Always saved-search for these APIs |
context.code | Internal code (14 for full, 15 for delta) |
latest_stage | Last reported pipeline stage |
stages | Stage timeline |
error | Error messages when failed (may be empty) |
Status values
| Status | Meaning |
|---|---|
ACCEPTED | File accepted, pipeline not finished |
INDEXING | Processing in progress |
INDEXED | Completed successfully (terminal success) |
FAILED | Completed with errors (terminal failure) |
REJECTED | Not executed, for example due to a lock or a concurrent upload conflict |
Pipeline stages (typical)
| Stage | Role |
|---|---|
analyzer | Validate claims, compile queries |
indexer | Build or update the Lucene Monitor index |
cleanup | Workflow cleanup. Success usually ends at this stage with INDEXED. |
Recommended polling
- Capture
idfrom the upload response. - Poll
GET .../status/{id}every few seconds. - Stop when status is
INDEXED,FAILED, orREJECTED. - On
FAILED, inspectmessageanderror.
End to End Integration Flow
- Prepare a JSONL feed. Required fields:
id,memberId,name. - Send
POST .../indexes/saved-search/filefor a full rebuild, orPUT .../indexes/saved-search/filefor a delta upsert. - Read
response.id. - Poll
GET .../indexes/saved-search/status/{id}. - When status is
INDEXED, the index is live for percolation and matching. - When status is
FAILEDorREJECTED, fix the feed or retry after the prior job completes.
For first time site setup, run a full upload first, then use delta uploads for incremental changes.
Error and Validation Checklist
| Issue | Symptom or tip |
|---|---|
Missing @ in curl -F | File not uploaded; path sent as a plain string |
| JSON array instead of JSONL | Validation fails, zero valid claims |
Missing id, memberId, or name | Invalid claims; job fails if none are valid |
| Unknown filter fields | Claim rejected (must match site schema) |
Bad geo_distance | Claim rejected |
| Concurrent full upload | REJECTED, wait for the prior job |
| Delta with no prior full upload | Indexing may fail, run a full upload first |
Wrong API version (/v1.0/.../saved-search/upload) | Not supported for this integration |
Quick Reference
| API | Method | Endpoint |
|---|---|---|
| Full upload | POST | /v2.1/sites/{siteKey}/indexes/saved-search/file |
| Delta upload | PUT | /v2.1/sites/{siteKey}/indexes/saved-search/file |
| List status | GET | /v2.1/sites/{siteKey}/indexes/saved-search/status?count={n} |
| One status | GET | /v2.1/sites/{siteKey}/indexes/saved-search/status/{uploadId} |
Common headers and body
Authorization: <API_KEY>- Body (uploads):
multipart/form-data, field[email protected] - File: UTF-8 JSONL, not zipped
Support Information to Collect
If a job fails, gather the following before opening a ticket:
- Site key
- Upload id
- Request timestamp and environment (QA or production)
- Sample of failing JSONL lines (redact PII as needed)
- Status payload for that id (
status,message,error,stages)
Related Reading
Updated about 12 hours ago
