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.

OperationHTTP methodEffect
Full uploadPOSTRebuilds the site's saved search index from this file. Replaces all prior queries.
Delta uploadPUTUpserts claims from this file into the existing index. Keeps prior queries. Requires an existing index from a prior successful full upload.
StatusGETReturns recent or single upload status for the saved search index.

Workflow

  1. You upload a JSONL file of saved searches (full or delta) to the Feed API.
  2. The indexing pipeline validates claims, builds or updates the saved search index, then cleans up.
  3. You poll status until the job is INDEXED (or FAILED / REJECTED).
  4. Live listings are matched against the active index, and matches can be delivered through webhooks.
📘

API version

Use v2.1 only. Older /v1.0/{site}/saved-search/upload paths 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.

HeaderValue
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

ItemRequirement
HTTP bodymultipart/form-data
Form field namefile (required)
File contentJSONL, one JSON object per line
CompressionNot supported (do not zip)
EncodingUTF-8

Claim object schema

Each line is one saved search (claim).

FieldRequiredTypeDescription
idYesstringUnique saved search / claim id. Used as the query id in the index.
memberIdYesnumber or stringOwner / member identifier.
nameYesstringDisplay name for the saved search.
qNostring or nullKeyword query. If missing, null, or blank, indexing treats it as filter only (q=*).
filterNoobjectStructured 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 fieldMeaning
latLonCenter as "lat,lon"
distance_kmRadius in kilometers
⚠️

Not the same as query time geo search

geo_distance here 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 from geo.filter, near(), and in(), 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

ItemValue
MethodPOST
URL/v2.1/sites/{siteKey}/indexes/saved-search/file
AuthAuthorization: <API_KEY>
Bodymultipart/form-data with field file

Path parameters

ParameterDescription
siteKeyUnbxd 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": {}
  }
}
FieldMeaning
idUpload / feed id, used with the status API
siteSite key
file.nameStored file name
file.pathStorage path
📘

HTTP 200 means the file was accepted, not that indexing finished. Poll status until INDEXED or FAILED.

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

ItemValue
MethodPUT
URL/v2.1/sites/{siteKey}/indexes/saved-search/file
AuthAuthorization: <API_KEY>
BodySame 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

ItemValue
MethodGET
URL/v2.1/sites/{siteKey}/indexes/saved-search/status
AuthAuthorization: <API_KEY>

Query parameters

ParameterRequiredDefaultMaxDescription
countNo20100Number 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

ItemValue
MethodGET
URL/v2.1/sites/{siteKey}/indexes/saved-search/status/{id}
AuthAuthorization: <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": []
}
FieldDescription
idUpload / feed id
fileNameStored file name
statusHigh level job state (see below)
originsaved-search-full-upload or saved-search-delta-upload
messageHuman readable summary (success, failed, processing, etc.)
exec_code200 for success, 400 or 500 on failure classes
durationElapsed processing time, when available
context.siteSite key
context.indexAlways saved-search for these APIs
context.codeInternal code (14 for full, 15 for delta)
latest_stageLast reported pipeline stage
stagesStage timeline
errorError messages when failed (may be empty)

Status values

StatusMeaning
ACCEPTEDFile accepted, pipeline not finished
INDEXINGProcessing in progress
INDEXEDCompleted successfully (terminal success)
FAILEDCompleted with errors (terminal failure)
REJECTEDNot executed, for example due to a lock or a concurrent upload conflict

Pipeline stages (typical)

StageRole
analyzerValidate claims, compile queries
indexerBuild or update the Lucene Monitor index
cleanupWorkflow cleanup. Success usually ends at this stage with INDEXED.

Recommended polling

  1. Capture id from the upload response.
  2. Poll GET .../status/{id} every few seconds.
  3. Stop when status is INDEXED, FAILED, or REJECTED.
  4. On FAILED, inspect message and error.

End to End Integration Flow

  1. Prepare a JSONL feed. Required fields: id, memberId, name.
  2. Send POST .../indexes/saved-search/file for a full rebuild, or PUT .../indexes/saved-search/file for a delta upsert.
  3. Read response.id.
  4. Poll GET .../indexes/saved-search/status/{id}.
  5. When status is INDEXED, the index is live for percolation and matching.
  6. When status is FAILED or REJECTED, 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

IssueSymptom or tip
Missing @ in curl -FFile not uploaded; path sent as a plain string
JSON array instead of JSONLValidation fails, zero valid claims
Missing id, memberId, or nameInvalid claims; job fails if none are valid
Unknown filter fieldsClaim rejected (must match site schema)
Bad geo_distanceClaim rejected
Concurrent full uploadREJECTED, wait for the prior job
Delta with no prior full uploadIndexing may fail, run a full upload first
Wrong API version (/v1.0/.../saved-search/upload)Not supported for this integration

Quick Reference

APIMethodEndpoint
Full uploadPOST/v2.1/sites/{siteKey}/indexes/saved-search/file
Delta uploadPUT/v2.1/sites/{siteKey}/indexes/saved-search/file
List statusGET/v2.1/sites/{siteKey}/indexes/saved-search/status?count={n}
One statusGET/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


Did this page help you?