---
title: "Find products by photo"
description: "Search Flume by photo with POST /search. Send an image and a JSON payload, then read strong matches, similar options and each product's detail."
url: https://docs.tryflume.ai/guides/find-products-by-photo
updated: 2026-11-11
---

# Find products by photo

> Search Flume by photo with POST /search. Send an image and a JSON payload, then read strong matches, similar options and each product's detail.

Send a photo and a category to `POST /search`. Flume returns products that look like the photo, in two lists: strong matches, then other similar options.

**Diagram: Steps 3 to 5: search, product, quote.** Each call carries both headers. The search returns a search_session_uuid that the later calls carry. A quote that is still pricing is polled.

```mermaid
sequenceDiagram
  participant page as Your page
  participant backend as Your backend
  participant flume as Flume servers
  backend->>flume: GET /filters
  page->>backend: photo, filters
  backend->>flume: POST /search
  flume->>backend: results, search_session_uuid
  backend->>flume: GET /products/sku_uuid
  backend->>flume: POST /request-instant-quote
  flume->>backend: READY, or still pricing
  backend->>flume: GET /quotes/quote_uuid (not every time)
  backend->>page: prices
```

1. Your backend calls GET /filters and builds your filter menus from the answer.
2. Your screen sends the photo and the chosen filters to your backend.
3. Your backend calls POST /search with the photo.
4. Flume returns up to 24 results a page and a search_session_uuid.
5. Your backend calls GET /products/{sku_uuid} for the product the user opens.
6. Your backend calls POST /request-instant-quote with the product, a quantity, a ZIP code and the search_session_uuid. It carries an Idempotency-Key.
7. Flume answers within 60 seconds: READY with prices, or FAILED with a reason.
8. Only when the reason is TEMPORARILY_UNAVAILABLE: your backend polls GET /quotes/{quote_uuid} until the quote is READY.
9. Your backend returns the prices to your screen.

## Before you begin

- A partner key and a [user access token](https://docs.tryflume.ai/guides/verify-a-user.md).
- A photo: JPEG, PNG or WebP, up to 6,000,000 bytes.

## 1. Read the filter options

Build your filter menus from this call, not from a hardcoded list. New values then appear without a release.

**curl**

```sh
curl -X GET "https://sandbox-api.tryflume.ai/partner/v1/filters" \
  -H "X-Flume-Partner-Key: $FLUME_PARTNER_KEY"
```

**Python**

```python
import os

import requests

headers = {
    "X-Flume-Partner-Key": os.environ["FLUME_PARTNER_KEY"],
}

response = requests.get(
    "https://sandbox-api.tryflume.ai/partner/v1/filters",
    headers=headers,
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

**Node.js**

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

**Response (200)**

```json
{
  "tile": {
    "material": [
      {
        "value": "PORCELAIN",
        "label": "Porcelain"
      },
      {
        "value": "CERAMIC",
        "label": "Ceramic"
      }
    ],
    "finish": [
      {
        "value": "MATTE",
        "label": "Matte",
        "also_matches": [
          "NATURAL",
          "HONED",
          "ANTI_SLIP"
        ]
      },
      {
        "value": "GLOSSY_POLISHED",
        "label": "Glossy / Polished",
        "also_matches": [
          "LAPPATO_SEMI_POLISHED"
        ]
      }
    ],
    "application_area": [
      {
        "value": "INTERIOR_DRY_FLOOR",
        "label": "Interior Dry Floor",
        "group": "FLOORS"
      },
      {
        "value": "SHOWER_WALL",
        "label": "Shower Wall",
        "group": "WALLS"
      }
    ]
  },
  "flooring": {
    "wear_layer_mil": [
      {
        "value": 20,
        "label": "20 mil"
      },
      {
        "value": 28,
        "label": "28 mil and up"
      }
    ],
    "installation_method": [
      {
        "value": "CLICK_LOCK",
        "label": "Click Lock"
      }
    ],
    "finish": [
      {
        "value": "MATTE",
        "label": "Matte"
      }
    ],
    "rigid_core": [
      {
        "value": "SPC",
        "label": "SPC"
      }
    ]
  }
}
```

- `value` is what you send. `label` is what you show.
- `also_matches` lists the similar tile finishes a finish also finds.

## 2. Search

Send `multipart/form-data` with two parts: `image` and `payload`.

**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());
```

**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
  }
}
```

| `payload` field | Required | Notes |
|---|---|---|
| `category` | Yes | `TILE` or `FLOORING` |
| `tile_filters` | No | Only with `TILE` |
| `flooring_filters` | No | Only with `FLOORING` |
| `project` | No | Files the search under a project in the user's Flume account |
| `page`, `page_size` | No | Defaults 1 and 24 |
| `crop_box` | No | Search on part of the photo: `x`, `y`, `w`, `h` as fractions from 0 to 1 |
| `spec_unit_price` | No | Adds `saving_vs_spec` to each result. USD per `SF` only |

Filter rules:

- A list filter matches any of its values.
- Different filters must all hold.
- A filter you leave out is no constraint.
- The photo ranks the products that pass the filters.

## 3. Read the results

- `strong_matches`: products that meet every filter and look closest to the photo. Best first.
- `other_similar_options`: close to the photo, with some filters relaxed. They start on the page where the strong matches end.
- `total` is at most 50.
- `as_low_as_per_sqft` is the lowest delivered price per square foot, before tax. An instant quote can be higher.
- `price_on_request: true` means there is no instant quote. [Hand off to sales](https://docs.tryflume.ai/support.md).
- Image links expire after about 24 hours, and earlier in the sandbox. Do not store them.

Keep `search_session_uuid`. Quotes, saves and deep links for these products need it.

## 4. Get the next page

Send the same photo and payload with `page: 2`.

> **Each page is a new search:** Every `POST /search` call spends one unit of the daily limit and returns a new `search_session_uuid`. Page 2 included. Use the id from the page that returned the product.

## 5. Read a product

**curl**

```sh
curl -X GET "https://sandbox-api.tryflume.ai/partner/v1/products/7f3c2a91-4b0e-4c8d-9a21-1f6b2d5e7c30" \
  -H "X-Flume-Partner-Key: $FLUME_PARTNER_KEY" \
  -H "X-Flume-User-Token: $FLUME_USER_TOKEN"
```

**Python**

```python
import os

import requests

headers = {
    "X-Flume-Partner-Key": os.environ["FLUME_PARTNER_KEY"],
    "X-Flume-User-Token": os.environ["FLUME_USER_TOKEN"],
}

response = requests.get(
    "https://sandbox-api.tryflume.ai/partner/v1/products/7f3c2a91-4b0e-4c8d-9a21-1f6b2d5e7c30",
    headers=headers,
    timeout=30,
)
response.raise_for_status()
print(response.json())
```

**Node.js**

```js
const response = await fetch("https://sandbox-api.tryflume.ai/partner/v1/products/7f3c2a91-4b0e-4c8d-9a21-1f6b2d5e7c30", {
  method: "GET",
  headers: {
    "X-Flume-Partner-Key": process.env.FLUME_PARTNER_KEY,
    "X-Flume-User-Token": process.env.FLUME_USER_TOKEN,
  },
});
if (!response.ok) throw new Error(`${response.status} ${await response.text()}`);
console.log(await response.json());
```

**Response (200)**

```json
{
  "sku_uuid": "7f3c2a91-4b0e-4c8d-9a21-1f6b2d5e7c30",
  "category": "TILE",
  "name": "Porcelain 24×48″ Matte",
  "as_low_as_per_sqft": "6.40",
  "price_on_request": false,
  "image_urls": [
    "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-main",
    "https://sandbox-api.tryflume.ai/partner/v1/images/example-image-2"
  ],
  "at_a_glance": [
    {
      "label": "Size",
      "value": "24×48″"
    },
    {
      "label": "Thickness",
      "value": "9 mm"
    },
    {
      "label": "Finish",
      "value": "Matte"
    }
  ],
  "specification": [
    {
      "group": "Slip resistance",
      "rows": [
        {
          "label": "DCOF",
          "value": "> 0.42",
          "test_method": "ANSI A326.3"
        }
      ]
    }
  ],
  "shipping_options": [
    {
      "source": "US_WAREHOUSE",
      "lead_time_min_days": 14,
      "lead_time_max_days": 30,
      "ships_from_country": "US",
      "minimum_order_sqft": 1000
    },
    {
      "source": "OVERSEAS_DIRECT",
      "lead_time_min_days": 90,
      "lead_time_max_days": 120,
      "ships_from_country": "ES",
      "minimum_order_sqft": 5000
    }
  ],
  "suitable_application_areas": [
    {
      "code": "INTERIOR_DRY_FLOOR",
      "label": "Interior Dry Floor"
    }
  ]
}
```

- `specification` leaves out any spec the product does not state.
- `shipping_options` lists each way the product ships, its minimum order and its delivery window. The price comes from an [instant quote](https://docs.tryflume.ai/guides/get-an-instant-quote.md).

## File the search under a project

Send `project.external_ref` and `project.name`. Flume saves the project in the user's Flume account. `project.status` in the response says what happened.

| `status` | Meaning |
|---|---|
| `FILED` | This search created the project |
| `EXISTING` | An earlier search created it |
| `NOT_FILED` | Not saved. `reason` is `MISSING_FIELDS` or `PROJECT_LIMIT_REACHED` |

The search runs either way.

## Handle the common errors

| You get | Do this |
|---|---|
| `400` `IMAGE_REQUIRED` | Add the `image` part |
| `413` `IMAGE_TOO_LARGE` | Send a photo under 6,000,000 bytes |
| `422` `IMAGE_UNSUPPORTED` | Send a JPEG, PNG or WebP of at most 50 megapixels |
| `422` `VALIDATION_FAILED` | Fix the field named in `error.field` |
| `429` `RATE_LIMITED` | Wait `Retry-After` seconds |
| `429` `DAILY_LIMIT_REACHED` | This user's own daily limit is used up (only when Flume set one). Tell the user when it resets: `details.reset_at`. Other users are not affected |
| `503` `SEARCH_UNAVAILABLE` | Retry with backoff. The search is not counted |

## See also

- [Search by photo](https://docs.tryflume.ai/api/search.md) in the API reference
- [Get an instant quote](https://docs.tryflume.ai/guides/get-an-instant-quote.md)
