nyris API Documentation

Integrating nyris? It takes two steps: import your product data into the nyris index (Product Data & Images), then integrate nyris into your website or app (Integration (for humans)). No data in nyris yet? Ask support@nyris.io for an API key to a demo index and start with the frontend.

For AI coding agents. Use the Agent Guide section, or download it as Markdown. That file is the spec for endpoints, image handling, and request formats. Do not invent endpoints from the rest of this page.

Search UI assets. Icons for camera, drop zone, upload, crop, search, and the controls around results are free to reuse. They are one color (currentColor) on a 24 × 24 canvas. Download the asset pack, or use a single file from search-ui/.

Camera Drop zone Upload Crop Search Similar search Filter Close Add Remove Loading

Welcome to nyris' API Documentation. nyris is a high performance visual product search, object detection and visual recommendations engine for retail and industry. This documentation covers our visual product search engine and the visual recommendations engine.


The implementation in your website or app comprises three steps.

1. Submit your product & image data
In order to recognize your products, we need to know your products. The product feed section covers all details how to submit your product data to us and the specifications for your product or object images.

2. Implement nyris API in your app, website or messenger bot
The nyris engine can be integrated in your app, website, messenger bots, BI solutions - wherever you want to make your customers happy or optimize your data management process. We provide you with code examples on how to query our Find API and Regions API and have ready to go SDKs for iOS and Android.

3. Implement automated feedback in your app, website or messenger bot
By tying user interactions to automated feedback in our Feedback API, you allow for automatic statistic generation and continuous improvement of the search results.

To get started, email us at mailto:support@nyris.io. Our team will provision API keys to access our API. Always feel free to contact us should you have questions or suggestions for our service or this documentation. In addition, you can always find our open source code github.com/nyris.

Try it with Postman — You can explore and test the nyris API directly using our public Postman workspace. It includes ready-to-use collections for Find, Recommend, Regions, and Images APIs.

Thank You & happy coding!
Markus (CTO / Founder)

Inputs you must have

Ask before coding if any of these are missing:

Input Required Default
Find API key yes environment variable NYRIS_API_KEY
Base URL no https://api.nyris.io
Target yes web, iOS, Android, React Native, Flutter, or backend
Text search no off
Filters no none
Existing text search engine and results page (Algolia, Elasticsearch, ...) no none. If there is one, follow Existing text search instead of building a separate results page.

Never hard-code the key. Never print it. Never commit it.

The key is sent from the client as X-Api-Key. It must be a Find key. Refuse to embed a Data Management key in a client app.

Endpoints

Name Request
Find POST {base}/find/v1.1
Regions POST {base}/find/v2/regions
Feedback POST {base}/feedback/v1
Filters GET {base}/find/v1/filters?limit=100
Recommend GET {base}/recommend/v1/{sku}
Product images GET {base}/find/v1.1/images/{sku}

Filters: use /find/v1/filters. The response is a JSON array of { "key": "color", "values": ["Red", "Blue"] } (empty if the index has no filters). To search the values of one key, call GET {base}/find/v1/filters/{key}/{value}?limit=100, where {value} is the search term; it returns string[].

CAD search (POST {base}/cad/find/v0.2, multipart field file) is not part of the public API reference. Use it only if the user confirms that nyris enabled CAD search for their account.

Request body formats

Find and Regions accept the image in three formats. The results are the same.

Format Content-Type Body Use when
Multipart (default) multipart/form-data part image, optional text, optional filter fields You need text or filters. Browsers and mobile HTTP clients support it natively.
Binary image/jpeg or image/png the raw image bytes Image only, no text or filters. Smallest request.
JSON (base64) application/json {"image": "<base64>", "text": "...", "filters": [...]} The client or platform only speaks JSON (low-code tools, serverless functions, tool-calling backends, some SDK wrappers).

JSON body schema:

Field Type Required Notes
image string yes, unless the search is text only Plain base64 (/9j/4AAQ...) or a data URI (data:image/jpeg;base64,/9j/...).
text string no Max 512 characters.
filters array no [{"key": "MachineSerial", "values": ["SN-40821"]}]. Same AND/OR rules as multipart.

Regions JSON takes only image.

Base64 makes the request about 33% larger. The 2 MB limit applies to the decoded image, not the encoded body. Still resize before encoding exactly as in step 4. Default to multipart. Use JSON when the user asks for it or the platform cannot send multipart or binary bodies.

Do the base64 encoding in code. If you are an agent that can call HTTP tools, never put image base64 into your own output or tool arguments by hand. It wastes tokens and gets corrupted.

Normative steps

Implement in this order.

  1. Capture. User picks a file or a camera frame. Show that image immediately. Do not wait for the network to show the preview.
  2. Normalize and keep. Decode the bitmap. Apply EXIF orientation. Flatten transparency onto white (rgb(255,255,255)). Keep this normalized original at full resolution in memory until the user removes the image. Show a screen-sized preview derived from it. Never crop the preview or the step 4 JPEG; crops come from this original only.
  3. Convert formats.
    • JPEG, PNG, WebP, GIF first frame, AVIF, BMP: decode as a bitmap.
    • HEIC/HEIF: decode with the platform, createImageBitmap, or heic-to / libheif, then continue as a bitmap.
    • TIFF: first page only. If pixels are 3-channel RGB, append alpha 255. Most browsers cannot decode TIFF natively; on the web, add a TIFF decoder or reject TIFF with a clear message. Do not list it in the file picker if you cannot decode it.
    • PDF: rasterize page 1, then continue as a bitmap.
    • Extensions .stp, .step, .stl, .obj, .glb, .gltf: do not send them to /find/v1.1. If CAD search is enabled for the account, POST the original file to /cad/find/v0.2 as field file. Otherwise treat it as unsupported.
    • Try the native decoder first for every other file. Route by extension only for CAD. Do not reject HEIC, AVIF, or other image types by name; the platform may already decode them.
    • Anything that still fails to decode: stop and tell the user the format is unsupported.
    • Always send JPEG. The API itself accepts only JPEG, PNG, and WebP; anything else returns 415 (binary or multipart) or 400 (JSON base64). Base64 does not change this: the bytes inside must still be JPEG.
  4. Resize. Check the source (or the crop) in original pixels. If its shortest edge is under 100 px, still search, but show a non-blocking hint that results may be less accurate. Scale with the area formula below so the output area is 1024 * 1024 (the example client always does this, including small sources). Encode JPEG at quality 0.9. Target size is about 100 kB; keep it at most 1 MB for latency. If the blob is still above ~200 kB, re-encode at 0.8. The API rejects images over 2 MB (2,097,152 bytes) with 413.
  5. Regions. POST /find/v2/regions with Content-Type: image/jpeg and the raw JPEG as the body. Multipart (part image) and JSON base64 also work; see Request body formats. Map pixel boxes to normalized 0..1 coordinates. If the list is empty, the crop is the full frame. If Regions fails, continue with the full frame; never block Find on Regions. As soon as Regions returns, auto-select the best region, mark its marker active, and run step 6 with that crop without waiting for a click. Draw the overlay as specified in Region overlay.
  6. Find. POST /find/v1.1 as multipart/form-data (or binary / JSON base64, see Request body formats). Field image is the JPEG. If there is a selected region that is not the full frame, crop the normalized original from step 2 (not the step 4 JPEG), then resize per step 4, then send that JPEG. Header Accept: application/items.complete+json for product fields; without it you get the minimal { sku, score } format. Header X-Api-Key.
  7. Text. If the user typed a query, append multipart field text (or JSON field text). Keep it under 512 characters. Do not put text in the query string; a query-string text returns results: []. Exception: with an existing text search engine that handles the keyword, do not send text to nyris (see Existing text search).
  8. Render. Read results[]. Identity is sku, else key, else objectID. Show a skeleton list while the request is in flight. On a new search, clear the previous list. HTTP 200 with an empty list is an empty state, not an error. Keep the order of results[]; it is the visual similarity order. Do not show score (or a percentage) on result cards. It is an internal ranking signal and misleads users. If the site already has a text search results page, render there; see Existing text search.
  9. Stale responses. If the user searches again, ignore or abort the older request.
  10. Session (optional). If the Find response has session, send it as header X-Session (or query ?session=) on follow-up searches, for example a crop search after a full-frame search. Only send ids the API returned; an invented value returns HTTP 400.
  11. Feedback (optional). POST /feedback/v1 with Content-Type: application/json. Required: request_id (Find id), timestamp (ISO 8601 UTC), event (click, conversion, feedback, or region). Optional: session_id (Find session) and an event-specific data object. See Feedback event. The ids are also in the response headers X-Matching-Request and X-Session, but browsers may not expose those headers, so read the body.

Without an existing text search engine, you may start Find (full frame) and Regions in parallel after step 4. When the user changes the box, run step 6 again with the crop. Cancel the in-flight Find first.

Feedback event

{
  "request_id": "<id from find>",
  "session_id": "<session from find>",
  "timestamp": "2026-09-29T12:00:00.000Z",
  "event": "click",
  "data": { "positions": [0], "product_ids": ["ABC-100"] }
}

data by event:

event data
click positions (zero-based) and/or product_ids
conversion optional: subtype (add_to_cart, purchase, support_request), product_ids, positions, quantity, revenue { amount, currency, order_id }
feedback success (boolean), optional comment
region rect { x, y, w, h }, each 0..1

A valid event returns HTTP 2xx.

Resize formula

aspect = width / height
targetArea = 1024 * 1024
outWidth = sqrt(targetArea * aspect)
outHeight = outWidth / aspect

Round to integers. Crop in normalized coordinates before this resize:

sx = originalWidth * x1
sy = originalHeight * y1
sw = originalWidth * (x2 - x1)
sh = originalHeight * (y2 - y1)

x1,y1 is top-left, x2,y2 is bottom-right, each in 0..1.

Regions come back in pixels of the JPEG you posted:

x1 = left / sentWidth
x2 = right / sentWidth
y1 = top / sentHeight
y2 = bottom / sentHeight

Region overlay

Do not draw every detected rectangle at once. Overlapping boxes make the photo unreadable. A row of "Object 1 / Object 2" buttons under the image is useless: those labels have no spatial link to the photo.

Required UI:

  1. Auto-select the highest-confidence region (full frame if none). Mark that object's center marker as active immediately, then search that crop. Do not wait for another click.
  2. Place a small dot at the center of every detected box: ((x1+x2)/2, (y1+y2)/2).
  3. Draw a bounding box only for the selected region (auto-selected, or the one the user last chose by clicking a dot or dragging a crop).
  4. Dim the rest of the image with a semi-transparent black overlay outside that box so the selected object is the focus. If the selection is the full frame, do not dim.
  5. Clicking a dot selects that object and re-runs Find with its crop. Dragging on the image may set a custom crop. Do not add object-index buttons.

Demo styling

For a demo or test site, match the layout and density of test.nyris.app: white header, light gray page (#fafafa), Source Sans 3, compact drop zone. Palette is white, black, and one or two neutrals (borders, muted text). Buttons may be black-on-white or the inverse. Do not use decorative colored page backgrounds.

Find request

POST /find/v1.1 HTTP/1.1
Host: api.nyris.io
X-Api-Key: $NYRIS_API_KEY
Accept: application/items.complete+json
Content-Type: multipart/form-data; boundary=----nyris

------nyris
Content-Disposition: form-data; name="image"; filename="query.jpg"
Content-Type: image/jpeg

<jpeg bytes>
------nyris
Content-Disposition: form-data; name="text"

optional query
------nyris--

Filters, when used (AND across keys, OR across values):

filters[0].filterType = category
filters[0].filterValues[0] = pumps

Do not set Content-Type: multipart/form-data manually if the HTTP client generates the boundary (browser FormData, OkHttp MultipartBody). Set it only when you build the body yourself (curl, raw sockets).

Same request as JSON (base64)

POST /find/v1.1 HTTP/1.1
Host: api.nyris.io
X-Api-Key: $NYRIS_API_KEY
Accept: application/items.complete+json
Content-Type: application/json

{
  "image": "/9j/4AAQSkZJRg...",
  "text": "optional query",
  "filters": [{ "key": "category", "values": ["pumps"] }]
}

Encode the resized JPEG bytes, not the original file. Omit text and filters when unused.

Same request as binary (image only)

POST /find/v1.1 HTTP/1.1
Host: api.nyris.io
X-Api-Key: $NYRIS_API_KEY
Accept: application/items.complete+json
Content-Type: image/jpeg

<jpeg bytes>

Example success body (items.complete)

{
  "results": [
    {
      "sku": "ABC-100",
      "score": 0.91,
      "title": "Example",
      "images": ["https://img.nyris.io/..."]
    }
  ],
  "id": "request-id",
  "session": "session-id"
}

Product fields besides sku and score depend on the index. Do not require a field the index does not have. For a thumbnail, use image if present, else images[0]. session may be missing; treat it as optional.

Regions request

POST /find/v2/regions HTTP/1.1
X-Api-Key: $NYRIS_API_KEY
Content-Type: image/jpeg

<jpeg bytes>

JSON alternative: Content-Type: application/json with body {"image": "<base64>"}.

{
  "regions": [
    {
      "confidence": 0.9,
      "region": { "left": 10, "top": 20, "right": 200, "bottom": 300 }
    }
  ]
}

Coordinates are floats. Ignore fields not shown here.

Errors

Status Action
200, empty results Empty state. Do not retry.
400 Malformed request (bad JSON or base64, invented X-Session). Do not retry; fix the request.
401 Key missing or wrong type. Stop.
413 Image over 2 MB. Lower quality or size, retry once.
415 Unsupported format reached the API (for example HEIC or TIFF). Fix step 3 so everything is converted to JPEG. Do not retry.
429, 5xx, network Retry at most twice with backoff, unless a newer search replaced this one.

If the key is missing locally, do not call the API. Show a configuration error.

Language notes

TypeScript (browser)

const body = new FormData();
body.append("image", jpegBlob, "query.jpg");
if (text) body.append("text", text);

const response = await fetch(`${baseUrl}/find/v1.1`, {
  method: "POST",
  headers: {
    "X-Api-Key": apiKey,
    Accept: "application/items.complete+json",
  },
  body,
  signal,
});

JSON variant, Node.js only (Buffer does not exist in browsers or most edge runtimes):

const image = Buffer.from(jpegBytes).toString("base64");
const response = await fetch(`${baseUrl}/find/v1.1`, {
  method: "POST",
  headers: {
    "X-Api-Key": apiKey,
    Accept: "application/items.complete+json",
    "Content-Type": "application/json",
  },
  body: JSON.stringify(text ? { image, text } : { image }),
});

In the browser, FileReader.readAsDataURL(jpegBlob) gives a data URI that you can send as image unchanged.

Python

Build the multipart body yourself or use requests / httpx files={"image": ("query.jpg", jpeg_bytes, "image/jpeg")} and data={"text": text}. Header X-Api-Key. Do not pass text as a URL query parameter.

JSON variant:

payload = {"image": base64.b64encode(jpeg_bytes).decode("ascii")}
if text:
    payload["text"] = text
response = httpx.post(f"{base_url}/find/v1.1", json=payload, headers=headers)

Swift

Draw UIImage into an opaque renderer filled white, jpegData(compressionQuality: 0.9), upload with URLSession multipart part name image.

Kotlin

BitmapFactory with inSampleSize, EXIF rotate, draw onto a white Canvas, compress(JPEG, 90), OkHttp part name image.

Edge cases

Case Required behavior
Image shorter than 100 px on one side Search anyway. Show a non-blocking hint that small images give less accurate results.
Multi-page TIFF or PDF Use page 1 only.
CMYK JPEG Convert to RGB before encode if the decoder gives CMYK.
PNG with alpha White background, then JPEG.
Rotated phone photo Apply EXIF before crop math, or boxes will not match the preview.
User clears the image Cancel the request and return to the start screen.
Text only Multipart field text with no image part, or JSON {"text": "..."}.
Platform can only send JSON Use the JSON base64 body. Resize first; base64 adds ~33%.
Base64 string has line breaks Strip whitespace before sending. Some encoders wrap lines (openssl base64 at 64 characters, MIME at 76).
Image plus keyword Either send both fields, or search by image and filter titles locally. Do not do both unless the user asked. With an existing text search engine, see Existing text search.
HEIC in Chrome or Firefox Native decode fails. Convert with heic-to or libheif (load it lazily; it is large), then continue as a bitmap.
HEIC without extension or MIME type Native decode fails and the name gives no hint. Try the HEIC decoder before rejecting the file.
Empty key Configuration error, not an empty result list.

Acceptance checklist

All of these must pass before you claim the integration is done:

  • Key is read from the environment or a config file that is gitignored.
  • A text search uses multipart field text and returns HTTP 200.
  • An image search sends a JPEG scaled with the area formula (about 1024×1024 pixels of area).
  • If the JSON base64 body is used, it encodes the resized JPEG and returns HTTP 200.
  • Transparent PNG becomes a JPEG on a white background.
  • EXIF orientation is applied so a portrait phone photo stays portrait.
  • JPEG, PNG, WebP, GIF, AVIF, and HEIC uploads all reach Find as JPEG (HEIC also in Chrome).
  • Regions response boxes are stored as normalized 0..1. The preview shows a center dot per region and a rectangle only on the selected region, with the rest of the image dimmed. No object-index buttons.
  • The auto-selected region's marker is active as soon as Regions returns, before Find responds.
  • Result cards show no score or percentage, and appear in the order of results[].
  • Changing the box sends a new Find with the cropped JPEG.
  • The crop is cut from the full-resolution original, then resized. A crop of a 4000 × 3000 photo is not cut from the 1024² JPEG or the preview.
  • A second search does not show the first search’s products after the second response arrives.
  • HTTP 200 with results: [] renders an empty state.
  • Missing API key does not call the network.
  • 401 and 413 each show a distinct message.
  • A source under 100 px still searches and shows the small-image hint.
  • No Data API write calls were added.
  • If an existing text search engine is used: visual results render on the existing results page; the rendered order equals the nyris order (tested); rules, personalization, re-ranking, and variant grouping are off for visual queries only; the page's fixed filters still apply; facets narrow without calling nyris; missing SKUs are dropped, never backfilled. Full list: Combine with your text search.

Test images

Use a photo of a product that is in the index behind the key. With a demo index key, ask the user which sample products it contains. If you have no photo, ask the user for one. Do not download random images from the web.

Expected: HTTP 200 from Find. The number of products depends on the index behind the key, so assert the status and that results is an array, not a specific SKU.

Out of scope

  • Creating or deleting index items.
  • Tuning X-Options unless the user named a threshold or a limit, or you combine with a text search engine (then limit=100). Index defaults are already applied server-side.
  • Reading response headers that echo resolved options. Do not depend on them. (X-Matching-Request and X-Session are fine to read.)

What you build

  1. The user picks or captures an image.
  2. You normalize it (decode, rotate, convert odd formats) and resize it.
  3. You ask nyris which objects are in the image.
  4. You search with the full image, or with the box the user selected.
  5. You show the products in the order nyris returns them and, if you want analytics, send a feedback event.

If your site already has a text search results page, show visual results there instead of on a separate screen. See Combine with your text search.

Keep the normalized original at full resolution for the whole session. Everything the user sees, and the full-frame JPEG for Regions, can be small. The crop for Find is always cut from the original and resized afterwards. Cropping the small preview or the full-frame JPEG throws away detail: a part a tenth as wide as a 4000 × 3000 photo has 400 × 300 px in the original but only about 118 × 89 px in the 1024² JPEG.

API

Call Method and path Body
Search POST https://api.nyris.io/find/v1.1 multipart/form-data with image and optional text (raw image bytes and JSON base64 also accepted)
Regions POST https://api.nyris.io/find/v2/regions raw JPEG (Content-Type: image/jpeg); multipart and JSON base64 also accepted
Feedback POST https://api.nyris.io/feedback/v1 JSON

Auth is the header X-Api-Key. Use a Find key. A Data Management key will not search.

Public API reference: API Endpoints.

Key in the client

This guide calls the Find API from the app, the same way the example web app does. The key ships in the client, so treat it as a search-only key:

  • It can run searches against your index.
  • It should not be a Data Management key, and it should not be able to change the catalog.
  • Rotate it if it leaks.
  • Put a small backend in front of the API when you need to hide the key, add your own auth, or rate-limit per user. See Errors and keys.

Chapters

  1. Quickstart — first search with curl, JavaScript, and Python.
  2. Image pipeline — formats, EXIF, resize, crop, size limits.
  3. Object detection — regions, normalized boxes, re-search.
  4. Find API — image, text, filters, response, feedback.
  5. Speed — payload size, overlap, perceived speed.
  6. Mobile apps — Swift, Kotlin, React Native, Flutter.
  7. Errors and keys.
  8. Combine with your text search — one results page with Algolia, Elasticsearch, and others, keeping the nyris order.

Runnable samples are in Examples.

Defaults used by the example web app

Setting Value
Max image area 1024 × 1024 pixels (aspect ratio kept, area-based)
JPEG quality 0.9
Transparent pixels filled white before encode
Text field multipart field text, not a query parameter
Response Accept: application/offers.complete+json (same fields as the documented application/items.complete+json)

Quickstart

You need a Find API key. Request one from support@nyris.io. Put it in the environment. Do not commit it.

export NYRIS_API_KEY="your-find-key"
export NYRIS_BASE_URL="https://api.nyris.io"

JavaScript (browser or Node 18+)

async function findByText(apiKey, text) {
  const body = new FormData();
  body.append("text", text);

  const response = await fetch("https://api.nyris.io/find/v1.1", {
    method: "POST",
    headers: {
      "X-Api-Key": apiKey,
      Accept: "application/items.complete+json",
    },
    body,
  });

  if (!response.ok) {
    throw new Error(`Find failed: ${response.status}`);
  }
  return response.json();
}

Do not set Content-Type yourself when using FormData. The runtime must add the multipart boundary.

Python

import os
import urllib.request

boundary = "----nyrisboundary"
text = "hydraulic pump"
payload = (
    f"--{boundary}\r\n"
    f'Content-Disposition: form-data; name="text"\r\n\r\n'
    f"{text}\r\n"
    f"--{boundary}--\r\n"
).encode()

request = urllib.request.Request(
    os.environ.get("NYRIS_BASE_URL", "https://api.nyris.io") + "/find/v1.1",
    data=payload,
    method="POST",
    headers={
        "X-Api-Key": os.environ["NYRIS_API_KEY"],
        "Accept": "application/items.complete+json",
        "Content-Type": f"multipart/form-data; boundary={boundary}",
    },
)
print(urllib.request.urlopen(request, timeout=60).read().decode()[:500])

Full scripts: examples/curl/find.sh, examples/python/find.py, examples/node/find.mjs.

What to build next

A useful product flow is: normalize and resize the image, call regions, let the user adjust the box, then search again with the crop. That is Object detection plus Find API.

Image pipeline

The Find API accepts JPEG, PNG, and WebP. Your app should turn every user file into one JPEG before upload. The example web app does this in @nyris/nyris-api (prepareImage) and in useImageSearch.

file → decode + EXIF rotate → optional format conversion → crop → resize → JPEG

Decode and orientation

Phone cameras store rotation in EXIF. If you ignore it, the search image is sideways and results get worse.

  • Web: decode with a loader that applies EXIF (the example web app uses blueimp-load-image with orientation: true in @nyris/nyris-api), or draw through createImageBitmap and respect the orientation.
  • iOS: UIImage already applies orientation when you draw it.
  • Android: read ExifInterface.TAG_ORIENTATION and rotate the bitmap before you scale it.

Fill transparent pixels with white before you encode JPEG. PNG and WebP transparency otherwise becomes black, and black backgrounds hurt matching. The example client does this:

fill the destination canvas with rgb(255, 255, 255)
draw the source image on top
encode JPEG

Formats

Input What to do
JPEG Decode, apply EXIF, resize, re-encode.
PNG, WebP, GIF (first frame) Decode, flatten on white, resize, JPEG.
HEIC / HEIF Convert to a bitmap first. On the web, try createImageBitmap; if that fails, convert with heic-to (or libheif). Platform photo APIs already yield a bitmap.
TIFF Decode the first page. If pixels are RGB (3 channels), add an opaque alpha, then JPEG. Most browsers cannot decode TIFF natively, so the web needs a TIFF decoder; without one, reject TIFF clearly and leave it out of the file picker.
AVIF, BMP Decode natively where the platform supports it, then JPEG.
PDF Rasterize page 1 (the example uses PDF.js at scale 2), then JPEG.
CAD (.stp, .step, .stl, .obj, .glb, .gltf) Do not rasterize for /find/v1.1. If nyris enabled CAD search for your account, POST the file to POST /cad/find/v0.2 as multipart field file. This endpoint is not in the public API reference; ask support@nyris.io.

Try the native decoder first. Do not reject a file by its extension unless it is CAD; browsers increasingly decode HEIC and AVIF themselves. Skip or reject anything that still fails to decode, with a clear message.

The API accepts only JPEG, PNG, and WebP. Other formats come back as HTTP 415 (binary or multipart) or 400 "Invalid image format" (JSON base64). Base64 is only the transport; the image inside must still be one of those formats. Converting everything to JPEG on the client avoids all of this and gives the smallest upload.

Resize

Default used by the example web app:

  • maxWidth = 1024
  • maxHeight = 1024
  • jpegQuality = 0.9

The scaler keeps the aspect ratio and targets a pixel area of maxWidth * maxHeight, not “longest edge ≤ 1024”. Extreme panoramas stay sharper this way.

targetArea = maxWidth * maxHeight
width  = sqrt(targetArea * aspectRatio)
height = width / aspectRatio
aspectRatio = originalWidth / originalHeight

Examples with 1024 × 1024:

Source aspect (w/h) Output size
1.0 (square) 1024 × 1024
1.5 (3:2) about 1254 × 836
0.75 (3:4 portrait) about 886 × 1182

Round to integers. If you prefer a hard box, fit the longest edge inside 1024 instead (getThumbSizeLongestEdge in @nyris/nyris-api). Area-based sizing is what the example app sends.

The example client always scales to that area, so a very small image is enlarged. Enlarging does not add detail. If the source (or the crop) is under 100 px on its shortest edge, the search still runs, but results get worse. Show the user a hint such as "This image is small; results may be less accurate."

Keep the original

After decode, EXIF rotation, and flattening, keep that bitmap at full resolution for as long as the user works with this image. Derive everything else from it:

Derived image Size Used for
Preview small, sized for the screen What the user sees, overlay markers
Full-frame JPEG area resize to 1024 × 1024 Regions, and Find when nothing is selected
Crop JPEG crop of the original, then area resize Find for the selected object

Never crop the preview or the full-frame JPEG. A 4000 × 3000 photo resized to about 1182 × 887 keeps only about 9% of its pixels. An object 400 × 300 px in the original (a tenth of the width and height) would become a crop of roughly 118 × 89 px. nyris then sees an enlarged, blurry part instead of real detail.

Release the original when the user removes the image or picks a new one.

Crop

Regions and the user box use normalized coordinates in 0..1. Because they are relative, the same box applies to the preview, the full-frame JPEG, and the original:

x1, y1 = top-left
x2, y2 = bottom-right

Pixel crop on the original image:

sx = originalWidth  * x1
sy = originalHeight * y1
sw = originalWidth  * (x2 - x1)
sh = originalHeight * (y2 - y1)

Resize after the crop, using the crop’s aspect ratio, so the object fills the JPEG you upload.

Limits

Recommended Accepted Rejected
Shortest edge 100 px or more Smaller, but results deteriorate Never rejected for small size
File size 1 MB or less (lower latency) Up to 2 MB (2,097,152 bytes) Larger than 2 MB: HTTP 413

With a JSON base64 body, the 2 MB limit applies to the decoded image, not the encoded request.

Aim for about 100 kB after JPEG encode. Quality 0.9 at the sizes above usually lands there. If the blob is still large, lower quality toward 0.8 before you shrink the pixel size further.

Pseudocode

original = flattenOnWhite(decode(file))   # apply EXIF; keep this for the session
preview  = resizeForScreen(original)      # display only

def searchJpeg(normalizedRect = None):
    bitmap = crop(original, normalizedRect) if normalizedRect else original
    bitmap = resizeArea(bitmap, 1024, 1024)   # always the target area
    return encodeJpeg(bitmap, quality=0.9)

Object detection

Regions tell you which objects are in the photo so the user can search one of them instead of the whole frame. The example web app calls this before the product search when the user uploads an image.

Request

Send the same resized JPEG you will use for search (or the full-frame JPEG before a user crop).

POST /find/v2/regions
X-Api-Key: <find-key>
Content-Type: image/jpeg

<raw jpeg bytes>

The example sends the raw image as the body. Two alternatives return the same result:

  • Multipart: Content-Type: multipart/form-data with the image in a part named image.
  • JSON: Content-Type: application/json with body {"image": "<base64 or data URI>"}.

Regions response

{
  "regions": [
    {
      "confidence": 0.92,
      "region": { "left": 120, "top": 40, "right": 640, "bottom": 700 }
    }
  ]
}

left, top, right, bottom are floating-point pixels of the image you sent, not of the original camera file. The response can contain more fields; ignore them.

Normalize

Store boxes in 0..1 so they survive another resize and map onto the preview:

x1 = left  / sentWidth
x2 = right / sentWidth
y1 = top    / sentHeight
y2 = bottom / sentHeight

Clamp each value to 0..1. Drop boxes whose width or height is below a few percent of the frame; they are rarely useful.

Which box to select

  1. If the list is empty, search the full image (x1=0, y1=0, x2=1, y2=1).
  2. Otherwise select the highest confidence, or the box closest to the center if several are close.
  3. Overlay the photo as in Preview. The user changes the selection by clicking a center dot or dragging a crop, not by a list of "Object N" buttons.
  4. When the box changes, crop the full-resolution original (not the preview, not the JPEG you sent to Regions), resize the crop, and call Find again. Do not send the regions response to Find; send the cropped JPEG. See Keep the original.

Order of calls

Two valid orders:

Order Behavior
Regions, then Find with the chosen crop One product search. Slightly slower to first products.
Find on the full image and regions in parallel, then Find again if the user changes the box First products appear sooner. You may replace them when the crop search returns.

The example web app runs regions first, then Find. Parallel calls are a speed option; see Speed. If results render on an existing text search page, use regions first: see Combine with your text search.

Preview

Show an EXIF-correct preview immediately, scaled down for the screen. Do not wait for the network to show the photo. The preview is for display only; crops always come from the original.

Do not stroke every region. That crowds the image. For each detected box:

dotX = ((x1 + x2) / 2) * frameW
dotY = ((y1 + y2) / 2) * frameH

Draw a small marker at (dotX, dotY). Draw a rectangle only for the selected region:

boxLeft   = x1 * frameW
boxTop    = y1 * frameH
boxWidth  = (x2 - x1) * frameW
boxHeight = (y2 - y1) * frameH

Dim everything outside that rectangle (semi-transparent black). The auto-selected object's marker must look active as soon as Regions returns. Clicking a marker selects that object. Do not add object buttons below the image; they are disconnected from the photo. For layout and a black/white test-site palette, follow test.nyris.app. See also Agent Guide.

Search with the Find API

POST /find/v1.1
X-Api-Key: <find-key>
Accept: application/items.complete+json
Content-Type: multipart/form-data; boundary=...

application/items.complete+json returns product fields (title, image, sku, filters, and so on). Without Accept (or with application/minimal+json) you get a minimal { sku, score } list. The example web app sends application/offers.complete+json, an older name that returns the same fields.

Body formats

The API accepts the image three ways:

Format Content-Type When
Multipart multipart/form-data Default in this guide. Carries image, text, and filters.
Binary image/jpeg or image/png Image only. Body is the raw bytes.
JSON application/json Image as base64. For JSON-only clients. See JSON body (base64).

Image

image = <jpeg file>

Field name is image. Filename can be query.jpg.

Text

text = hydraulic pump

With multipart/form-data, text must be a form field. A query string ?text= is ignored and you get empty results. Text-only search sends no image part.

Keep text under 512 characters.

Image and text together

Two options. Pick one and keep it consistent:

  • Combined on the server: send image and text in the same request and let the API combine them.
  • Visual search, then local filter (what the example web app does): when there is an image, the app sends only image and filters the returned titles with the keyword on the client.

Client-side title filter (what the example does after a visual search): case-insensitive match on the product title, with prefix matching so BOL still matches BOLLSIDAN.

If the site has its own text search engine (Algolia, Elasticsearch, ...), use that engine for the keyword and facets, restricted to the nyris SKUs, and keep the nyris order. See Combine with your text search.

Filters

AND across filter types, OR across values of one type.

filters[0].filterType = category
filters[0].filterValues[0] = pumps
filters[0].filterValues[1] = valves
filters[1].filterType = brand
filters[1].filterValues[0] = acme

Catalog of filter keys: GET /find/v1/filters?limit=100 with the same X-Api-Key. It returns an array of { "key": "color", "values": ["Red", "Blue"] }. Search values of one key with GET /find/v1/filters/{key}/{value}?limit=100, which returns string[]. {value} is the search term. Filters are on /find/v1, not /find/v1.1.

JSON body (base64)

Use this when your platform works with JSON end to end and cannot easily send multipart or binary bodies: low-code tools, serverless functions, tool-calling backends, or a gateway that only forwards JSON.

POST /find/v1.1
X-Api-Key: <find-key>
Accept: application/items.complete+json
Content-Type: application/json
{
  "image": "/9j/4AAQSkZJRg...",
  "text": "hydraulics",
  "filters": [
    { "key": "MachineSerial", "values": ["SN-40821"] },
    { "key": "Assembly", "values": ["Hydraulics", "Pneumatics"] }
  ]
}
  • image is plain base64 or a data URI (data:image/jpeg;base64,...). Leave it out only for a text-only search ({"text": "..."}).
  • text is optional, max 512 characters.
  • filters is optional. Each entry is { key, values }, with the same AND/OR rules as multipart.
printf '{"image":"%s"}' "$(base64 < query.jpg | tr -d '\n')" > body.json
curl -sS -X POST "https://api.nyris.io/find/v1.1" \
  -H "X-Api-Key: $NYRIS_API_KEY" \
  -H "Accept: application/items.complete+json" \
  -H "Content-Type: application/json" \
  --data-binary @body.json

Writing the body to a file avoids the shell's argument-length limit for large images.

Base64 adds about 33% to the request. The 2 MB limit applies to the decoded image, not the encoded body. Resize first (see Image pipeline) and encode the resized JPEG, not the camera original. When you control the client, multipart or binary is smaller and faster.

Sessions

Every response carries a request id (id in the body, header X-Matching-Request) and usually a session id (session, header X-Session). Treat the session as optional. To group related searches (for example a crop search after the full-frame search), send the session from the previous response:

X-Session: <session from previous response>

or POST /find/v1.1?session=<session>. Only send session ids the API returned; an invented value returns HTTP 400. Browsers may not expose the response headers to your code, so read id and session from the body.

Geolocation header

If your products have coordinates (WGS84), limit results by distance from the user:

Field Example Meaning
lat 52.5200 Latitude of the user
lon 13.4050 Longitude of the user
dist 10000 Maximum distance in meters

With multipart, send them as form fields. Otherwise use query parameters: POST /find/v1.1?lat=52.52&lon=13.405&dist=10000. Field names follow docs.nyris.io; the example web app sends latitude / longitude form fields instead.

Search response

{
  "results": [
    {
      "sku": "ABC-100",
      "score": 0.91,
      "title": "Hydraulic pump",
      "image": "https://..."
    }
  ],
  "id": "request-id",
  "session": "session-id"
}

Field names on each product depend on the index. Always read sku (fall back to key or objectID if you see those). Keep id and session for feedback.

results is already sorted by visual similarity. Keep that order; do not re-sort by stock, popularity, or recommendations. score is an internal ranking signal. Do not show it as a percentage or confidence on result cards. Users treat that number as accuracy, and it is misleading.

results: [] with HTTP 200 means the search ran and nothing matched. It does not mean the request was malformed. An empty result after a text search is often the text query-string mistake above.

Feedback

Optional. Improves analytics in the portal.

On a website that calls the Find API from the browser, the nyris events tracking script can send clicks and conversions for you. See Events tracking script in Analytics & Feedback. Use the raw API below for mobile apps, backends, or custom tracking.

POST /feedback/v1
X-Api-Key: <find-key>
Content-Type: application/json
{
  "request_id": "<id from find>",
  "session_id": "<session from find>",
  "timestamp": "2026-09-28T12:00:00.000Z",
  "event": "click",
  "data": { "positions": [0], "product_ids": ["ABC-100"] }
}
event When
feedback User says results were useful or not. data.success is boolean.
click User opens a product. positions is the zero-based index in results.
conversion User adds to cart or completes the action you care about. Optional data.subtype: add_to_cart, purchase, or support_request. A purchase can add data.revenue { amount, currency, order_id }.
region User changed the box. data.rect is { x, y, w, h }, each 0..1.

timestamp is ISO 8601 UTC and required. A valid event returns HTTP 2xx.

Recommend by SKU

When the user is already on a product page:

GET /recommend/v1/{sku}
X-Api-Key: <find-key>
Accept: application/items.complete+json

Returns visually similar products. The SKU itself is not in the results.

Product images by SKU

GET /find/v1.1/images/{sku}
X-Api-Key: <find-key>
Accept: application/items.complete+json

Returns { "images": ["https://img.nyris.io/..."] }.

Speed

Most of the wait is upload plus the model, not your UI. Shrink the JPEG on the device, show the photo immediately, and do not block the screen on a spinner.

Shrink before upload

A 4000×3000 camera JPEG is several megabytes. The area resize in Image pipeline usually produces about 100 kB. That is the single biggest win.

  • Resize on the device, not on a server round-trip you control.
  • Use JPEG quality 0.9, then 0.8 if you are still over ~200 kB.

Show something before the response

The example web app:

  1. Navigates to the results route as soon as the user drops a file.
  2. Shows the preview from the local file.
  3. Renders skeleton cards while results is empty and the request is in flight.
  4. Clears the previous product list when a new search starts, so stale products do not flash.

Do not cover the page with a full-screen blocking overlay.

Overlap network calls

Regions and the first Find call both need the resized JPEG. Start them together:

original = normalize(image)          # decoded, EXIF-rotated, flattened
jpeg = resize(original)
start find(jpeg)                     # full frame, show these products first
start regions(jpeg)                  # when this returns, draw boxes
# if the user changes the box:
start find(resize(crop(original)))   # crop the original, then resize

Skip the parallel full-frame Find when results go through an existing text search engine. Each Find there also costs an engine query, and the page would flip when the crop result arrives. See Combine with your text search.

Cancel the previous Find if the user fires another search before it finishes (AbortController, URLSession task cancel, OkHttp Call.cancel()). Ignore responses that are not the latest request id you stored.

Text input: debounce about 300 ms so you do not search on every keystroke.

Connection setup

  • preconnect to https://api.nyris.io (and https://img.nyris.io for product images) on the page that can start a search: <link rel="preconnect" href="https://api.nyris.io">.
  • Reuse HTTP connections. Browsers do this for fetch. On mobile, use one URLSession / one OkHttp client, not a new client per request.
  • Prefer multipart or raw bytes over the JSON base64 body when you can choose. Base64 adds about 33% to the upload. If your platform needs JSON, it works fine; just resize first.

What not to do

  • Do not upload the original camera file “and let the API resize it”.
  • Do not wait for regions before showing the preview.
  • Do not run region detection again when only the text query changed.

Mobile apps

The HTTP contract does not change. Only capture, decode, and resize change.

Snippets: examples/swift/Find.swift, examples/kotlin/Find.kt.

nyris also publishes native SDKs with camera UI and API client: SDK.Android and Nyris.IMX.iOS. Use them if you want a ready-made flow; use this chapter if you build your own.

iOS (Swift)

  1. UIImagePickerController or PHPickerViewController for the library. AVCaptureSession for the camera.
  2. Draw the UIImage into a new bitmap. UIKit applies EXIF orientation when you draw.
  3. Flatten, crop, area-resize, jpegData(compressionQuality: 0.9).
  4. URLSession upload task, multipart body, header X-Api-Key.
func jpegForSearch(image: UIImage, maxSide: CGFloat = 1024, quality: CGFloat = 0.9) -> Data? {
    let aspect = image.size.width / image.size.height
    let area = maxSide * maxSide
    let width = max(1, (area * aspect).squareRoot().rounded())
    let height = max(1, (width / aspect).rounded())
    let format = UIGraphicsImageRendererFormat.default()
    format.scale = 1 // default() uses the screen scale; sizes here are pixels
    format.opaque = true
    let renderer = UIGraphicsImageRenderer(size: CGSize(width: width, height: height), format: format)
    let rendered = renderer.image { ctx in
        UIColor.white.setFill()
        ctx.fill(CGRect(x: 0, y: 0, width: width, height: height))
        image.draw(in: CGRect(x: 0, y: 0, width: width, height: height))
    }
    return rendered.jpegData(compressionQuality: quality)
}

Store the key in the app target as a build setting or remote config, not in a screenshot in the repo. A Find-only key in the binary is the model this guide uses.

Android (Kotlin)

  1. Photo Picker or CameraX.
  2. Decode with BitmapFactory.Options.inSampleSize so you never allocate the full 12 megapixel bitmap.
  3. Rotate using ExifInterface before the exact scale.
  4. Draw on a white canvas, compress(JPEG, 90, stream).
  5. OkHttp MultipartBody.FORM, part name image. execute() blocks, so call it off the main thread.
fun jpegForSearch(source: Bitmap, maxSide: Int = 1024, quality: Int = 90): ByteArray {
    val aspect = source.width.toFloat() / source.height
    val area = maxSide * maxSide.toFloat()
    val width = kotlin.math.sqrt(area * aspect).roundToInt().coerceAtLeast(1)
    val height = (width / aspect).roundToInt().coerceAtLeast(1)
    val out = Bitmap.createBitmap(width, height, Bitmap.Config.ARGB_8888)
    val canvas = Canvas(out)
    canvas.drawColor(Color.WHITE)
    val paint = Paint(Paint.FILTER_BITMAP_FLAG) // smooth downscaling
    canvas.drawBitmap(source, null, RectF(0f, 0f, width.toFloat(), height.toFloat()), paint)
    val stream = ByteArrayOutputStream()
    out.compress(Bitmap.CompressFormat.JPEG, quality, stream)
    return stream.toByteArray()
}

HEIC from the camera is already a Bitmap after ImageDecoder / BitmapFactory on current Android. You do not upload the HEIC container.

React Native

Use the system picker (expo-image-picker or react-native-image-picker). Ask the picker to resize (quality: 0.9, maxWidth / maxHeight around 1024) or resize yourself so you match the area formula.

Send with FormData:

const body = new FormData();
body.append("image", {
  uri: asset.uri,
  name: "query.jpg",
  type: "image/jpeg",
});
await fetch("https://api.nyris.io/find/v1.1", {
  method: "POST",
  headers: {
    "X-Api-Key": apiKey,
    Accept: "application/items.complete+json",
  },
  body,
});

On React Native, do not set Content-Type: multipart/form-data by hand; the boundary would be missing.

Flutter

image_picker returns a file. Package image can decode, bake orientation, resize, and encodeJpg(quality: 90). Upload with package:http MultipartRequest and MultipartFile.fromBytes.

Shared UI

  • Full-screen camera is optional. A gallery button plus a camera button is enough.
  • Show the captured photo immediately, then skeleton results.
  • One box overlay using normalized coordinates. Pinch-to-zoom on the preview is optional.
  • Read the key from configuration (build setting or remote config). Do not ask users to paste a key. The example web app reads it from settings.js or from the signed-in user.

Errors and keys

Status codes

Status Meaning What to do
200 and results: [] Search worked, no hits Show an empty state. Do not retry.
400 Malformed request (bad JSON or base64, invented session id) Fix the request. Do not retry as is.
401 Key missing or not a Find key Check X-Api-Key. Data keys do not search.
413 Image larger than 2 MB (2,097,152 bytes) Resize and lower JPEG quality, then retry once.
415 Unsupported image format (HEIC, TIFF, BMP, …) Convert to JPEG on the client. Do not retry as is.
429 Rate limit Back off. Do not tight-loop.
5xx or network drop Transient Retry up to 2 times with a short delay.

The example client throws if apiKey is empty before the request (The api-key is not set.). Surface that as “search is not configured”, not as “no products”.

Retries

Retry only idempotent search POSTs that failed on the network or with 5xx. Do not retry 400, 401, or 413 without changing the request, the key, or the image.

Images under 100 px on the shortest edge are not an error. The API accepts them, but results deteriorate, so show the user a hint instead of blocking the search.

If the user already started a newer search, drop the old response even if it succeeds later.

Key in the app

This guide sends X-Api-Key from the website or the mobile app.

  • Use a Find key. Anyone with the app can extract it and run searches on your index.
  • Do not ship a Data Management key in a client. That key can change the catalog.
  • If it leaks, ask support@nyris.io to rotate the Find key, then ship an app update. Loading the key from your own config endpoint avoids the app update.
  • Do not log the key, do not put it in analytics, and do not commit it. Samples read NYRIS_API_KEY.

Add a backend proxy when at least one of these is true:

  • You must not expose a key at all.
  • You already have user login and want per-user quotas.
  • You need to attach prices or stock that must not be guessed from the client.

The proxy is a thin POST forwarder: the app uploads the JPEG to you, you add X-Api-Key, you call https://api.nyris.io/find/v1.1, you return the JSON. Keep the resize on the device so your server does not receive multi-megabyte photos.

Request options

X-Options is an optional request header of space-separated key=value flags. The public ones are limit (1 to 100, default 20) and threshold (minimum score), for example X-Options: limit=10 threshold=0.5. The index already has server-side defaults. Leave the header unset until you have a reason to change ranking. See Find API.

Rule 1: one results page

Visual search is a query mode of the normal search results page, not a separate screen.

  • The search box shows the query image as a thumbnail chip with a remove (×) button. Removing it returns to normal text search.
  • The image preview with region markers sits above the results, or in a side column on wide screens. It collapses on small screens. It follows the Region overlay rules: a dot per object, a box only on the selected object, the rest dimmed.
  • Facets, filters, product cards, badges, and pagination are the same components the text search uses.
  • Clicking another object or dragging a new crop runs a new visual search and refreshes the same page.
  • When the user adds an image, clear the typed text query and the selected facets. The image is a new search.

Rule 2: nyris order is final

Visual search is exact search. The user photographed a specific item and expects the most visually similar product first. Keep the order nyris returns. Nothing may reorder it.

For visual search queries:

  • No relevance re-ranking by the text engine.
  • No merchandising rules, pinned or promoted items, or banners inserted into the result list.
  • No personalization, dynamic re-ranking, or "AI ranking" features.
  • No boosting or sinking by stock, margin, popularity, rating, or newness.
  • No recommendations or sponsored products mixed into the list. Recommendation carousels may appear below the results, clearly separate, never interleaved.
  • Out-of-stock products stay in their position and stay visible. Show the normal out-of-stock badge.
  • The only sort is "Most similar". Hide or disable the sort dropdown while an image is active.
  • Do not show the nyris score or a percentage. See Find API.

Filters and facets may remove products. The remaining products keep their nyris order.

Rule 3: same product IDs in both systems

The join works only if a nyris sku identifies the same product in your text search index, for example the Algolia objectID or an sku attribute. Agree on this mapping before you write code.

  • nyris returns a product that the text index does not have: drop it and report it as a catalog sync problem. Do not log every miss from every browser; sample or aggregate.
  • The text index hides a product (unpublished, wrong market, not visible to this customer group): drop it. Keep the order of the rest.
  • Never backfill missing positions with text search results, bestsellers, or recommendations.
  • If the index groups variants (one record per product, many SKUs), decide whether nyris SKUs are variant or parent IDs and map them accordingly. Turn off result grouping (distinct in Algolia, collapse in Elasticsearch) for visual queries, or the engine may return a different variant than the one nyris matched.

How it works

  1. Run the nyris image pipeline as in Agent Guide: normalize, resize, Regions, auto-select the best object, then one Find with that crop. With a text engine, do not also run a full-frame Find in parallel. That would cost a second engine query and make the results page flip.
  2. Ask nyris for enough candidates for facets to be useful: X-Options: limit=100, the maximum (see Errors and keys). The default of 20 is fine for a plain list, but facets over 20 products are thin.
  3. Take results[].sku in order. Build a rank map sku → position. If a SKU appears twice, keep the first position.
  4. Query the text engine with an empty text query, restricted to those SKUs, plus whatever facet filters the user selected and the filters the page always applies (market, visibility, customer group). Ask for all of them in one page and request facet counts.
  5. Sort the returned records by the nyris rank map. Records the engine did not return are gone (filtered or missing).
  6. Render with the existing product cards. Paginate over the reordered list in the client; with at most 100 products this is one engine request.

When the user changes a facet, repeat steps 4 and 5 with the same SKU list. Do not call nyris again; the image did not change. When the user changes the crop, start again at step 1 and clear the selected facets.

Treat the whole chain (nyris, then the engine) as one search for stale-response handling. If the user changes the crop or a facet while a request is in flight, abort it or ignore its result.

Why reorder in your code

Text engines rank by their own criteria: text relevance, custom ranking, rules, personalization. Even with an empty query, the order they return is not the nyris order. Reordering by the rank map in your code is the only engine-independent way to guarantee it. Also switch off engine features that add, pin, or remove items (see the engine notes below), because reordering cannot undo an inserted or hidden product.

Elasticsearch can also return documents in a given ID order with a pinned query (at most 100 IDs; OpenSearch has no pinned query). That is optional. Keep the client-side reorder anyway as the final guarantee.

Known limitation: facets see only the candidates

Facets narrow the up to 100 products nyris returned. A product of the selected brand at nyris position 150 will not appear, and facet counts describe the candidates, not the catalog. If a facet also exists as a nyris filter (GET /find/v1/filters), you may re-run Find with that filter instead, so nyris searches within the brand. See Find API.

Text and image together

If the user types a keyword while an image is active, the keyword narrows the visual results. It does not re-rank them. Pick one approach for the whole site:

  • Keyword in the engine: send the keyword to the text engine as the query, still restricted to the nyris SKUs. Do not send text to nyris. Reorder by the nyris rank map as usual; the engine's relevance order is discarded.
  • Keyword in nyris: send text to nyris together with the image (see Find API), then continue at step 3 with the new nyris order.

Do not run a separate full-catalog text search and merge the two lists.

Example: Algolia

Shown with the algoliasearch v5 JavaScript client and a search-only key.

import { algoliasearch } from "algoliasearch";

const algolia = algoliasearch(ALGOLIA_APP_ID, ALGOLIA_SEARCH_KEY);
const LAST = Number.MAX_SAFE_INTEGER;

interface ProductHit {
  objectID: string;
  [field: string]: unknown;
}

function quoteFilterValue(value: string): string {
  return `"${value.replace(/\\/g, "\\\\").replace(/"/g, '\\"')}"`;
}

function skuFilter(skus: string[]): string {
  return skus.map((sku) => `objectID:${quoteFilterValue(sku)}`).join(" OR ");
}

function rankMap(skus: string[]): Map<string, number> {
  const rank = new Map<string, number>();
  skus.forEach((sku, index) => {
    if (!rank.has(sku)) rank.set(sku, index);
  });
  return rank;
}

export async function visualResults(
  nyrisSkus: string[],
  facetFilters: string[][],
  pageFilters: string,
) {
  if (nyrisSkus.length === 0) return { hits: [], facets: {} };

  const rank = rankMap(nyrisSkus);
  const skuRestriction = `(${skuFilter(nyrisSkus)})`;

  const response = await algolia.searchSingleIndex<ProductHit>({
    indexName: "products",
    searchParams: {
      query: "",
      filters: pageFilters ? `(${pageFilters}) AND ${skuRestriction}` : skuRestriction,
      facetFilters,
      facets: ["*"],
      hitsPerPage: rank.size,
      distinct: false,
      enableRules: false,
      enablePersonalization: false,
      enableReRanking: false,
      analyticsTags: ["visual-search"],
    },
  });

  const hits = [...response.hits].sort(
    (a, b) => (rank.get(a.objectID) ?? LAST) - (rank.get(b.objectID) ?? LAST),
  );
  return { hits, facets: response.facets ?? {} };
}

Notes:

  • enableRules: false stops Query Rules from pinning, hiding, or inserting items. enablePersonalization: false and enableReRanking: false stop personalization and Dynamic Re-Ranking from changing the order. distinct: false stops variant grouping. Use these settings for visual queries only; text search keeps its normal settings.
  • pageFilters are the filters your page always applies (market, visibility). Combine them with the SKU restriction using AND; never replace them.
  • If the SKU is stored in another attribute than objectID, filter on that attribute and sort by it (hit.sku). It must be in attributesForFaceting, for example as filterOnly(sku).
  • A single searchSingleIndex call returns conjunctive facet counts: after the user picks one brand, the other brands drop out of the brand facet. The existing results page usually shows disjunctive counts. Get those from InstantSearch or the Algolia helper, which send the extra queries for you, or send one extra query per refined facet yourself.
  • Use analyticsTags so visual searches do not distort text search analytics. If you also send Algolia Insights events, set clickAnalytics: true to get a queryID, and tag the events as visual search so they do not train the text ranking.

With InstantSearch

Keep the existing facet, refinement, and hits widgets. While an image is active:

  • Add the SKU restriction and the settings above through the Configure widget. If the page already sets filters there, AND the SKU restriction onto the existing value instead of replacing it.
  • Set hitsPerPage to the number of SKUs and reorder in the hits widget's transformItems with the rank map.
  • Hide the stock pagination widget and paginate the reordered hits in the client. Server-side pages would split the nyris order across requests.
  • If URL routing is on, keep the SKU list out of the URL (exclude it in stateMapping). Otherwise a reload restores the SKU restriction without the image.

Remove all of this when the user removes the image.

Example: Elasticsearch / OpenSearch

Restrict with a terms filter on the SKU field (mapped as keyword). Put the page's fixed filters in query, and the user's facet selections in post_filter, so facet counts stay disjunctive like on the normal results page:

{
  "size": 3,
  "query": {
    "bool": {
      "filter": [
        { "terms": { "sku": ["ABC-100", "XYZ-7", "QRS-55"] } },
        { "term": { "market": "de" } }
      ]
    }
  },
  "post_filter": { "term": { "brand": "norelem" } },
  "aggs": {
    "brand": { "terms": { "field": "brand", "size": 50 } }
  }
}

Set size to the number of SKUs. Do not wrap this in a function_score, add collapse, or apply learning-to-rank for visual queries. Then sort the hits by the nyris rank map before rendering.

Where to run the join

Place When
Browser Both keys are search-only keys. Simplest. The browser calls nyris, then the text engine.
Your backend You need to hide keys, apply customer-specific visibility, or combine with prices from another system. The backend calls nyris and the engine and returns the merged, ordered list.

Either way, the image pipeline (normalize, resize, crop) runs on the device before upload. See Image pipeline.

When the engine renders the product cards, nyris only needs to return SKUs. You may request Accept: application/minimal+json to make the response smaller, but then you cannot show nyris products on their own (see the text engine failure row below).

Analytics and feedback

  • Send nyris feedback (POST /feedback/v1) for clicks and conversions with the nyris request id. Set positions to the product's zero-based index in the nyris results list, not its position on the filtered page, and always include product_ids. See Find API.
  • If the text engine records clicks, tag them as visual search (see the Algolia notes).

Empty states and errors

Situation Show
nyris returns no results "No visually similar products found." Offer to adjust the crop or search by text.
nyris returns SKUs, the engine returns none Same empty state. Report the SKUs as a sync problem.
Facets filter everything out The normal "no results for these filters" state, with a way to clear filters.
nyris fails An error with a retry. Do not silently replace the list with text search results or bestsellers.
Text engine fails An error with a retry. Only if the shop has no per-market or per-customer visibility rules may you show the nyris products in nyris order from nyris fields. Otherwise you could show products the shopper must not see.

Checklist

  • Visual search uses the existing search results page, with the image as a removable chip and the region preview on the same page.
  • nyris sku maps to a product ID in the text index, and the mapping was tested on real products, including variants.
  • Results appear in exactly the nyris order. A test compares the rendered order with the nyris response.
  • Merchandising rules, personalization, re-ranking, result grouping, and promoted items are off for visual queries only.
  • The page's fixed filters (market, visibility) still apply to visual queries.
  • Out-of-stock products keep their position and show the normal badge.
  • There is no sort option other than "Most similar" while an image is active.
  • Selecting a facet narrows the list without reordering it, and does not call nyris again.
  • Changing the crop calls nyris again, clears facets, and resets the list.
  • Products missing from the text index are dropped and reported, never backfilled.
  • No score or percentage is shown.
  • Recommendations, if any, are below the results and visually separate.

Examples

Runnable clients for the Find API. They read NYRIS_API_KEY from the environment. Download them all as nyris-examples.zip, or one at a time:

Run them from the examples folder:

export NYRIS_API_KEY="your-find-key"

sh curl/find.sh text "chair"
python3 python/find.py text "chair"
node node/find.mjs text "chair"

Image search expects a JPEG. The scripts label every upload image/jpeg, so convert PNGs first (the API accepts PNG, but these scripts do not detect it). Resize in your app first; these scripts upload the file as given so you can see the HTTP contract.

image-json sends the same image as a JSON body with base64, for platforms that only speak JSON:

python3 python/find.py image-json ./query.jpg

resize-check prints the area-based output size and does not call the network:

python3 python/find.py resize-check 3000x2000
node node/find.mjs resize-check 3000x2000

Swift and Kotlin files are reference snippets. They are not a full Xcode or Gradle project.

Authentication

All nyris API requests require an API key passed in the X-Api-Key header.

How do I get an API key?

To get started, email us at support@nyris.io. Our team will provision an API key for you.

You can call the API from a website or app. A key shipped in frontend code can be read from the page source or from the requests the browser sends, so treat it as public. If you want the key to stay on your server, send requests through your own backend, which then calls the nyris API.

Find API

Send an image to the Find API and receive a JSON object with metadata for the identified items. You can optionally pass a session ID to correlate multiple requests.

POST https://api.nyris.io/find/v1.1

To search with filters, send the image and filter fields as multipart/form-data. Filtered responses include only items matching the given filter types and values. You can also append a session ID as a query parameter: POST https://api.nyris.io/find/v1.1?session=xxxxxx.

Request Headers

Key Value
X-Api-Key The API key provided by us.
Content-Type image/jpeg or image/png (binary), multipart/form-data, or application/json (base64). WebP is accepted in multipart and JSON bodies.
Content-Length The size of the request body in bytes. Most HTTP clients set it automatically.
Accept (Optional) application/items.complete+json for the full response format. Default is minimal.
X-Session (Optional) The session value returned by a previous response. Any other value is rejected with 400.
X-Options (Optional) Search parameters, e.g. limit=10 threshold=0.5. See below.

If no value is provided for X-Session (or the session query argument), a new session is started and its ID is returned with the response. Front-ends are encouraged to pass the session ID returned by the previous request to the next request, thereby linking requests together.

Request Body

  • Image only (binary): send the raw image bytes as the request body with Content-Type: image/jpeg or image/png.
  • Image with filters (multipart): use multipart/form-data and include the image plus the filter fields described below. An optional text form field sends a text query.
  • JSON body (base64): send Content-Type: application/json with a JSON object. The image is a base64-encoded string. You can also send text without an image for a text-only search.

JSON Body Schema

Field Type Required Description
image String Yes, unless text is given (text-only search) Base64-encoded image (plain or data URI, e.g. data:image/jpeg;base64,...). JPEG, PNG, or WebP.
text String No Optional text query (max 512 characters). Longer text is rejected with 400.
filters Array No Same filter format as multipart: [{"key": "MachineSerial", "values": ["SN-40821"]}].

With a JSON (base64) body, the 2 MB limit applies to the decoded image. The encoded request is about 33% larger than the image.

Filter Schema (multipart)

When sending filters via multipart/form-data, include one or more filter type/value pairs. The AND operator is used between filter types; OR is used between values of the same type.

Form field names are indexed: filters[0].filterType, filters[0].filterValues[0], filters[0].filterValues[1], filters[1].filterType, and so on.

Send text as a form field, not as a query parameter. A text query parameter is ignored.

Name Type Explanation
filterType String The filter type, example: MachineSerial
filterValues String [] The filter values, example: [SN-40821]
text String Optional text query (max 512 characters). Longer text is rejected with 400.

Search Parameters (X-Options)

Control result count and score thresholds via the X-Options header. Parameters are key=value pairs separated by a space, e.g. X-Options: limit=10 threshold=0.5.

Parameter Example Value Explanation
limit limit=20 (default), integers between 1 and 100 The upper limit for the number of results to be returned.
threshold threshold=0.8 The final threshold to apply to the scores. Only results above or equal to that score will be returned.

Response Headers

Requests are automatically tagged with a Request ID and a Session ID:

Key Value
X-Matching-Request The identifier of the request; used for tracing and with the Feedback API.
X-Session A session identifier covering multiple requests; used for tracing and with the Feedback API.

Both headers are exposed to browsers via CORS. The same values are in the body as id and session.

Response Format

By default, a minimal response is returned. Use the Accept header to request the full format:

Accept value Description
application/minimal+json Return minimal response (the default).
application/items.complete+json Return all available fields.
application/offers.complete+json Older alias of application/items.complete+json.

We guarantee that existing fields will not be removed or have their type changed. Additional fields can be added at any point; application code should gracefully ignore unknown fields.

Image Requirements

Recommended Accepted Rejected
Shortest edge 100 px or more Smaller images are accepted, but result quality deteriorates as the image gets smaller. Never rejected for small size.
Resolution About 1 Mpx (e.g. 1000 × 1000 px), regardless of aspect ratio Any —
File size 1 MB or less for low latency. Around 100 kB is typical for a 1 Mpx JPEG. Up to 2 MB (2,097,152 bytes) Larger than 2 MB: HTTP 413
Format JPEG, converted on the client JPEG/JFIF, PNG, WebP All other formats (e.g. HEIC, TIFF, BMP, GIF): HTTP 415 (binary or multipart) or 400 "Invalid image format" (JSON base64)

Convert other formats (HEIC from iPhones, TIFF, BMP, GIF, PDF pages) to JPEG on the client before sending. Base64 is only a transport encoding: the image inside a JSON body must still be JPEG, PNG, or WebP.

Resize and crop on the client before sending. Apply cropping to the original image first, then resize.

With a JSON (base64) body, the 2 MB limit applies to the decoded image. The encoded request is about 33% larger than the image.

A binary body with Content-Type: image/webp is not accepted. Send WebP as multipart or JSON.

Error Responses

Status When
400 Invalid request. Examples: unsupported image format inside a JSON body ("Invalid image format"), invalid base64, text over 512 characters, unknown X-Session. The body is RFC 9110 problem JSON.
401 X-Api-Key missing or invalid.
413 Image larger than 2 MB (2,097,152 bytes; for JSON bodies, measured on the decoded image).
415 Unsupported image format in a binary or multipart body (e.g. HEIC, TIFF, BMP).

Additional fields may appear. Application code should ignore fields it does not use.

Example Request

POST
https://api.nyris.io/find/v1.1
import java.nio.file.Files;
import java.nio.file.Path;

OkHttpClient client = new OkHttpClient();

byte[] imageBytes = Files.readAllBytes(Path.of("image.jpg"));
RequestBody body = RequestBody.create(imageBytes, MediaType.parse("image/jpeg"));

Request request = new Request.Builder()
  .url("https://api.nyris.io/find/v1.1")
  .post(body)
  .addHeader("x-api-key", "KEY")
  .addHeader("x-session", "...")
  .build();

Response response = client.newCall(request).execute();

Example with Filters

POST
https://api.nyris.io/find/v1.1
OkHttpClient client = new OkHttpClient();
RequestBody body = new MultipartBody.Builder().setType(MultipartBody.FORM)
    .addFormDataPart("filters[0].filterType","color")
    .addFormDataPart("filters[0].filterValues[0]","Red")
    .addFormDataPart("image","image.jpg",
        RequestBody.create(MediaType.parse("image/jpeg"),
            new File("image.jpg")))
    .build();

Request request = new Request.Builder()
    .url("https://api.nyris.io/find/v1.1")
    .method("POST", body)
    .addHeader("x-api-key", "<SET_YOUR_KEY_HERE>")
    .addHeader("cache-control", "no-cache")
    .build();

Response response = client.newCall(request).execute();

Example with JSON Body (Base64)

POST
https://api.nyris.io/find/v1.1
{
    "image": "/9j/4AAQSkZJRg...",
    "text": "hydraulics",
    "filters": [
        {
            "key": "MachineSerial",
            "values": ["SN-40821"]
        },
        {
            "key": "Assembly",
            "values": ["Hydraulics", "Pneumatics"]
        }
    ]
}
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

OkHttpClient client = new OkHttpClient();

byte[] imageBytes = Files.readAllBytes(Path.of("image.jpg"));
String imageB64 = Base64.getEncoder().encodeToString(imageBytes);

String json = "{"
    + "\"image\":\"" + imageB64 + "\","
    + "\"text\":\"hydraulics\","
    + "\"filters\":["
    + "{\"key\":\"MachineSerial\",\"values\":[\"SN-40821\"]},"
    + "{\"key\":\"Assembly\",\"values\":[\"Hydraulics\",\"Pneumatics\"]}"
    + "]}";

RequestBody body = RequestBody.create(
    MediaType.parse("application/json"), json);

Request request = new Request.Builder()
    .url("https://api.nyris.io/find/v1.1")
    .post(body)
    .addHeader("x-api-key", "KEY")
    .build();

Response response = client.newCall(request).execute();

Example Minimal Response

{
    "id": "c7ac45c0165a54feb005aa2049f19235",
    "session": "c7ac45c0165a54feb005aa2049f19235",
    "results": [
        {
            "sku": "123454643",
            "score": 0.96,
            "image": "https://img.nyris.io/example.jpg",
            "ids": {
                "catalogItem": "123454643"
            }
        },
        {
            "sku": "123454644",
            "score": 0.92,
            "image": "https://img.nyris.io/example2.jpg",
            "ids": {
                "catalogItem": "123454644"
            }
        }
    ]
}

Example Full Response

{
    "id": "c7ac45c0165a54feb005aa2049f19235",
    "session": "c7ac45c0165a54feb005aa2049f19235",
    "results": [{
        "title": "test product",
        "descriptionShort": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr.",
        "descriptionLong": "Lorem ipsum dolor sit amet, consetetur sadipscing elitr, sed diam nonumy eirmod tempor invidunt ut labore et dolore magna aliquyam erat.",
        "language": "de-DE",
        "brand": "test brand",
        "catalogNumbers": ["test_catalog_number_0", "test_catalog_number_1"],
        "customIds": {
            "DEMO_CUSTOM_ID": "DEMO_ID_123"
        },
        "keywords": ["demo", "demonstration", "test"],
        "categories": ["Demo", "Test"],
        "availability": "in stock",
        "feedId": "1",
        "groupId": "1",
        "price": "10.00 EUR",
        "salePrice": "1.00 EUR",
        "links": {
            "main": "https://example.com/product/123",
            "mobile": "https://example.com/product/123/thumbnail.jpg"
        },
        "images": ["https://img.nyris.io/example1.jpg", "https://img.nyris.io/example2.jpg"],
        "metadata": "This product is for demonstrations only",
        "sku": "123456",
        "score": 1,
        "image": "https://img.nyris.io/example.jpg",
        "oid": "c7ac45c0165a54feb005aa2049f19235",
        "ids": {
            "catalogItem": "123456"
        },
        "filters": {
            "color": ["Red"],
            "product": ["Shoes"]
        }
    }]
}

Geolocation

If products have geolocation data, you can narrow results by distance. Coordinates must be in WGS84 format.

When using multipart/form-data (for filters), pass geolocation as form fields. Otherwise, use query parameters: POST https://api.nyris.io/find/v1.1?lat=LAT&lon=LON&dist=DIST.

Name Example Explanation
lat 52.5200 The latitude of the user.
lon 13.4050 The longitude of the user.
dist 10000 The maximum distance from the center in meters.

Regions API

The Find API matches on the entire image. For images containing multiple objects, you may want to detect and crop to specific areas first. The Regions API identifies parts of the image likely to contain relevant objects, allowing your UI to propose sections of the image for the user to search with.

POST https://api.nyris.io/find/v2/regions

Request Headers

Key Value
X-Api-Key The API key provided by us.
Content-Type image/jpeg, image/png, image/webp, multipart/form-data, or application/json (base64).
Content-Length The size of the request body in bytes. Most HTTP clients set it automatically.

Request Body

  • Binary: send the raw image bytes as the request body with Content-Type: image/jpeg, image/png, or image/webp.
  • Multipart: use multipart/form-data and include the image as a file field named image.
  • JSON body (base64): send Content-Type: application/json with a JSON object containing the image as a base64-encoded string.

JSON Body Schema

Field Type Required Description
image String Yes Base64-encoded image (plain or data URI).

Response Format

The response contains a regions array. Each region has a confidence score (0–1) and a bounding box with pixel coordinates relative to the submitted image.

Property Description
confidence Confidence value (0..1) that an object was detected at this location.
region.top Top-most coordinate in pixels (floating point).
region.left Left-most coordinate in pixels (floating point).
region.bottom Bottom-most coordinate in pixels (floating point).
region.right Right-most coordinate in pixels (floating point).
classId Optional class identifier. A number, for example -1.

The response may contain additional fields, including modelName and modelVersion at the top level. Ignore fields you do not use.

Example Request

POST
https://api.nyris.io/find/v2/regions
import java.nio.file.Files;
import java.nio.file.Path;

OkHttpClient client = new OkHttpClient();

byte[] imageBytes = Files.readAllBytes(Path.of("image.jpg"));
RequestBody body = RequestBody.create(imageBytes, MediaType.parse("image/jpeg"));

Request request = new Request.Builder()
  .url("https://api.nyris.io/find/v2/regions")
  .post(body)
  .addHeader("x-api-key", "KEY")
  .build();

Response response = client.newCall(request).execute();

Example with JSON Body (Base64)

POST
https://api.nyris.io/find/v2/regions
{
    "image": "/9j/4AAQSkZJRg..."
}
import java.nio.file.Files;
import java.nio.file.Path;
import java.util.Base64;

OkHttpClient client = new OkHttpClient();

byte[] imageBytes = Files.readAllBytes(Path.of("image.jpg"));
String imageB64 = Base64.getEncoder().encodeToString(imageBytes);
String json = "{\"image\":\"" + imageB64 + "\"}";

RequestBody body = RequestBody.create(
    MediaType.parse("application/json"), json);

Request request = new Request.Builder()
    .url("https://api.nyris.io/find/v2/regions")
    .post(body)
    .addHeader("x-api-key", "KEY")
    .build();

Response response = client.newCall(request).execute();

Example Response

{
    "regions": [
        {
            "classId": -1,
            "confidence": 0.8876233,
            "region": {
                "top": 43.0,
                "left": 153.0,
                "bottom": 577.0,
                "right": 427.0
            }
        }
    ],
    "modelName": "redacted",
    "modelVersion": null
}

Recommend API

Get visually similar items for a product in your catalogue by submitting its SKU. Since we already have your product data (from the feed import), we perform a visual similarity search based on the main image of the given product. The submitted SKU itself is excluded from results.

GET https://api.nyris.io/recommend/v1/{sku}

Request Headers

Key Value
X-Api-Key The API key provided by us.
Accept (Optional) application/items.complete+json for full response.
Accept-Language (Optional) Filter results by language, e.g. de,*;q=0.5.

Path Parameters

Name Example Explanation
sku 03764560 The SKU of the product to find recommendations for.

Example Response

GET
https://api.nyris.io/recommend/v1/{sku}
{
    "id": "575e838883cf5e60859889c112b1463e",
    "session": "575e838883cf5e60859889c112b1463e",
    "results": [
        {
            "sku": "123454643",
            "image": "https://img.nyris.io/example.jpg",
            "score": 0.96
        },
        {
            "sku": "123454644",
            "image": "https://img.nyris.io/example2.jpg",
            "score": 0.92
        }
    ]
}

Images API

Retrieve the public images associated with a product by its SKU.

GET https://api.nyris.io/find/v1.1/images/{sku}

Request Headers

Key Value
X-Api-Key The API key provided by us.
Accept application/items.complete+json

Path Parameters

Name Example Explanation
sku D005 The SKU of the product to retrieve images for.

Example Response

GET
https://api.nyris.io/find/v1.1/images/{sku}
{
    "images": [
        "https://img.nyris.io/7bb/7bb5d2940a23084f307ed9a4bf8d74c6e03cf38cd10ca1e909eb6205774629ff.jpg"
    ]
}

Filter API

Retrieve the available filter attributes that can be used with the Find API.

GET https://api.nyris.io/find/v1/filters

Request Headers

Key Value
X-Api-Key The API key provided by us.
Accept application/items.complete+json

An optional limit query parameter is accepted, for example GET https://api.nyris.io/find/v1/filters?limit=100.

To look up values for one filter key, call GET https://api.nyris.io/find/v1/filters/{key}/{value}?limit=100. The response is a JSON array of strings.

Example Response

GET
https://api.nyris.io/find/v1/filters
[
    {
        "key": "MachineSerial",
        "values": ["SN-40821", "SN-40822"]
    },
    {
        "key": "Assembly",
        "values": ["Hydraulics", "Pneumatics"]
    }
]

Widget (JS)

The fastest and easiest way to integrate visual search to your website is using our Javascript Widget. All you have to do is to add some lines of code to a runtime file of your website and you are ready to start using visual search.

What I need to start using this code?

You need to follow these steps:

  1. Copy the script into a text editor and configure the settings parameters.
  1. After making the necessary updates, copy all the lines and add to the runtime file (ex. index.html) to embed the visual search widget into your platform.

Done. You are ready to use visual search on your website!

Sample

See the nyris demo widget in action: https://widget.nyris.app/

Where is the source code? Can I change it

The code is on github. Follow the readme file to build the project. You can change or adjust the JS / CSS files according to your needs and host them on your server as well.

Android SDK

You can find the latest Android SDK including the implementation documentation on github:

https://github.com/nyris/SDK.Android

iOS SDK

You can find the latest iOS SDK including the implementation documentation on github:

https://github.com/nyris/Nyris.IMX.iOS

Should you have any questions, please send an e-mail to support@nyris.io

Submit your product data

There are several ways to submit your product and image data to us. You can provide any kind of structured file that we can pull (xml, csv, txt, json, etc.), or use the Data Management API to programmatically manage your items and images. If you are already providing your data to an affiliate network like Affilinet, Awin or Google Shopping, you are probably all set for the start. In this case we just need access to your feed URL. Please be aware that those feeds usually only include the link to the main product image. As image recognition works best the more images of a specific product we have, you might need to adjust those feeds to include all your product images. Please find more details below.

Feed Format & Updates

  • The feed can be submitted as csv, xml, txt or json file
  • Preferred encoding for the files is UTF-8
  • For csv, please use if possible "," as delimiter and '"' (double quote) for enclosing text
  • Please provide us with a link to your feed. If the access to the feed is secured with username/password, please also provide us those credentials
  • Please revise your server settings in order to ensure that the IP address we use to access your server remains unblocked at all times in spite of frequent accessing
  • Your feed will be imported daily. Should you need shorter import cycles, please contact support@nyris.io

If you don't have a product feed, we can also directly access your product data through an API for the following systems:

Shop Systems

  • BigCommerce
  • Magento 1.x SOAP
  • PrestaShop
  • Shopify
  • WooCommerce

PIM

  • Akeneo
  • hybris

You can also create a data feed using Google Sheets. Details on how to use Google Sheets and an example file can be found in a section below.

Data Management API

The Data Management API lets you programmatically manage items and images in your nyris index. Use it to create, update, list, and delete product data and associated images.

The Data Management API requires a separate API key. For security, the keys used for the Find API and Recommend API do not work with the Data Management API, and vice versa. Contact mailto:support@nyris.io to obtain your Data API key.

The full OpenAPI reference is available at api.nyris.io/manage/swagger.

Authentication

All Data Management API requests require an API key passed via the X-Api-Key header. This key is different from the one used for the Find and Recommend APIs.

Key Value
X-Api-Key Your Data Management API key (not the same as the Find API key).

Create an Item

Creates a new item in your index. The key field is required and must be unique within your index.

POST
https://api.nyris.io/manage/v1/items

Example Request

{
    "key": "product-001",
    "text": {
        "name": "Red Running Shoe",
        "description": "Lightweight running shoe in red",
        "brand": "ExampleBrand",
        "language": "en-US"
    },
    "category": ["Shoes", "Running"],
    "links": {
        "main": "https://example.com/products/product-001"
    },
    "price": {
        "value": 89.99,
        "currency": "EUR"
    }
}

Example Response

{
    "id": "18b7f87a-1577-4486-a680-a460d970b584",
    "indexId": "adc23661-bc7f-4514-875e-c60d1ac61b93",
    "key": "product-001",
    "text": {
        "name": "Red Running Shoe",
        "description": "Lightweight running shoe in red",
        "brand": "ExampleBrand",
        "language": "en-US"
    },
    "category": ["Shoes", "Running"],
    "links": {
        "main": "https://example.com/products/product-001"
    },
    "price": {
        "value": 89.99,
        "currency": "EUR"
    },
    "images": [],
    "created": "2026-03-19T12:00:00Z",
    "modified": "2026-03-19T12:00:00Z"
}

Upsert an Item

Creates an item if the key does not exist yet, or updates the existing item. This is the recommended way to keep your index in sync with your product catalog.

The request body is the same as for creating an item. Returns 201 if a new item was created, 200 if an existing item was updated.

PUT
https://api.nyris.io/manage/v1/items

List Items

Returns a paginated list of items in your index.

Name Type Description
size integer Number of items per page (optional).
pageToken uuid Token for the next page (from a previous response).
sort string Sort by UpdateDate, CreationDate, or UniqueKey.
order string Ascending or Descending.
GET
https://api.nyris.io/manage/v1/items

Example Response

{
    "items": [
        {
            "id": "18b7f87a-1577-4486-a680-a460d970b584",
            "key": "product-001",
            "text": { "name": "Red Running Shoe" },
            "images": [],
            "created": "2026-03-19T12:00:00Z",
            "modified": "2026-03-19T12:00:00Z"
        }
    ],
    "pageToken": "18b7f87a-1577-4486-a680-a460d970b584",
    "nextPageToken": "29c5bf8b-2688-5597-b791-991443195695"
}

Get Item by Key

Retrieves a single item using your unique key.

GET
https://api.nyris.io/manage/v1/items/by-key?key={key}

Delete an Item

Deletes an item by its unique key. All associated images are also removed.

DELETE
https://api.nyris.io/manage/v1/items?key={key}

Add an Image to an Item

Adds an image to an existing item, identified by its key. You can provide the image as a URL (JSON body) or upload it directly (multipart/form-data or raw image bytes).

POST
https://api.nyris.io/manage/v1/items/by-key/images?key={key}

JSON body (image from URL)

{
    "url": "https://example.com/images/product-001.jpg",
    "visible": true,
    "main": true
}

Example Response

{
    "id": "63a4ac04-d267-442c-9be4-880332084127",
    "indexId": "adc23661-bc7f-4514-875e-c60d1ac61b93",
    "itemId": "18b7f87a-1577-4486-a680-a460d970b584",
    "url": "https://example.com/images/product-001.jpg",
    "visible": true,
    "main": true,
    "created": "2026-03-19T12:01:00Z"
}

Get Images for an Item

Returns all images belonging to an item.

GET
https://api.nyris.io/manage/v1/items/by-key/images?key={key}

Delete an Image

Removes a specific image by its ID.

DELETE
https://api.nyris.io/manage/v1/images/{id}

Full API Reference

The complete OpenAPI specification, including all endpoints, schemas, and error codes, is available in the interactive Swagger documentation:

api.nyris.io/manage/swagger

You can also import the OpenAPI spec directly into Postman or other API tools using the JSON URL:

https://api.nyris.io/manage/swagger/v1/swagger.json

Available Feed Attributes

The bare minimum is a unique ID (sku) and the main_image_link. To get a full list of possible attributes please download the following sheet or use a copy of it as base for your item or product feed. When gathering your data, please keep always in mind that all submitted information will be used for improving your search results. Completeness and quality of data often makes the difference:

Download Feed Example and full attribute description (google sheets)

Required Fields

Attribute Example Comment
sku sku1 unique item or product identifier (e.g. EAN code)
main_image_link https://storage.googleapis.com/nyris/share/image_a1.png main image link. should be publicly available. ideally, product in question should be at the center of the image

Optional Fields

Image Links

To improve the search, additional images can be provided. All referenced images (main, additional and recognition types) are used for search. main and additional images are treated identically, with the main image being the designated reference to be shown e.g. in the Portal. If requested, these image links can be returned as part of the response. recognition image links, on the other hand, are purely used for recognition and are never returned as part of any response.

Attribute Example Comment
additional_image_link_0 https://storage.googleapis.com/nyris/share/image_a1.png additional images
additional_image_link_1 additional images
additional_image_link_N Nth additional image
recognition_image_link_0 additional recognition images (images that will never show up on frontend) ideally taken from different angles and w/o packaging
recognition_image_link_1 https://storage.googleapis.com/nyris/share/image_r1.png additional recognition images (images that will never show up on frontend) ideally taken from different angles and w/o packaging
recognition_image_link_N Nth additional recognition image

Textual description

Providing these values will enable both OCR (text matching) functionality, finer grained recognition of results and item class distribution results. While optional, providing these values is thus strongly recommended.

Attribute Example Comment
title title 1 Product or item title (supports OCR recognition step)
brand brand 1 Brand (supports brand recognition step)
description_short description short 1 Short description (supports OCR recognition step)
description_long description long 1 Long description (supports OCR recognition step)
custom_category cat1 > cat2 your product or item category. Please separate with > (e.g. building materials, wood, windows & doors > windows > window ledges). Providing this field will be used to improve recommendations.
language de-DE Language Culture Name (https://learn.microsoft.com/en-us/openspecs/office_standards/ms-oe376/db9b9b72-b10b-4e7e-844c-09f88c972219)

Key-value pairs

By providing custom key-value pairs, we are able to improve detection in combination with text matching functionality, and/or will be able to return named IDs as part of the response. If your system requires multiple IDs or custom data pairs, use these fields.

Attribute Example Comment
custom_id_key_0 pzn custom ID key (e.g. for Barcode recognition) and
custom_id_value_0 123456 custom ID value (e.g. for Barcode recognition),
custom_id_key_N pzn Nth custom ID key and
custom_id_value_N 123456 Nth custom ID value

Catalog numbers

These values are mainly used for identifying products within catalogs, and catalogs by IDs.

Attribute Example Comment
catalog_number_0 3306 catalog numbers from your printed advertisement. Used by PDF importer
catalog_number_1 cat1 catalog numbers from your printed advertisement. Used by PDF importer
catalog_number_N ... Nth catalog number

Keywords

These values are used for exactly matching keywords, such as item materials, colors, etc., with text found during OCR; thus, providing these values improves text matching. Keywords are treated as a set, so values specified multiple times would be considered identical.

To simplify specification of keywords, you can specify keywords in a delimiter-separated list via the configurable keywords column. By default, keywords are split at the , (comma) delimiter:

Attribute Example Comment
keywords keyword, keyword, keyword 2 Registers the two keywords keyword and keyword 2
keyword(",") keyword 1, keyword 2 Registers the two keywords keyword 1 and keyword 2
keyword("AND") keyword 1 AND keyword 2 Registers the two keywords keyword 1 and keyword 2

To provide single keywords or individual keywords per column, use the keyword / keyword_N variant instead. Here, values are not split:

Attribute Example Comment
keyword a nice, individual keyword Registers the keyword a nice, individual keyword
keyword_0 keyword 0 The first keyword in a set
keyword_1 keyword 1 The second keyword in a set
keyword_N ... The Nth keyword in a set

Geolocation Fields

If provided, the coordinates can be used to narrow down the search by the location of the product. The provided coordinates have to be in the WGS84 format with signed decimal numbers as the notation.

Attribute Example Comment
latitude 52.5200 Latitude of the product. Use together with longitude to associate the product with a location.
longitude 13.4050 Longitude of the product. Use together with latitude to associate the product with a location.

Item relations

Some items are related to other entries, and some IDs may have different meaning across logical separations of feeds (e.g. when there is a feed per country).

Attribute Example Comment
feed_id 1 Feed ID, required only if you have multiple feeds as one merchant. The value has to be an integer.
group_id 23 for variation products (size, colour etc. variations)

Item URLs

Attribute Example Comment
main_offer_link https://www.nyris.io/products/visual-search-api product or landing page link
mobile_offer_link https://vimeo.com/788577734 secondary product or landing page link (mobile, video etc.)

PreFilter Attributes

Attribute Example Comment
filter_key_0 color filter key used by faceted search and
filter_value_0 Red,Blue comma separated filter values for the key for the item,
filter_key_N shape Nth filter key (Max N = 10)and
filter_value_N candle,capsule Nth filter values for the item

Other Optional Fields

Attribute Example Comment
meta_data some additional text data any data you would like to store additionally (text field); the content is not interpreted
availability in stock valid values: in stock, out of stock, preorder, back in stock soon, removed
price 17.30 EUR . as decimal, 3-digit ISO Code for currency
sale_price 15.00 EUR . as decimal, 3-digit ISO Code for currency
image_version 1234 image version; if changed, images will be re-imported

Visual search analytics show how customers interact with search results and help improve search performance over time.

Nyris provides two levels of analytics:

Type Merchant implementation What you get
General usage & index analytics None required Search volume, product count, image count in your index
Click & conversion analytics Required (see below) Which results were clicked, which led to conversions, tied to each search request_id

This document covers click & conversion analytics — how to send customer events to the Nyris Feedback API.


What the script does

Once embedded, the script:

  • intercepts Nyris Find API responses and captures the request_id of each search,
  • detects clicks on search-result cards and CTA buttons,
  • detects add-to-cart actions by configurable button labels,
  • forwards conversions declared via HTML attributes, JavaScript, or your GA4 dataLayer,
  • and posts events to the Feedback API, keyed to the originating search.

It requires no changes to your store's source code in the typical case and is built so it cannot break the host page (all logic is error-isolated).

Quick start

Place the tag in the <head>. async is recommended.

Minimal — only the API key is required:

Typical commerce setup — configure result cards, product ids, and add-to-cart labels:

With this markup, a click on the card (or its link) is reported as a click, and a click on the add-to-cart button is reported as a conversion — both carrying the product id:

<script
  async
  src="https://assets.nyris.io/nyris-tracking/v1/nyris-tracker.min.js"
  data-api-key="YOUR_NYRIS_API_KEY">
</script>
<script
  async
  src="https://assets.nyris.io/nyris-tracking/v1/nyris-tracker.min.js"
  data-api-key="YOUR_NYRIS_API_KEY"
  data-results-selector=".product-card"
  data-product-id-attr="data-product-id"
  data-cart-button-labels="Add to cart,In den Warenkorb">
</script>
<div class="product-card" data-product-id="SKU-9987">
  <a href="/products/sku-9987">Trail Runner Jacket</a>
  <button type="button">Add to cart</button>
</div>

Prerequisites

The script works when the browser calls the Nyris Find API directly (https://api.nyris.io/find/...). It intercepts those responses to obtain the request_id.

If your store proxies search through your own backend and never calls the Find API from the browser, inject the request id into the results container instead:

Without a request_id (from interception or data-nyris-request-id), click and conversion events are not sent.

<div id="search-results" data-nyris-request-id="REQUEST_ID_FROM_SERVER">
  <!-- result cards -->
</div>

Product id resolution

The tracker resolves the product id for each result card in this order:

  1. Attribute — value of data-product-id-attr on (or inside) the card (default: data-product-id).
  2. Text — trimmed text of data-product-text-selector, if configured.
  3. Position — SKU at the same zero-based position in the Find API response (no DOM markup required).

For most stores the position fallback works out of the box when result order matches the API response.

Button label matching

Add-to-cart and support requests are detected from the text of the button that was clicked. The same matcher backs data-cart-button-labels and data-support-button-labels.

Multilingual stores: list every language in the same attribute. Labels are comma-separated and there is no locale detection — a click counts when any label matches:

Matching rules:

  • Case-insensitive. ADD TO CART and in den warenkorb both match.
  • Setting the attribute replaces the defaults (Add to cart, Add to basket, In den Warenkorb) rather than adding to them, so list every label you need. Clicks that match no label are reported as an ordinary click.
  • Exact or prefix. Add to cart also matches Add to cart – €49.99, which is useful for buttons that include a price. A very short label such as Add will over-match — keep labels specific.
  • Ancestor walk. Matching starts at the clicked element and walks up to 6 ancestors, skipping any whose combined text exceeds 60 characters. This lets a click on an icon or <span> inside the button register, without a whole card counting as a button.

For support requests, pair the labels with data-support-button-scope so only clicks inside your inquiry dialog qualify — see below.

data-cart-button-labels="Add to cart,In den Warenkorb"

Declaring purchases and support requests

Add-to-cart is detected automatically from button labels. Purchases and support requests must be declared when they happen.

Supported conversion subtypes (fixed): add_to_cart, purchase, support_request. Unknown values are ignored by the tracker.

Purchase — HTML attributes:

Support request — script tag only (preferred for inquiry Send):

No button markup. Matching label clicks inside the scope fire support_request. Omit scope only for intentional page-wide matching.

Or mark a specific button with data-nyris-conversion="support_request".

JavaScript API:

Custom DOM event (useful with tag managers):

If you already send GA4 e-commerce events to window.dataLayer, enable the GA4 bridge (default) to mirror add_to_cart and purchase events automatically. Support requests are not a standard GA4 ecommerce event — declare them via HTML, JS, or the custom DOM event.

<button
  data-nyris-conversion="purchase"
  data-nyris-product-ids="SKU-9987,SKU-1234"
  data-nyris-order-id="ORD-123"
  data-nyris-revenue-amount="49.99"
  data-nyris-revenue-currency="EUR">
  Complete purchase
</button>
<script async
  src="https://assets.nyris.io/nyris-tracking/v1/nyris-tracker.min.js"
  data-api-key="YOUR_NYRIS_API_KEY"
  data-results-selector=".wrap-main-item-result"
  data-support-button-labels="Send,Senden"
  data-support-button-scope="[role=dialog], .inquiry-modal">
</script>

Configuration reference

Set options as data-* attributes on the script tag, or as window.nyrisTrackerConfig before the script loads.

data-* attribute nyrisTrackerConfig key Default Description
data-api-key apiKey — (required) Nyris API key. Sent as X-Api-Key.
data-results-selector resultsSelector .product-card CSS selector for a single search-result card.
data-results-container resultsContainer document.body Scope where results live.
data-product-id-attr productIdAttr data-product-id Attribute holding the product id/SKU.
data-product-text-selector productTextSelector none CSS selector whose text is the product id.
data-cart-button-labels cartButtonLabels Add to cart,Add to basket,In den Warenkorb Comma-separated, case-insensitive add-to-cart labels. Replaces defaults when set.
data-support-button-labels supportButtonLabels none Labels for support/inquiry Send (e.g. Send,Senden). Fires support_request — one script tag, no button markup.
data-support-button-scope supportButtonScope none CSS selector limiting those matches (e.g. [role=dialog], .inquiry-modal). Recommended.
data-mode mode cookiefree cookiefree or full. See GDPR modes.
data-attribution-window-minutes attributionWindowMinutes 30 How long a search stays eligible for attributed conversions.
data-ga4-bridge ga4Bridge true Bidirectional GA4 dataLayer integration.
data-feedback-url feedbackUrl https://api.nyris.io/feedback/v1 Override endpoint (restricted to api.nyris.io/feedback/).
data-debug debug false Log [NyrisTracker] messages to the console.

window.nyrisTrackerConfig example:

<script>
  window.nyrisTrackerConfig = {
    apiKey: 'YOUR_NYRIS_API_KEY',
    resultsSelector: '.search-result',
    cartButtonLabels: ['Add to cart', 'Buy now'],
  };
</script>
<script async src="https://assets.nyris.io/nyris-tracking/v1/nyris-tracker.min.js"></script>

JavaScript API

Method Description
NyrisTracker.conversion(subtype, options) Report a conversion. subtype: add_to_cart, purchase, or support_request. options: product_ids, positions, quantity, revenue (purchase).
NyrisTracker.grantConsent() Switch to full mode after cookie consent.
NyrisTracker.getRequestId() Current Nyris request_id, or null.

GDPR modes

Mode Storage Consent Notes
cookiefree (default) sessionStorage, else in-memory Not required No cookies, no persistent visitor id.
full localStorage Required Call NyrisTracker.grantConsent() after opt-in.

The default cookiefree mode uses no cookies and no cross-session identifier. Confirm with your DPO whether a consent banner is required for your jurisdiction.

Debugging

Enable debug output while integrating:

Then open the browser console and look for [NyrisTracker] initialized, search captured, click, and add_to_cart / conversion messages.

data-debug="true"

When the script is not enough

Use the raw Feedback API implementation below if you:

  • cannot call the Find API from the browser,
  • need full control over event timing and payloads,
  • integrate from a mobile app or server-side system, or
  • already have your own analytics pipeline and only need to forward events.

2. Alternative: Raw Feedback API implementation

Implement click and conversion analytics yourself by POSTing JSON events to the Feedback API. Use this path when you cannot embed the tracking script or need a custom integration.

Overview

Every Find API response includes a unique request id and optionally a session id:

Source Header JSON body fallback
Request id X-Matching-Request id
Session id X-Session session

Store the request id when you receive search results. Attach it to every subsequent click or conversion event for that search.

Perform a Find API search first (see Find API documentation), then POST events to the Feedback API using the same API key.

Endpoint

POST https://api.nyris.io/feedback/v1

Request headers

Header Value
X-Api-Key Your Nyris API key
Content-Type application/json, application/event+json, or text/json

Request body

Every event is a JSON object with these fields:

Property Required Description
request_id Yes Request id from the Find API response
session_id No Session id from the Find API response. Groups multiple searches from the same user.
timestamp Yes Event time in ISO 8601 UTC, e.g. 2020-11-06T17:53:25.511Z
event Yes Event type: click, conversion, feedback, or region
data Depends on event Event-specific payload (see below)

A successful POST returns HTTP 2xx. Invalid JSON or missing required fields may return an error.

Event types

event value Purpose
click User clicked or selected a result (position and/or product id)
conversion A result led to business value (e.g. add to cart, purchase)
feedback End-user feedback on the result set ("Did we find what you were looking for?")
region User selected a crop/region before searching (object proposal mode)

Click events

Tracks which result the user interacted with.

data properties:

Property Type Description
product_ids string[] Product ids as in your product data / Find API SKUs
positions number[] Zero-based index in the Find API result list

Provide product ids where possible — they remain accurate even if the frontend reorders or filters results.

Example:


{
  "request_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "session_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "timestamp": "2020-11-06T17:53:25.511Z",
  "event": "click",
  "data": {
    "positions": [0],
    "product_ids": ["1234"]
  }
}

Conversion events

Tracks that a search result led to a conversion (add to cart, purchase, support request, etc.).

data properties:

Property Type Description
subtype string Optional conversion type. Recommended fixed values below. Max 128 chars; trimmed and lowercased by the API.
product_ids string[] Product ids involved in the conversion
positions number[] Zero-based positions in the original result list
quantity number Optional item quantity
revenue object Optional; typically used with purchase
revenue.amount number Order total
revenue.currency string ISO 4217 currency code
revenue.order_id string Optional order id (deduplication / reporting)

The data field is optional. A conversion with no data still marks the request as successful if a prior click event was sent for the same request_id.

The API accepts any non-empty subtype string. For consistent analytics, use these fixed values:

subtype Meaning
add_to_cart Soft conversion — product from search added to cart
purchase Hard / sales conversion — order completed
support_request User started support from search (chat, email, ticket)

Omit subtype for legacy undifferentiated conversions.

Example — add to cart:

Example — purchase:

Example — support request:

Example (minimal — relies on prior click):


{
  "request_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "session_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "timestamp": "2020-11-06T17:53:25.511Z",
  "event": "conversion",
  "data": {
    "subtype": "add_to_cart",
    "positions": [0],
    "product_ids": ["1234"],
    "quantity": 1
  }
}
{
  "request_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "session_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "timestamp": "2020-11-06T17:53:25.511Z",
  "event": "conversion",
  "data": {
    "subtype": "purchase",
    "positions": [0],
    "product_ids": ["1234"],
    "quantity": 1,
    "revenue": {
      "amount": 49.99,
      "currency": "EUR",
      "order_id": "ORD-123"
    }
  }
}
{
  "request_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "session_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "timestamp": "2020-11-06T17:53:25.511Z",
  "event": "conversion",
  "data": {
    "subtype": "support_request",
    "product_ids": ["1234"],
    "positions": [0]
  }
}
{
  "request_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "session_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "timestamp": "2020-11-06T17:53:25.511Z",
  "event": "conversion"
}

Feedback events

End-user feedback on the overall result set, e.g. from a "Did we find what you were looking for?" prompt.

data properties:

Property Type Description
success boolean Whether the search was successful from the user's perspective
comment string Optional free-text comment

Where possible, also send click and conversion events — they provide automatic, behaviour-based feedback.

Example:


{
  "request_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "session_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "timestamp": "2020-11-06T17:53:25.511Z",
  "event": "feedback",
  "data": {
    "success": true,
    "comment": "Thank you!"
  }
}

Region events

Tracks the image region the user selected before searching (crop / object proposal mode).

data properties:

Property Type Description
rect object Normalized crop rectangle

rect properties (all fractions in the range 0..1):

Property Description
x Left edge, as a fraction of image width
y Top edge, as a fraction of image height
w Width, as a fraction of image width
h Height, as a fraction of image height

A region covering the full image: "x": 0.0, "y": 0.0, "w": 1.0, "h": 1.0.

Example:


{
  "request_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "session_id": "e8848827-27af-41d7-a98f-1a2f94b0f19a",
  "timestamp": "2020-11-06T17:53:25.511Z",
  "event": "region",
  "data": {
    "rect": {
      "x": 0.25,
      "y": 0.25,
      "w": 0.5,
      "h": 0.5
    }
  }
}

Implementation checklist (raw)

  1. Capture ids — On every Find API response, read X-Matching-Request / X-Session (or id / session from the JSON body if headers are not exposed).
  2. Store ids — Keep the request id available for the user's session (in memory, session storage, or your backend).
  3. Send click — When the user clicks a result, POST a click event with product_ids and positions.
  4. Send conversion — When the user converts (cart, purchase, etc.), POST a conversion event with the same request_id and relevant product_ids.
  5. Send feedback / region — If your UI collects explicit feedback or region selection, POST those event types as applicable.

Product ids must match the SKUs in your product data and Find API results, or Nyris cannot correlate events with your catalogue.


Example: end-to-end with curl

All event types should return HTTP 2xx when the payload is valid.

Show examples in:
nyris
nyris API Docs