---
title: "Search by photo"
description: "POST /search: Returns products that look like a photo and match the filters you send."
url: https://docs.tryflume.ai/api/search
updated: 2026-11-11
---

# Search by photo

> POST /search: Returns products that look like a photo and match the filters you send.

Returns products that look like a photo and match the filters you send.

```http
POST https://sandbox-api.tryflume.ai/partner/v1/search
```

- Send `multipart/form-data` with two parts: `image` and `payload`.
- `image`: a JPEG, PNG or WebP, up to 6 MB.
- `payload`: JSON. `category` is required.
- The response has two lists: `strong_matches`, then `other_similar_options`.
- Keep `search_session_uuid`. Quotes, saves and deep links for these products need it.
- For the next page, send the same photo and payload with `page: 2`. Each call counts as a new search and returns a new `search_session_uuid`.
- Send `spec_unit_price` to get `saving_vs_spec` on each result.
- Daily limit: 1,000 searches per partner.

## Request

### Headers

| Header | Required | Description |
|---|---|---|
| `X-Flume-Partner-Key` | Yes | Your partner key. |
| `X-Flume-User-Token` | Yes | The access token of the user you call for. |

### Body (`multipart/form-data`)

| Field | Type | Required | Description |
|---|---|---|---|
| `image` | file | Yes |  |
| `payload` | object | Yes | The `payload` part of a search, sent beside the photo. |
| `payload.project` | object or null | No | The project to file the search under. |
| `payload.project.external_ref` | string or null | No | Your id for the project. Max 128 characters. |
| `payload.project.name` | string or null | No | The project's name. Max 255 characters. |
| `payload.project.project_type` | enum or null | No | The kind of project. One of: `RESIDENTIAL`, `MULTIFAMILY`, `COMMERCIAL`, `HOSPITALITY`, `HEALTHCARE`, `EDUCATION`, `RETAIL`, `OTHER`. |
| `payload.project.city` | string or null | No | The project's city. Max 128 characters. |
| `payload.project.state` | string or null | No | The US state, as 2 capital letters. 2 capital letters. |
| `payload.project.destination_zip` | string or null | No | The ZIP code the material is delivered to. 5 digits. |
| `payload.project.estimated_area_sqft` | integer or null | No | The project's total area, in square feet. 1 to 10,000,000. |
| `payload.project.unit_count` | integer or null | No | The number of homes, units or rooms. 1 to 1,000,000. |
| `payload.project.target_install_date` | string (date) or null | No | When installation is planned. |
| `payload.category` | enum | Yes | What to search for: `TILE` or `FLOORING` (vinyl flooring). Send the filters for that category only. |
| `payload.tile_filters` | object or null | No | Filters for a `TILE` search. |
| `payload.tile_filters.material` | array of enums | No | Match any of these materials. Any of: `PORCELAIN`, `CERAMIC`. |
| `payload.tile_filters.finish` | array of enums | No | Match any of these finishes. Similar finishes are included: `MATTE` also finds `NATURAL`, `HONED` and `ANTI_SLIP`. `GET /filters` lists what each finish also finds. Any of: `MATTE`, `GLOSSY_POLISHED`, `SATIN`, `HONED`, `LAPPATO_SEMI_POLISHED`, `TEXTURED`, `NATURAL`, `BUSH_HAMMERED`, `ANTI_SLIP`, `CRACKLE`. |
| `payload.tile_filters.finish_exclude` | array of enums | No | Leave these finishes out. Any of: `MATTE`, `GLOSSY_POLISHED`, `SATIN`, `HONED`, `LAPPATO_SEMI_POLISHED`, `TEXTURED`, `NATURAL`, `BUSH_HAMMERED`, `ANTI_SLIP`, `CRACKLE`. |
| `payload.tile_filters.application_area` | array of enums | No | Where the tile is used. A product must suit every area listed. Any of: `INTERIOR_WALL_BACKSPLASH`, `SHOWER_WALL`, `STEAM_SHOWER_WET_ROOM_WALL`, `EXTERIOR_WALL_FACADE`, `INTERIOR_DRY_FLOOR`, `INTERIOR_WET_FLOOR`, `SHOWER_FLOOR`, `EXTERIOR_FLOOR_PATIO`, `RAMP_STAIR`, `POOL_DECK_WET_SURROUND`, `POOL_SUBMERGED`, `COUNTERTOP`. |
| `payload.tile_filters.size` | object or null | No | The tile size to match. |
| `payload.tile_filters.size.width` | number or null | No | The width, in `unit`. More than 0. Max 10,000. |
| `payload.tile_filters.size.length` | number or null | No | The length, in `unit`. More than 0. Max 10,000. |
| `payload.tile_filters.size.unit` | enum | No | The unit of `width` and `length`. One of: `in`, `cm`, `mm`. Default `"in"`. |
| `payload.tile_filters.thickness_mm` | number or null | No | The minimum thickness, in millimetres. Products up to 2 mm thinner still match. More than 0. Max 500. |
| `payload.tile_filters.glazed` | boolean or null | No | `true`: glazed only. `false`: unglazed only. |
| `payload.tile_filters.mosaic` | boolean | No | `false` leaves mosaics out. `true` returns mosaics only. Default `false`. |
| `payload.flooring_filters` | object or null | No | Filters for a `FLOORING` search. |
| `payload.flooring_filters.size` | object or null | No | The plank size to match. |
| `payload.flooring_filters.size.width` | number or null | No | The width, in `unit`. More than 0. Max 10,000. |
| `payload.flooring_filters.size.length` | number or null | No | The length, in `unit`. More than 0. Max 10,000. |
| `payload.flooring_filters.size.unit` | enum | No | The unit of `width` and `length`. One of: `in`, `cm`, `mm`. Default `"in"`. |
| `payload.flooring_filters.thickness_mm` | number or null | No | The minimum thickness, in millimetres. Products up to 20% thinner still match. More than 0. Max 50. |
| `payload.flooring_filters.thickness_mm_max` | number or null | No | The maximum thickness, in millimetres. More than 0. Max 50. |
| `payload.flooring_filters.wear_layer_mil` | array of integer enums | No | Match any of these wear layers, in mil. `28` means 28 mil and thicker. Any of: `8`, `12`, `20`, `22`, `28`. |
| `payload.flooring_filters.installation_method` | array of enums | No | Match any of these installation methods. Any of: `CLICK_LOCK`, `GLUE_DOWN`, `LOOSE_LAY`. |
| `payload.flooring_filters.finish` | array of enums | No | Match any of these finishes. Any of: `MATTE`, `LOW_GLOSS`. |
| `payload.flooring_filters.eir` | boolean or null | No | `true`: embossed in register (EIR) only. `false`: not EIR. |
| `payload.flooring_filters.rigid_core` | array of enums | No | Match any of these core types. Any of: `SPC`, `WPC`. |
| `payload.flooring_filters.attached_pad` | boolean or null | No | `true`: with an attached pad. `false`: without one. |
| `payload.page` | integer | No | The page to return. A search finds at most 50 products. 1 to 50. Default `1`. |
| `payload.page_size` | integer | No | Products per page. Use the same value for every page of one search. 1 to 24. Default `24`. |
| `payload.crop_box` | object or null | No | The part of the photo to search on. |
| `payload.crop_box.x` | number | Yes | The left edge, from 0 to 1. 0 to 1. |
| `payload.crop_box.y` | number | Yes | The top edge, from 0 to 1. 0 to 1. |
| `payload.crop_box.w` | number | Yes | The width, as a fraction of the photo's width. More than 0. Max 1. |
| `payload.crop_box.h` | number | Yes | The height, as a fraction of the photo's height. More than 0. Max 1. |
| `payload.spec_unit_price` | object or null | No | The unit price of the product the user has now. When sent, each result carries `saving_vs_spec`. Only USD per `SF` gives a saving. |
| `payload.spec_unit_price.amount` | decimal string | Yes | The price, as a decimal. 0 to 1,000,000. |
| `payload.spec_unit_price.currency` | string | Yes | The 3-letter currency code. 3 characters. |
| `payload.spec_unit_price.unit` | string or null | No | The unit the price is for. It matches the unit of the quantity. Max 16 characters. |

### Sample

**curl**

```sh
curl -X POST "https://sandbox-api.tryflume.ai/partner/v1/search" \
  -H "X-Flume-Partner-Key: $FLUME_PARTNER_KEY" \
  -H "X-Flume-User-Token: $FLUME_USER_TOKEN" \
  -F "image=@photo.jpg" \
  -F 'payload={"category":"TILE","tile_filters":{"material":["PORCELAIN"],"finish":["MATTE"],"size":{"width":24,"length":48,"unit":"in"}},"project":{"external_ref":"proj_7781","name":"Harbor Lofts - L2","destination_zip":"22102"}}'
```

**Python**

```python
import os
import json

import requests

headers = {
    "X-Flume-Partner-Key": os.environ["FLUME_PARTNER_KEY"],
    "X-Flume-User-Token": os.environ["FLUME_USER_TOKEN"],
}
payload = {
    "category": "TILE",
    "tile_filters": {
        "material": ["PORCELAIN"],
        "finish": ["MATTE"],
        "size": {
            "width": 24,
            "length": 48,
            "unit": "in",
        },
    },
    "project": {
        "external_ref": "proj_7781",
        "name": "Harbor Lofts - L2",
        "destination_zip": "22102",
    },
}

with open("photo.jpg", "rb") as image:
    response = requests.post(
        "https://sandbox-api.tryflume.ai/partner/v1/search",
        headers=headers,
        files={"image": image},
        data={"payload": json.dumps(payload)},
        timeout=30,
    )
response.raise_for_status()
print(response.json())
```

**Node.js**

```js
import { readFile } from "node:fs/promises";

const form = new FormData();
form.append("image", new Blob([await readFile("photo.jpg")]), "photo.jpg");
form.append("payload", JSON.stringify({
  "category": "TILE",
  "tile_filters": {
    "material": [
      "PORCELAIN"
    ],
    "finish": [
      "MATTE"
    ],
    "size": {
      "width": 24,
      "length": 48,
      "unit": "in"
    }
  },
  "project": {
    "external_ref": "proj_7781",
    "name": "Harbor Lofts - L2",
    "destination_zip": "22102"
  }
}));

const response = await fetch("https://sandbox-api.tryflume.ai/partner/v1/search", {
  method: "POST",
  headers: {
    "X-Flume-Partner-Key": process.env.FLUME_PARTNER_KEY,
    "X-Flume-User-Token": process.env.FLUME_USER_TOKEN,
  },
  body: form,
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
console.log(await response.json());
```

## Returns (200)

Every field is always present. A field with no value is `null`.

| Field | Type | Description |
|---|---|---|
| `search_session_uuid` | string (UUID) | The search's id. Send it when you request a quote, save an item or create a deep link for a product on this page. Every call returns a new one, page 2 included. |
| `total` | integer | Products across all pages, at most 50. |
| `strong_match_count` | integer | How many of `total` are strong matches. |
| `other_similar_count` | integer | How many of `total` are other similar options. |
| `page` | integer | The page returned. |
| `page_size` | integer | Products per page. |
| `has_more` | boolean | `true` when a later page has more products. |
| `strong_matches` | array of objects | Products on this page that meet every filter and look closest to the photo, best first. |
| `strong_matches[].sku_uuid` | string (UUID) | The product's id. |
| `strong_matches[].name` | string | The product's name, written by Flume from its specification. |
| `strong_matches[].image_url` | string or null | The main product photo. |
| `strong_matches[].more_image_urls` | array of strings | Other product photos. |
| `strong_matches[].size` | string or null | The size, as text. Tile only. |
| `strong_matches[].thickness` | string or null | The thickness, as text. Tile only. |
| `strong_matches[].material` | string or null | The material. Tile only. |
| `strong_matches[].as_low_as_per_sqft` | string or null | The lowest delivered price per square foot, before tax. An instant quote for a real quantity and ZIP code can be higher. `null` when `price_on_request` is `true`. |
| `strong_matches[].price_on_request` | boolean | `true`: Flume's sales team prices this product, and there is no instant quote. |
| `strong_matches[].lead_times` | array of objects | The ways the product ships, with delivery times. |
| `strong_matches[].lead_times[].source` | enum | `US_WAREHOUSE` or `OVERSEAS_DIRECT`. |
| `strong_matches[].lead_times[].lead_time_min_days` | integer | The shortest delivery time, in days. |
| `strong_matches[].lead_times[].lead_time_max_days` | integer | The longest delivery time, in days. |
| `strong_matches[].suitable_application_areas` | array of objects | Every area where the product can be used: floors first, then walls, then the rest. Empty for vinyl flooring, or when Flume knows none. |
| `strong_matches[].suitable_application_areas[].code` | string | The area's code, as `GET /filters` lists it. |
| `strong_matches[].suitable_application_areas[].label` | string | The area's name, to show. |
| `strong_matches[].saving_vs_spec` | object or null | The saving against `spec_unit_price`, when you sent one. `null` when the saving is under 5%, or when the product costs the same or more. |
| `strong_matches[].saving_vs_spec.percent` | integer | The saving, in percent. |
| `strong_matches[].saving_vs_spec.label` | string | The saving as text, to show. |
| `other_similar_options` | array of objects | Products on this page that look close to the photo, with some filters relaxed. They come after every strong match. |
| `other_similar_options[].sku_uuid` | string (UUID) | The product's id. |
| `other_similar_options[].name` | string | The product's name, written by Flume from its specification. |
| `other_similar_options[].image_url` | string or null | The main product photo. |
| `other_similar_options[].more_image_urls` | array of strings | Other product photos. |
| `other_similar_options[].size` | string or null | The size, as text. Tile only. |
| `other_similar_options[].thickness` | string or null | The thickness, as text. Tile only. |
| `other_similar_options[].material` | string or null | The material. Tile only. |
| `other_similar_options[].as_low_as_per_sqft` | string or null | The lowest delivered price per square foot, before tax. An instant quote for a real quantity and ZIP code can be higher. `null` when `price_on_request` is `true`. |
| `other_similar_options[].price_on_request` | boolean | `true`: Flume's sales team prices this product, and there is no instant quote. |
| `other_similar_options[].lead_times` | array of objects | The ways the product ships, with delivery times. |
| `other_similar_options[].lead_times[].source` | enum | `US_WAREHOUSE` or `OVERSEAS_DIRECT`. |
| `other_similar_options[].lead_times[].lead_time_min_days` | integer | The shortest delivery time, in days. |
| `other_similar_options[].lead_times[].lead_time_max_days` | integer | The longest delivery time, in days. |
| `other_similar_options[].suitable_application_areas` | array of objects | Every area where the product can be used: floors first, then walls, then the rest. Empty for vinyl flooring, or when Flume knows none. |
| `other_similar_options[].suitable_application_areas[].code` | string | The area's code, as `GET /filters` lists it. |
| `other_similar_options[].suitable_application_areas[].label` | string | The area's name, to show. |
| `other_similar_options[].saving_vs_spec` | object or null | The saving against `spec_unit_price`, when you sent one. `null` when the saving is under 5%, or when the product costs the same or more. |
| `other_similar_options[].saving_vs_spec.percent` | integer | The saving, in percent. |
| `other_similar_options[].saving_vs_spec.label` | string | The saving as text, to show. |
| `project` | object or null | What happened to the `project` you sent. `null` when you sent none. |
| `project.external_ref` | string or null | Your id for the project. |
| `project.status` | enum | `FILED`: this search saved the project in Flume. `EXISTING`: an earlier search saved it. `NOT_FILED`: not saved, and `reason` says why. |
| `project.reason` | enum or null | With `NOT_FILED`: `MISSING_FIELDS` (see `missing`) or `PROJECT_LIMIT_REACHED`. Otherwise `null`. |
| `project.missing` | array of strings | With `MISSING_FIELDS`: the fields to send next time. |
| `project.message` | string or null | A sentence you can show the user. |

**Response (200)**

```json
{
  "search_session_uuid": "5f0c1d2e-8a7b-4c3d-9e1f-2a3b4c5d6e7f",
  "total": 31,
  "strong_match_count": 9,
  "other_similar_count": 22,
  "page": 1,
  "page_size": 24,
  "has_more": true,
  "strong_matches": [
    {
      "sku_uuid": "7f3c2a91-4b0e-4c8d-9a21-1f6b2d5e7c30",
      "name": "Porcelain 24×48″ Matte",
      "image_url": "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-main",
      "more_image_urls": [
        "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-2",
        "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-3"
      ],
      "size": "24×48 in",
      "thickness": "9 mm",
      "material": "Porcelain",
      "as_low_as_per_sqft": "6.40",
      "price_on_request": false,
      "lead_times": [
        {
          "source": "US_WAREHOUSE",
          "lead_time_min_days": 14,
          "lead_time_max_days": 30
        },
        {
          "source": "OVERSEAS_DIRECT",
          "lead_time_min_days": 90,
          "lead_time_max_days": 120
        }
      ],
      "suitable_application_areas": [
        {
          "code": "INTERIOR_DRY_FLOOR",
          "label": "Interior Dry Floor"
        },
        {
          "code": "INTERIOR_WALL_BACKSPLASH",
          "label": "Interior Wall / Backsplash"
        }
      ],
      "saving_vs_spec": null
    }
  ],
  "other_similar_options": [
    {
      "sku_uuid": "0b6f9d34-2e71-4c5a-8f10-3d9a7e2b6c41",
      "name": "Porcelain 24×48″ Natural",
      "image_url": "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-main",
      "more_image_urls": [
        "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-2",
        "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-3"
      ],
      "size": "24×48 in",
      "thickness": "9 mm",
      "material": "Porcelain",
      "as_low_as_per_sqft": "5.90",
      "price_on_request": false,
      "lead_times": [
        {
          "source": "US_WAREHOUSE",
          "lead_time_min_days": 14,
          "lead_time_max_days": 30
        },
        {
          "source": "OVERSEAS_DIRECT",
          "lead_time_min_days": 90,
          "lead_time_max_days": 120
        }
      ],
      "suitable_application_areas": [
        {
          "code": "INTERIOR_DRY_FLOOR",
          "label": "Interior Dry Floor"
        },
        {
          "code": "INTERIOR_WALL_BACKSPLASH",
          "label": "Interior Wall / Backsplash"
        }
      ],
      "saving_vs_spec": null
    }
  ],
  "project": {
    "external_ref": "proj_7781",
    "status": "FILED",
    "reason": null,
    "missing": [],
    "message": null
  }
}
```

## Errors

| Status | Code | Meaning |
|---|---|---|
| 400 | `IMAGE_REQUIRED` | The request has no `image` part, or the part is empty. |
| 401 | `EMBED_TOKEN_INVALID` | You sent an `Authorization` header and no `X-Flume-Partner-Key`. Flume read the call as one from its own iframe. |
| 401 | `PARTNER_KEY_INVALID` | The partner key is missing, unknown, revoked, expired, or from the other environment. On live, also a platform Flume has not enabled yet. |
| 401 | `USER_TOKEN_INVALID` | The user access token is unknown, revoked, another partner's, or replaced by a later verification. A verification in the iframe replaces it too. |
| 403 | `USER_ACCOUNT_CLOSED` | The user's email belongs to a Flume account that Flume has closed. |
| 403 | `USER_NOT_CONNECTED` | No `X-Flume-User-Token` header was sent. |
| 413 | `IMAGE_TOO_LARGE` | The photo is over 6,000,000 bytes. |
| 422 | `IMAGE_UNSUPPORTED` | The file is not a JPEG, PNG or WebP, or it is over 50 megapixels. Flume reads the bytes, not the file name. |
| 422 | `VALIDATION_FAILED` | A field is missing, unknown or has an unusable value. `field` names it as a dotted path. Some refusals carry the detail in `details` and leave `field` empty. |
| 429 | `DAILY_LIMIT_REACHED` | This user's own daily limit of searches or instant quotes is used up. Flume sets this limit per partner account, and it is off by default. `details` holds `kind`, `limit`, `reset_at` and `retry_after_s`. |
| 429 | `RATE_LIMITED` | Today's limit for this call is used up. `Retry-After` is the number of seconds until 00:00 UTC. |
| 500 | `INTERNAL` | Something failed on Flume's side. |
| 503 | `SEARCH_UNAVAILABLE` | Search is briefly unavailable. The call is not counted against your limit. |

Every code, with its fix: [API error codes](https://docs.tryflume.ai/api/errors.md).

Guide: [Find products by photo](https://docs.tryflume.ai/guides/find-products-by-photo.md).
