Developer guide

Similarity AI API

Index your catalog images, then find visually similar images in your tenant’s catalog. Start with the requests below or try the live demo.

Quick start

  1. Get your account API key.
  2. Index your public image URLs.
  3. Search by image URL or indexed image ID.

API base URL

https://similarity-api.newbytesolutions.com

Authentication

Choose direct API-key access from your server or a short-lived HMAC grant for a request sent by your browser. Both determine your tenant from its active key; never send a tenant_id field.

Direct API key

Send your tenant API key from your trusted server. Keep long-lived API keys and signing secrets on your server.

Authorization: Bearer YOUR_TENANT_API_KEY

HMAC grant

Your server checks the user’s permission, then signs a grant using its tenant signing secret and key ID. The browser receives only the grant, the exact request body and its idempotency key. A grant expires within five minutes and authorizes the signed HTTP method, exact path and query, body bytes and idempotency key.

Authorization: HMAC YOUR_SIGNED_GRANT
Content-Type: application/json
Idempotency-Key: THE_KEY_SIGNED_BY_YOUR_SERVER

Sign on your server with Node.js:

import { createHash, createHmac, randomUUID } from 'node:crypto';

const path = '/v1/images/match';
const idempotencyKey = randomUUID();
const body = JSON.stringify({
  images: [{ query_id: 'query-001', image_url: approvedImageUrl }],
  top_k: 5,
  filters: [],
});
const expiresAt = Date.now() + 5 * 60_000;
const digest = createHash('sha256').update(body).digest('base64url');
const prefix = ['grant', tenantSigningKeyId, expiresAt, digest].join('.');
const message = [prefix, 'POST', path, idempotencyKey].join('\n');
const signature = createHmac('sha256', tenantSigningSecret)
  .update(message).digest('base64url');
const authorization = prefix + '.' + signature;

// Authorize the caller first, then return with Cache-Control: no-store.
return { path, body, authorization, idempotencyKey };

Send the signed request from your browser:

await fetch(serviceUrl + grant.path, {
  method: 'POST',
  headers: {
    Authorization: 'HMAC ' + grant.authorization,
    'Content-Type': 'application/json',
    'Idempotency-Key': grant.idempotencyKey,
  },
  body: grant.body,
});

Send the body string exactly as returned; parsing and re-serializing it can invalidate the signature. GET grants sign an empty body and the exact path and query, with an empty idempotency-key line.

Retries and errors

Use a fresh UUID as the Idempotency-Key for each new index, match, bulk submission, or update. When retrying, reuse the same key and request. Keys stay reserved while work is in progress and for 24 hours after completion.

Index retries return the saved outcomes. Retrying a completed search returns result_not_retained without running or charging for another search.

For PATCH requests, append the exact quoted If-Match header as a fifth line of the HMAC signing message and include an Idempotency-Key. This binds the version check to the signature. DELETE signs its exact body and URL and requires no idempotency key.

Metadata and attributes

metadata stores descriptive information returned with a match, such as a title, listing URL or nested listing details. attributes stores structured fields you want to filter and update independently, such as bedrooms, property type and availability.

{
  "metadata": {
    "title": "3-bedroom house in DHA",
    "listing_url": "https://example.com/property/123",
    "city": "Lahore"
  },
  "attributes": {
    "bedrooms": 3,
    "property_type": "house",
    "available": true
  }
}

Both support filtering on top-level scalar fields. Metadata may contain nested JSON, but nested objects and arrays cannot be used as filter values. Attributes must be flat: each value is a string (up to 2,048 characters), finite number or boolean. Keys start with a letter and contain up to 64 letters, digits or underscores; constructor and prototype are reserved.

The update endpoint replaces metadata, attributes or both in one call without fetching or embedding the image again. It cannot change the image URL or the external ID. Neither metadata nor attributes changes the visual similarity score; they describe records and narrow eligible results. These fields are supplied by you, not inferred from the image.

Index images

POST/v1/images/index$0.01 per newly indexed image

Send 1–20 images per request. Each item needs an external_id and a public image_url. Optional metadata is a JSON object up to 16 KiB. Optional attributes is a flat map of strings, numbers and booleans, with at most 32 fields and 16 KiB total.

Give each image a unique external_id within your account. If a listing has multiple photos, use IDs such as listing-123-photo-1 and listing-123-photo-2.

curl 'https://similarity-api.newbytesolutions.com/v1/images/index' \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: REPLACE_WITH_A_FRESH_UUID' \
  --data-raw '{
  "images": [
    {
      "external_id": "product-001",
      "image_url": "https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg",
      "metadata": {
        "title": "Golden Retriever puppy"
      },
      "attributes": {}
    }
  ]
}'

Example response:

{
  "request_id": "…",
  "profile_id": "similarity-v1",
  "results": [
    {
      "item_index": 0,
      "external_id": "product-001",
      "status": "indexed",
      "image_id": "…"
    }
  ],
  "summary": {
    "indexed": 1,
    "duplicate": 0,
    "pending": 0,
    "failed": 0
  }
}

Items return indexed, duplicate, pending or failed. Resubmitting an indexed external_id with the exact same URL returns duplicate and the existing image_id. A different URL returns external_id_conflict. Inspect pending records before starting new work.

Bulk indexing

POST/v1/images/bulk$0.005 per newly indexed image

Use bulk indexing to queue 50,000 or more new images in one job.

curl 'https://similarity-api.newbytesolutions.com/v1/images/bulk' \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: REPLACE_WITH_A_FRESH_UUID' \
  --data-raw '{
  "manifest_url": "https://your-public-host.example/catalog.jsonl",
  "manifest_sha256": "REPLACE_WITH_THE_64_CHARACTER_SHA256"
}'

Returns 202 with bulk_job_id, status and status_url. Supply the SHA-256 of the exact uncompressed UTF-8 manifest bytes.

Each physical JSONL line is one image object:

{"external_id":"product-001","image_url":"https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg","metadata":{"title":"Golden Retriever puppy"},"attributes":{}}
{"external_id":"product-002","image_url":"https://your-public-host.example/product-002.jpg","metadata":{"title":"Golden Retriever puppy"},"attributes":{}}

The manifest can contain up to 100,000 lines, with a limit of 100 MiB per file and 64 KiB per line. Blank or malformed lines return item errors.

GET/v1/bulk/{job_id}

Check job progress and counts. A job finishes with completed, rejected, or failed. A completed job can contain failed items.

GET/v1/bulk/{job_id}/items?limit=20

Read item outcomes, including returned image IDs and safe errors. Use the returned next_cursor for subsequent pages; set limit from 1–100.

Get an indexed image

GET/v1/images/{image_id}

curl 'https://similarity-api.newbytesolutions.com/v1/images/YOUR_IMAGE_ID' \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY'

Returns the owned image’s ID, external ID, URL, metadata, attributes, status and profile. Foreign or unknown image IDs return 404. Use this route to inspect pending indexing.

Update images

PATCH/v1/images/YOUR_IMAGE_ID/update

GET/v1/images/YOUR_IMAGE_ID

GET returns attribute_version and a matching quoted ETag, initially "0". Use that value as If-Match when updating metadata or attributes. The shared version advances once per update.

curl 'https://similarity-api.newbytesolutions.com/v1/images/YOUR_IMAGE_ID/update' \
  -X PATCH \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: REPLACE_WITH_A_FRESH_UUID' \
  -H 'If-Match: "0"' \
  --data-raw '{"attributes":{"bedrooms":3,"property_type":"house","available":true},"metadata":{"title":"adv"}}'

Send attributes, metadata or both. Each supplied object is replaced completely; omitted fields stay unchanged. An empty object clears that field. Metadata accepts nested JSON up to 16 KiB and 64 nesting levels. 200 means updated. 202 means pending. Poll GET for the published attributes and version. Missing If-Match returns 428, a stale version returns 412, and an overlapping mutation returns 409. Retry with the same key, body and version; completed results replay for 24 hours. This operation is unbilled.

Delete images

DELETE/v1/images/YOUR_IMAGE_ID

curl 'https://similarity-api.newbytesolutions.com/v1/images/YOUR_IMAGE_ID' \
  -X DELETE \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY'

DELETE returns 204 when the image is removed. You can safely retry after a timeout or retryable error. Deletion is free. Indexing the image again counts as a new index operation.

Delete up to 20 images at once

curl 'https://similarity-api.newbytesolutions.com/v1/images' \
  -X DELETE \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY' \
  -H 'Content-Type: application/json' \
  --data-raw '{"image_ids":["IMAGE_ID_1","IMAGE_ID_2"]}'

Send 1–20 distinct IDs. The response reports success or failure for each image. Retry with the same IDs after a timeout. DELETE needs no idempotency key.

Find matches

POST/v1/images/match$0.002 per successful query image

You can search with up to 50 images in one request. Give each query a unique query_id and supply either a public image_url or an indexed image_id.

Use top_k to limit each result list to 1–50 items (default 10). Add filters to search only part of your catalog. Successful searches count toward usage even when they return no matches.

curl 'https://similarity-api.newbytesolutions.com/v1/images/match' \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY' \
  -H 'Content-Type: application/json' \
  -H 'Idempotency-Key: REPLACE_WITH_A_FRESH_UUID' \
  --data-raw '{
  "images": [
    {
      "query_id": "query-001",
      "image_url": "https://upload.wikimedia.org/wikipedia/commons/6/6e/Golde33443.jpg"
    }
  ],
  "top_k": 5,
  "filters": []
}'

To search using an indexed image, replace the query item with:

{
  "query_id": "query-001",
  "image_id": "YOUR_RETURNED_IMAGE_ID"
}

When you search by indexed image ID, that image is excluded from its own results. Each successful query returns up to top_k candidates in each list:

  • matches contains confirmed near-exact copies (near_exact) and verified edited copies (edited_copy).
  • needs_verification contains candidates whose evidence was inconclusive or couldn't be checked.

Results include the catalog image ID, external ID, URL, metadata, and match_reason. The API returns results in ranked order. The similarity score measures visual resemblance. Use match_reason to see why the API returned a match. The response also summarizes successful, failed, and billable queries.

Filtering search results

Filters restrict which stored catalog images are eligible for matching. All conditions must pass (AND). Fields use metadata.<key> or attributes.<key>; a bare key means attributes. Missing fields or values of the wrong type do not match. String equality is case-sensitive.

{
  "images": [
    {
      "query_id": "q1",
      "image_url": "https://example.com/query.jpg"
    }
  ],
  "filters": [
    {
      "field": "metadata.city",
      "op": "eq",
      "value": "Lahore"
    },
    {
      "field": "attributes.bedrooms",
      "op": "gte",
      "value": 3
    },
    {
      "field": "attributes.property_type",
      "op": "in",
      "value": [
        "house",
        "flat"
      ]
    }
  ],
  "top_k": 5
}

This searches houses or flats in Lahore with at least three bedrooms, using the values you stored in the catalog.

OperatorMeaningValues
eqEqualsString, number or boolean
inEquals one listed value1–50 strings or 1–50 numbers; no mixed types
gte / lteGreater/less than or equalNumbers or UTC timestamps such as 2026-10-24T00:00:00.000Z

Use up to 10 conditions. Repeat a field with gte and lte for a range. Raw database filters, nested paths, OR and NOT are not accepted. Search always stays inside the authenticated account.

Pricing

Usage rates are in USD. Demo accounts record usage with zero billable amounts.

OperationRate per image
Standard indexing$0.01
Bulk indexing (50,000+ new images per job)$0.005
Successful similarity search$0.002

Failed or duplicate indexing adds no charge. Successful searches using image_url or image_id cost $0.002 per query image, even when they return zero matches.

The $299 monthly minimum is applied against usage. Pilot onboarding is $500, with $1,000 due if you continue after the pilot. Custom integration is $3,000. See the full pricing page for program details.

Monthly usage reports

GET/v1/usage/monthly/2026-10

Use YYYY-MM to get your account's usage for a UTC calendar month.

curl 'https://similarity-api.newbytesolutions.com/v1/usage/monthly/2026-10' \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY'

The report shows actual and billable counts, USD amounts by operation, and the report timestamp. Refreshing it creates no charges. Use it to track usage before invoicing.

Check readiness

GET/full-health

curl 'https://similarity-api.newbytesolutions.com/full-health' \
  -H 'Authorization: Bearer YOUR_TENANT_API_KEY'

Returns ready and checks for the database, search and inference. All must be ready for indexing/search. Missing provider/model readiness returns 503. Invalid tenant keys return 401.

GET/health

Public process liveness only. A 200 here does not mean the model or cloud providers are ready.

Errors & limits

Processed batches return 200 with per-item outcomes, including mixed successes and failures. Inspect every item. Request-level failures use this safe envelope:

{
  "error": {
    "code": "dependency_unavailable",
    "message": "dependency unavailable",
    "retryable": true
  },
  "request_id": "…"
}
StatusMeaning
401Missing or invalid tenant key
404Unknown or inaccessible image
409Idempotency conflict, operation running, or result not retained
413JSON body larger than 1 MiB
422Invalid envelope or idempotency key
429Capacity full. Follow the Retry-After header.
503Provider or model unavailable
504Processing deadline exceeded

JPEG, PNG and single-frame WebP only; at least 224 pixels per side, at most 4096 per side, 12 million pixels and 20 MiB. URLs must be public HTTP(S) on standard ports. Local/private destinations and unsafe redirects are rejected.

Example photograph: Golden Trvs Gol twister / Wikimedia Commons, CC BY-SA 3.0, unchanged.