Version 1.4.0

Snapdex API

Index product images and find products by photo. Create a collection, add products with their images, wait for indexing, then search with any photo. Every mutation that touches the index is asynchronous: it returns an indexing run you can poll.

Base URL https://snapdex.dev/api/v1. All responses are JSON; send Accept: application/json.

Quickstart

From nothing to a working photo search in four requests. Create an API key under Settings → API keys first.

BASE=https://snapdex.dev/api/v1; KEY=sdx_your_key

# 1. a collection; its id in the response is what the other calls use
curl -X POST $BASE/collections -H "Authorization: Bearer $KEY"   -H "Content-Type: application/json" -d '{"name":"Sneakers"}'
COLLECTION=01k6h2m4q8r3t5v7x9z1b3d5f7

# 2. a product
curl -X POST $BASE/collections/$COLLECTION/products -H "Authorization: Bearer $KEY"   -H "Content-Type: application/json" -d '{"id":"sku-100","name":"Red runner"}'

# 3. its images (indexing starts right away; the response contains the run to poll)
curl -X POST $BASE/collections/$COLLECTION/products/sku-100/images -H "Authorization: Bearer $KEY"   -F "files[][email protected]" -F "files[][email protected]"

# 4. search with any photo
curl -X POST $BASE/collections/$COLLECTION/search -H "Authorization: Bearer $KEY" -F "[email protected]"

Prefer a GUI client? Postman, Insomnia and Bruno import the OpenAPI document directly and give you every request pre-filled.

SDKs

Official clients for JavaScript/TypeScript and PHP. Their method names are the operationIds used on this page, so every endpoint below tells you the SDK method to call. Each download is a small source package with a README; drop it into your project (npm and Packagist releases are coming).

// JavaScript / TypeScript
import { Snapdex } from '@snapdex/sdk';

const snapdex = new Snapdex({ apiKey: KEY });
await const { id: collection } = await snapdex.createCollection({ name: 'Sneakers' });
await snapdex.createProduct(collection, { id: 'sku-100', name: 'Red runner' });
const { run } = await snapdex.addProductImages(collection, 'sku-100', [{ url: 'https://cdn.example.com/a.jpg' }]);
await snapdex.waitForRun(collection, run.id);
const { data } = await snapdex.search(collection, photoBlob, { limit: 3 });
// PHP
use Snapdex\Client;

$snapdex = new Client($key);
['id' => $collection] = $snapdex->createCollection(['name' => 'Sneakers']);
$snapdex->createProduct($collection, ['id' => 'sku-100', 'name' => 'Red runner']);
['run' => $run] = $snapdex->addProductImages($collection, 'sku-100', ['https://cdn.example.com/a.jpg']);
$snapdex->waitForRun($collection, $run['id']);
$result = $snapdex->search($collection, '/path/photo.jpg', limit: 3);

Authentication

Create keys under Settings → API keys. Send Authorization: Bearer <key> on every request. A key sees everything its team owns.

curl https://snapdex.dev/api/v1/collections \
  -H "Authorization: Bearer sdx_your_key" \
  -H "Accept: application/json"

Errors

Errors use conventional status codes and always carry a message. Validation failures (422) add an errors map with the problems per field. 404 means the resource does not exist in your team. 402 means a plan limit was reached (see Plans and quotas) and 429 that you are sending too many requests (see Rate limits). From search, 503 means the search service is briefly unavailable: retry with backoff; 502 means it could not process that particular photo, so retrying the same photo will not help.

{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

Rate limits

Requests per minute are limited per API key (120 on the free plan, more on paid plans). Over the limit you get 429 with a Retry-After header. Bulk work (CSV import, adding many images in one request) counts as one request, so prefer it over many single calls.

Plans and quotas

Every team is on a plan that includes a number of searches per calendar month, stored images and collections. On the free plan these are hard limits: once one is reached the request fails with 402 and a quota object naming the limit, and it succeeds again when you free capacity, the month starts over or you upgrade. Paid plans keep working past their included searches and images and bill the extra per 1,000; only stored images have a ceiling, where uploads fail with 402. Check where you stand with GET /usage or the X-Quota-Searches-* headers on search responses. Requests per minute also depend on the plan and are reported in the X-RateLimit-* headers.

Pagination

List endpoints that can grow large (products) are paginated with page and per_page and return links and meta alongside data.

Typical workflow

  1. Create a collection (POST /collections), once.
  2. Create products with your own ids and metadata (POST /collections/{id}/products), or import a CSV.
  3. Add images to each product (POST …/products/{id}/images). This returns an indexing run; poll it, or just check the product's images.indexed count.
  4. Search with a photo (POST /collections/{id}/search) and use confidence to decide whether to trust the top result.

Webhooks

Set webhook_url on a collection (API or portal) and we POST a JSON event to it. Verify each request by computing an HMAC-SHA256 of the raw body with the collection's webhook.secret and comparing it to the X-Webhook-Signature header (sha256=<hex>). The X-Webhook-Event and X-Webhook-Delivery headers carry the event name and delivery id; deliveries are retried, so treat the id as the idempotency key.

eventindexing_run.finished

An indexing run finished

Sent when an import, retry or full re-index reaches a final status (completed, completed_with_errors, failed or cancelled). Respond with any 2xx within 15 seconds; anything else is retried with growing delays (1 min, 5 min, 15 min, 1 h, 2 h, 6 h) before the delivery is marked failed.

Payload

event"indexing_run.finished" | "webhook.test"
created_atdatetime
collectionobject
runobject
messagestringOnly for webhook.test.
{
  "event": "indexing_run.finished",
  "created_at": "2026-09-24T19:40:12Z",
  "collection": {
    "id": "01k6h2m4q8r3t5v7x9z1b3d5f7",
    "name": "Sneakers"
  },
  "run": {
    "id": "01k6h3s9w4c2e7f5g8j1k3m6n9",
    "type": "import",
    "status": "completed",
    "total": 12,
    "processed": 12,
    "indexed": 12,
    "failed": 0,
    "percent": 100,
    "replaces_index": false,
    "created_at": "2026-09-24T19:39:50Z",
    "started_at": "2026-09-24T19:39:51Z",
    "finished_at": "2026-09-24T19:40:12Z"
  }
}

Verifying the signature

// Node.js
const crypto = require('crypto');
const expected = 'sha256=' + crypto.createHmac('sha256', WEBHOOK_SECRET).update(rawBody).digest('hex');
const ok = crypto.timingSafeEqual(Buffer.from(expected), Buffer.from(req.headers['x-webhook-signature']));

// PHP
$expected = 'sha256=' . hash_hmac('sha256', $rawBody, $webhookSecret);
$ok = hash_equals($expected, $_SERVER['HTTP_X_WEBHOOK_SIGNATURE']);

Changelog

1.4.0 · 2026-10-01

  • Product metadata can hold a list for a key with several values, e.g. {"color": ["red", "white"]}. A search filter on that key matches any of the values. Keys with a single value are unchanged.
  • In the portal, repeating a key in a product's metadata gives it several values.

1.3.0 · 2026-10-01

  • Search filters: mark product metadata keys as filterable with the new filterable_keys field of a collection, then send a filter with a search, e.g. {"color": "red", "size": ["41", "42"]}, to only consider products whose metadata matches. Changing a product's filterable metadata or the collection's filterable keys takes effect without a re-index.
  • The SDKs' search methods accept a filter.

1.2.0 · 2026-10-01

  • Breaking: collections are addressed by a ULID instead of their slug. This applies to the collection id field, the {collection} path parameter and the collection.id in webhook payloads. Unlike the slug, which came from the name a collection was created with, the id has nothing to do with the collection's name. Find your collections' ids with GET /collections or on the collection page in the portal.

1.1.0 · 2026-10-01

  • Breaking: image, indexing run and webhook delivery ids are now strings (ULIDs) instead of integers. This applies to their id fields, the {image} and {run} path parameters, image_id in search results, the run in webhook payloads and the X-Webhook-Delivery header.
  • Product ids can no longer contain a slash, since they are part of the product's URL.

1.0.0 · 2026-09-24

  • Initial public release: collections, products with metadata, images (upload or URL), CSV import, indexing runs, search by photo.
  • Webhooks: indexing_run.finished and webhook.test events, signed with a per-collection secret.
  • Plans and quotas: GET /usage, 402 responses when a limit is reached, X-Quota-Searches-* headers on search.
  • Official SDKs for JavaScript/TypeScript and PHP.

Collections

A collection is one searchable catalogue with its own index. Products of a collection with has_groups can share a group_id; searches then report one result per group.

get/collections

List collections sdk: listCollections()

All collections of the team that owns the API key, alphabetically.

Responses

200The collections.

dataobject[]
↳ idstringA ULID; what URLs and webhooks refer to the collection by. It stays the same when the collection is renamed.
↳ namestring
↳ descriptionstring | null
↳ has_groupsbooleanWhether products can share a group_id and searches report one result per group.
↳ filterable_keysstring[]The product metadata keys that searches can filter on; see the `filter` of a search.
↳ modelstring | nullThe embedding model the index was built with.
↳ productsinteger
↳ imagesobject
↳ active_runsinteger
↳ created_atdatetime
↳ updated_atdatetime
↳ webhookobject | nullPresent when a webhook is configured.

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections -H "Authorization: Bearer $KEY"
post/collections

Create a collection sdk: createCollection()

Creates an empty collection and its vector index. has_groups is fixed at creation.

Request body · application/json

namestring
descriptionstring | null
has_groupsbooleanOnly on creation.
filterable_keysarray | nullThe product metadata keys that searches can filter on. Only these keys are copied into the search index; changing them updates the index in the background within moments, without a re-index. Keys cannot contain =. Send null or [] to turn filtering off.
webhook_urlstring | nullWhere to POST events; null disables webhooks. A signing secret is generated the first time a URL is set.
rotate_webhook_secretbooleanGenerate a new signing secret (update only).
{
  "name": "Sneakers",
  "description": "All sneaker product photos",
  "has_groups": true
}

Responses

201The new collection.

dataobject
↳ idstringA ULID; what URLs and webhooks refer to the collection by. It stays the same when the collection is renamed.
↳ namestring
↳ descriptionstring | null
↳ has_groupsbooleanWhether products can share a group_id and searches report one result per group.
↳ filterable_keysstring[]The product metadata keys that searches can filter on; see the `filter` of a search.
↳ modelstring | nullThe embedding model the index was built with.
↳ productsinteger
↳ imagesobject
↳ active_runsinteger
↳ created_atdatetime
↳ updated_atdatetime
↳ webhookobject | nullPresent when a webhook is configured.

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

402A plan limit was reached. `quota` names the limit; upgrade the plan or free up capacity.

messagestring
quotaobject
↳ metric"searches_per_month" | "images" | "collections"
↳ limitinteger
↳ usedinteger
{
  "message": "The monthly search quota of your plan is used up. Upgrade the plan or wait for the next month.",
  "quota": {
    "metric": "searches_per_month",
    "limit": 1000,
    "used": 1000
  }
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X POST https://snapdex.dev/api/v1/collections -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"name":"Sneakers","has_groups":true}'
get/collections/{collection}

Get a collection sdk: getCollection()

The collection with its product and image counts.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Responses

200The collection.

dataobject
↳ idstringA ULID; what URLs and webhooks refer to the collection by. It stays the same when the collection is renamed.
↳ namestring
↳ descriptionstring | null
↳ has_groupsbooleanWhether products can share a group_id and searches report one result per group.
↳ filterable_keysstring[]The product metadata keys that searches can filter on; see the `filter` of a search.
↳ modelstring | nullThe embedding model the index was built with.
↳ productsinteger
↳ imagesobject
↳ active_runsinteger
↳ created_atdatetime
↳ updated_atdatetime
↳ webhookobject | nullPresent when a webhook is configured.

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7 -H "Authorization: Bearer $KEY"
patch/collections/{collection}

Update a collection sdk: updateCollection()

Change the name, description, webhook or filterable keys. has_groups cannot be changed after creation.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Request body · application/json

namestring
descriptionstring | null
has_groupsbooleanOnly on creation.
filterable_keysarray | nullThe product metadata keys that searches can filter on. Only these keys are copied into the search index; changing them updates the index in the background within moments, without a re-index. Keys cannot contain =. Send null or [] to turn filtering off.
webhook_urlstring | nullWhere to POST events; null disables webhooks. A signing secret is generated the first time a URL is set.
rotate_webhook_secretbooleanGenerate a new signing secret (update only).
{
  "description": "Sneakers, spring catalogue"
}

Responses

200The updated collection.

dataobject
↳ idstringA ULID; what URLs and webhooks refer to the collection by. It stays the same when the collection is renamed.
↳ namestring
↳ descriptionstring | null
↳ has_groupsbooleanWhether products can share a group_id and searches report one result per group.
↳ filterable_keysstring[]The product metadata keys that searches can filter on; see the `filter` of a search.
↳ modelstring | nullThe embedding model the index was built with.
↳ productsinteger
↳ imagesobject
↳ active_runsinteger
↳ created_atdatetime
↳ updated_atdatetime
↳ webhookobject | nullPresent when a webhook is configured.

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X PATCH https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7 -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"description":"Spring catalogue"}'
delete/collections/{collection}

Delete a collection sdk: deleteCollection()

Deletes the collection, all its products and images, and the index. This cannot be undone.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Responses

204Deleted.

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X DELETE https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7 -H "Authorization: Bearer $KEY"

Products

What a search returns. Identified by your own id, with an optional name, group and free-form metadata.

get/collections/{collection}/products

List products sdk: listProducts()

Paginated products of the collection, oldest first. Filter with search (matches id and name) or group_id.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
searchquerystringSubstring of the product id or name.
group_idquerystringOnly products in this group.
pagequeryinteger
per_pagequeryinteger

Responses

200A page of products.

dataobject[]
↳ idstringYour identifier.
↳ namestring | null
↳ group_idstring | null
↳ metadataobjectFree-form key/value pairs, returned with search results. A key with several values has a list.
↳ imagesobject
↳ image_listobject[]Only present on `getProduct`.
↳ created_atdatetime
↳ updated_atdatetime
linksobject
↳ firststring | null
↳ laststring | null
↳ prevstring | null
↳ nextstring | null
metaobject
↳ current_pageinteger
↳ last_pageinteger
↳ per_pageinteger
↳ totalinteger

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl "https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/products?search=red&per_page=20" -H "Authorization: Bearer $KEY"
post/collections/{collection}/products

Create a product sdk: createProduct()

Create the product first, then add its images. id is your own identifier and must be unique within the collection; group_id is only stored for collections with has_groups.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Request body · application/json

idstringRequired on creation; unique within the collection. Cannot contain a slash, because it is part of the product's URL.
namestring | null
group_idstring | null
metadataobjectFree-form key/value pairs, returned with search results. A value is text or a number, or a list of up to 50 for a key with several values, e.g. {"color": ["red", "white"]}; values are stored as text. The values of the collection's filterable_keys can also be used to filter searches, and a filter matches any value of a list.
{
  "id": "sku-100",
  "name": "Red runner",
  "group_id": "runner",
  "metadata": {
    "brand": "Acme",
    "colour": "red"
  }
}

Responses

201The new product.

dataobject
↳ idstringYour identifier.
↳ namestring | null
↳ group_idstring | null
↳ metadataobjectFree-form key/value pairs, returned with search results. A key with several values has a list.
↳ imagesobject
↳ image_listobject[]Only present on `getProduct`.
↳ created_atdatetime
↳ updated_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X POST https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/products -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"id":"sku-100","name":"Red runner","metadata":{"brand":"Acme"}}'
get/collections/{collection}/products/{product}

Get a product sdk: getProduct()

The product with its metadata and the list of its images and their indexing status.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
productpath · requiredstringYour product id, as given when the product was created.

Responses

200The product, including `image_list`.

dataobject
↳ idstringYour identifier.
↳ namestring | null
↳ group_idstring | null
↳ metadataobjectFree-form key/value pairs, returned with search results. A key with several values has a list.
↳ imagesobject
↳ image_listobject[]Only present on `getProduct`.
↳ created_atdatetime
↳ updated_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/products/sku-100 -H "Authorization: Bearer $KEY"
patch/collections/{collection}/products/{product}

Update a product sdk: updateProduct()

Partial update: only the fields you send change. Sending metadata replaces the whole map. Changing group_id only affects search after a full re-index, because the group is stored in the index; the response then carries a warning.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
productpath · requiredstringYour product id, as given when the product was created.

Request body · application/json

idstringRequired on creation; unique within the collection. Cannot contain a slash, because it is part of the product's URL.
namestring | null
group_idstring | null
metadataobjectFree-form key/value pairs, returned with search results. A value is text or a number, or a list of up to 50 for a key with several values, e.g. {"color": ["red", "white"]}; values are stored as text. The values of the collection's filterable_keys can also be used to filter searches, and a filter matches any value of a list.
{
  "name": "Red runner v2",
  "metadata": {
    "brand": "Acme",
    "season": "SS26"
  }
}

Responses

200The updated product.

dataobject
↳ idstringYour identifier.
↳ namestring | null
↳ group_idstring | null
↳ metadataobjectFree-form key/value pairs, returned with search results. A key with several values has a list.
↳ imagesobject
↳ image_listobject[]Only present on `getProduct`.
↳ created_atdatetime
↳ updated_atdatetime
warningstring

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X PATCH https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/products/sku-100 -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"name":"Red runner v2"}'
delete/collections/{collection}/products/{product}

Delete a product sdk: deleteProduct()

Deletes the product and its images; they stop matching immediately.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
productpath · requiredstringYour product id, as given when the product was created.

Responses

204Deleted.

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X DELETE https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/products/sku-100 -H "Authorization: Bearer $KEY"

Images

Photos of a product. New images are indexed incrementally in the background; the live index keeps answering searches meanwhile.

get/collections/{collection}/products/{product}/images

List a product's images sdk: listProductImages()

All images of the product with their indexing status.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
productpath · requiredstringYour product id, as given when the product was created.

Responses

200The images.

dataobject[]
↳ idstringA ULID.
↳ product_idstring
↳ status"pending" | "queued" | "processing" | "indexed" | "failed"Only `indexed` images take part in searches.
↳ errorstring | nullWhy indexing failed.
↳ filenamestring | null
↳ source_urlstring | null
↳ indexed_atdatetime
↳ created_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/products/sku-100/images -H "Authorization: Bearer $KEY"
post/collections/{collection}/products/{product}/images

Add images to a product sdk: addProductImages()

Upload files (multipart files[]) or pass public urls (JSON) that we download. The images are stored and an import run indexes them in the background; existing images are untouched. Poll the returned run, or the product, to see when they are indexed. JPG, PNG or WebP, up to 15 MB and 100 images per request.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
productpath · requiredstringYour product id, as given when the product was created.

Request body · multipart/form-data

files[] *file[]

Request body · application/json

urls *string[]
{
  "urls": [
    "https://cdn.example.com/sku-100-front.jpg",
    "https://cdn.example.com/sku-100-side.jpg"
  ]
}

Responses

202Accepted: the images and the indexing run that processes them.

dataobject[]
↳ idstringA ULID.
↳ product_idstring
↳ status"pending" | "queued" | "processing" | "indexed" | "failed"Only `indexed` images take part in searches.
↳ errorstring | nullWhy indexing failed.
↳ filenamestring | null
↳ source_urlstring | null
↳ indexed_atdatetime
↳ created_atdatetime
runobject
↳ idstringA ULID.
↳ type"import" | "reindex" | "retry"
↳ status"queued" | "running" | "completed" | "completed_with_errors" | "failed" | "cancelled"
↳ totalinteger
↳ processedinteger
↳ indexedinteger
↳ failedinteger
↳ percentinteger
↳ replaces_indexbooleanTrue for a full re-index that swaps in a new index when it completes.
↳ created_atdatetime
↳ started_atdatetime
↳ finished_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

402A plan limit was reached. `quota` names the limit; upgrade the plan or free up capacity.

messagestring
quotaobject
↳ metric"searches_per_month" | "images" | "collections"
↳ limitinteger
↳ usedinteger
{
  "message": "The monthly search quota of your plan is used up. Upgrade the plan or wait for the next month.",
  "quota": {
    "metric": "searches_per_month",
    "limit": 1000,
    "used": 1000
  }
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X POST https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/products/sku-100/images -H "Authorization: Bearer $KEY" -F "files[][email protected]" -F "files[][email protected]"
get/collections/{collection}/images/{image}

Get an image sdk: getImage()

The indexing status of one image, including the error when it failed.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
imagepath · requiredstringThe image id (a ULID, e.g. 01k6h3t7v2m9q8x4r5n0c1b2d3).

Responses

200The image.

dataobject
↳ idstringA ULID.
↳ product_idstring
↳ status"pending" | "queued" | "processing" | "indexed" | "failed"Only `indexed` images take part in searches.
↳ errorstring | nullWhy indexing failed.
↳ filenamestring | null
↳ source_urlstring | null
↳ indexed_atdatetime
↳ created_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/images/01k6h3t7v2m9q8x4r5n0c1b2d3 -H "Authorization: Bearer $KEY"
delete/collections/{collection}/images/{image}

Delete an image sdk: deleteImage()

Removes the image from the index and from storage.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
imagepath · requiredstringThe image id (a ULID, e.g. 01k6h3t7v2m9q8x4r5n0c1b2d3).

Responses

204Deleted.

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X DELETE https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/images/01k6h3t7v2m9q8x4r5n0c1b2d3 -H "Authorization: Bearer $KEY"
post/collections/{collection}/imports

Import products and images from CSV sdk: importCsv()

Bulk-create products and their images from CSV rows with a header: image_url, product_id, and optionally product_name and group_id. Unknown product ids are created, known ones get the images added. Up to 10,000 rows per request. Send the CSV as a multipart csv file or as a rows string.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Request body · multipart/form-data

csv *file

Request body · application/json

rows *string
{
  "rows": "image_url,product_id,product_name,group_id\nhttps://cdn.example.com/a.jpg,sku-100,Red runner,runner"
}

Responses

202Accepted.

imported_imagesinteger
runobject
↳ idstringA ULID.
↳ type"import" | "reindex" | "retry"
↳ status"queued" | "running" | "completed" | "completed_with_errors" | "failed" | "cancelled"
↳ totalinteger
↳ processedinteger
↳ indexedinteger
↳ failedinteger
↳ percentinteger
↳ replaces_indexbooleanTrue for a full re-index that swaps in a new index when it completes.
↳ created_atdatetime
↳ started_atdatetime
↳ finished_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

402A plan limit was reached. `quota` names the limit; upgrade the plan or free up capacity.

messagestring
quotaobject
↳ metric"searches_per_month" | "images" | "collections"
↳ limitinteger
↳ usedinteger
{
  "message": "The monthly search quota of your plan is used up. Upgrade the plan or wait for the next month.",
  "quota": {
    "metric": "searches_per_month",
    "limit": 1000,
    "used": 1000
  }
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X POST https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/imports -H "Authorization: Bearer $KEY" -F "[email protected]"

Indexing runs

Background work on the index. Imports add new images to the live index; a full re-index builds a fresh index and swaps it in when done.

get/collections/{collection}/runs

List indexing runs sdk: listRuns()

The 50 most recent runs, newest first.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Responses

200The runs.

dataobject[]
↳ idstringA ULID.
↳ type"import" | "reindex" | "retry"
↳ status"queued" | "running" | "completed" | "completed_with_errors" | "failed" | "cancelled"
↳ totalinteger
↳ processedinteger
↳ indexedinteger
↳ failedinteger
↳ percentinteger
↳ replaces_indexbooleanTrue for a full re-index that swaps in a new index when it completes.
↳ created_atdatetime
↳ started_atdatetime
↳ finished_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/runs -H "Authorization: Bearer $KEY"
post/collections/{collection}/runs

Start a re-index or retry sdk: startRun()

retry re-processes failed and pending images into the live index. reindex embeds every image into a fresh index that replaces the live one on completion (needed after group changes or a model upgrade); search keeps working meanwhile. A re-index cannot start while another run is active.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Request body · application/json

type *"reindex" | "retry"
{
  "type": "retry"
}

Responses

200Nothing to do: there were no images to process.

messagestring
datanull

202The started run.

dataobject
↳ idstringA ULID.
↳ type"import" | "reindex" | "retry"
↳ status"queued" | "running" | "completed" | "completed_with_errors" | "failed" | "cancelled"
↳ totalinteger
↳ processedinteger
↳ indexedinteger
↳ failedinteger
↳ percentinteger
↳ replaces_indexbooleanTrue for a full re-index that swaps in a new index when it completes.
↳ created_atdatetime
↳ started_atdatetime
↳ finished_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X POST https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/runs -H "Authorization: Bearer $KEY" -H "Content-Type: application/json" -d '{"type":"retry"}'
get/collections/{collection}/runs/{run}

Get a run sdk: getRun()

Poll this to follow progress. processed counts indexed plus failed images; the run is done when finished_at is set.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
runpath · requiredstringThe run id (a ULID, e.g. 01k6h3t7v2m9q8x4r5n0c1b2d3).

Responses

200The run.

dataobject
↳ idstringA ULID.
↳ type"import" | "reindex" | "retry"
↳ status"queued" | "running" | "completed" | "completed_with_errors" | "failed" | "cancelled"
↳ totalinteger
↳ processedinteger
↳ indexedinteger
↳ failedinteger
↳ percentinteger
↳ replaces_indexbooleanTrue for a full re-index that swaps in a new index when it completes.
↳ created_atdatetime
↳ started_atdatetime
↳ finished_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/runs/01k6h3s9w4c2e7f5g8j1k3m6n9 -H "Authorization: Bearer $KEY"
delete/collections/{collection}/runs/{run}

Cancel a run sdk: cancelRun()

Stops an active run. Images that were already processed keep their state; the rest go back to how they were. Cancelling a finished run is a no-op.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).
runpath · requiredstringThe run id (a ULID, e.g. 01k6h3t7v2m9q8x4r5n0c1b2d3).

Responses

200The run after cancelling.

dataobject
↳ idstringA ULID.
↳ type"import" | "reindex" | "retry"
↳ status"queued" | "running" | "completed" | "completed_with_errors" | "failed" | "cancelled"
↳ totalinteger
↳ processedinteger
↳ indexedinteger
↳ failedinteger
↳ percentinteger
↳ replaces_indexbooleanTrue for a full re-index that swaps in a new index when it completes.
↳ created_atdatetime
↳ started_atdatetime
↳ finished_atdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X DELETE https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/runs/01k6h3s9w4c2e7f5g8j1k3m6n9 -H "Authorization: Bearer $KEY"

Search

Match a photo against a collection.

Webhooks

Instead of polling runs, set a webhook_url on a collection and we POST an indexing_run.finished event to it when any import, retry or re-index completes. Every request is signed; see the Webhooks guide.

get/collections/{collection}/webhook-deliveries

List webhook deliveries sdk: listWebhookDeliveries()

The 50 most recent events sent to the collection's webhook, newest first, with the outcome of the attempts so far. Use it to debug an endpoint that is not receiving events.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Responses

200The deliveries.

dataobject[]
↳ idstringA ULID.
↳ event"indexing_run.finished" | "webhook.test"
↳ status"pending" | "delivered" | "failed"`failed` after all retries (about a day) were exhausted.
↳ attemptsinteger
↳ lastStatusinteger | nullHTTP status of the last attempt.
↳ lastErrorstring | null
↳ createdAtdatetime
↳ deliveredAtdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/webhook-deliveries -H "Authorization: Bearer $KEY"
post/collections/{collection}/webhook-test

Send a test event sdk: sendTestWebhook()

Queues a webhook.test event to the collection's webhook URL so you can verify your endpoint and signature check without waiting for a real run. Requires a configured webhook_url.

Parameters

collectionpath · requiredstringThe collection id, a ULID (e.g. 01k6h2m4q8r3t5v7x9z1b3d5f7).

Responses

202The queued delivery; poll the deliveries list for the outcome.

dataobject
↳ idstringA ULID.
↳ event"indexing_run.finished" | "webhook.test"
↳ status"pending" | "delivered" | "failed"`failed` after all retries (about a day) were exhausted.
↳ attemptsinteger
↳ lastStatusinteger | nullHTTP status of the last attempt.
↳ lastErrorstring | null
↳ createdAtdatetime
↳ deliveredAtdatetime

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

404No such resource in this team.

messagestring
{
  "message": "No query results for model [App\\Models\\Collection]."
}

422The request was invalid; `errors` lists the problems per field.

messagestring
errorsobject
{
  "message": "The id field is required.",
  "errors": {
    "id": [
      "The id field is required."
    ]
  }
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl -X POST https://snapdex.dev/api/v1/collections/01k6h2m4q8r3t5v7x9z1b3d5f7/webhook-test -H "Authorization: Bearer $KEY"

Usage

Your plan, its quotas and how much of them you have used.

get/usage

Get plan and usage sdk: getUsage()

The plan of the team that owns the key, its quotas with what is used, when the monthly search counter resets, and daily counts of requests, searches and indexed images for the last 30 days.

Responses

200The usage.

dataobject
↳ planobject
↳ quotasobject
↳ periodobject
↳ dailyobject[]The last 30 days, oldest first.
{
  "data": {
    "plan": {
      "id": "free",
      "name": "Free",
      "requests_per_minute": 120
    },
    "quotas": {
      "searches_per_month": {
        "limit": 1000,
        "used": 412,
        "remaining": 588,
        "hard_limit": 1000
      },
      "images": {
        "limit": 5000,
        "used": 1290,
        "remaining": 3710,
        "hard_limit": 5000
      },
      "collections": {
        "limit": 3,
        "used": 1,
        "remaining": 2,
        "hard_limit": 3
      }
    },
    "period": {
      "start": "2026-09-01T00:00:00Z",
      "resets_at": "2026-10-01T00:00:00Z"
    },
    "daily": [
      {
        "date": "2026-09-24",
        "requests": 57,
        "searches": 40,
        "images_indexed": 12
      }
    ]
  }
}

401Missing, invalid or revoked API key.

messagestring
{
  "message": "Unauthenticated. Pass a valid API key as a Bearer token in the Authorization header."
}

429Too many requests for your plan's per-minute limit. Wait the number of seconds in `Retry-After`, then try again.

messagestring
{
  "message": "Too Many Attempts."
}

Example

curl https://snapdex.dev/api/v1/usage -H "Authorization: Bearer $KEY"