# Product matching

Salons often type their products into your salon app by hand. Product matching links each of those products to a
product in the Beauty Router catalog. You send your products, and Beauty Router returns a match for each one. A product
that is not in the catalog yet gets added to it. When a match is final, it has the Beauty Router product ID.

Matches belong to the company of the logged-in user. Other companies' matches are not visible: a request for one fails
with `not_found`, and lists leave them out. `externalId` is unique within the company.

## Send products

`POST /v1/product-matches` takes up to 500 products per request.

| Field        | Description                                                                         |
| ------------ | ----------------------------------------------------------------------------------- |
| `externalId` | Your ID for the product, up to 128 characters, unique within the company. Required. |
| `name`       | The product name as your user typed it. Required.                                   |
| `barcode`    | The barcode. Optional.                                                              |
| `brand`      | The brand name. Optional.                                                           |
| `size`       | The size, such as `33.8 fl oz`. Optional.                                           |
| `details`    | Anything else that helps identify the product. Optional.                            |
| `images`     | Up to 10 photos of the product, as HTTPS URLs that Beauty Router downloads. Optional. |

```bash
curl https://api.beautyrouter.com/v1/product-matches \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "products": [
      {
        "externalId": "inv-10482",
        "name": "Amberlin bond shampoo liter",
        "barcode": "850012345678",
        "brand": "Amberlin",
        "size": "33.8 fl oz"
      },
      {
        "externalId": "inv-10517",
        "name": "Jadewell 6N",
        "brand": "Jadewell",
        "details": "Hair color tube, dark natural blonde",
        "images": ["https://app.example.com/uploads/inv-10517-front.jpg"]
      }
    ]
  }'
```

The response has one match for each product:

```json
{
  "data": [
    {
      "id": "match-3Pa8sDf2Gh",
      "externalId": "inv-10482",
      "status": "matched",
      "productId": "prod-4Fz9QbX2kL",
      "createdAt": "2026-10-27T09:14:02Z",
      "updatedAt": "2026-10-27T09:14:02Z"
    },
    {
      "id": "match-9Lk4jHg7Fd",
      "externalId": "inv-10517",
      "status": "in_review",
      "createdAt": "2026-10-27T09:14:02Z",
      "updatedAt": "2026-10-27T09:14:02Z"
    }
  ]
}
```

A product with a barcode can come back `matched` right away, as the first one did here.

The same `externalId` twice in one request fails with `validation_failed`.

## Photos

Photos help identify a product, so send them when you have them.

Beauty Router caches the result of analyzing a photo by its URL. The cache is global and permanent: it covers every user
and salon location, and it never expires. So:

- **Give different pictures different URLs.** Never reuse a URL for a totally different picture, even years later, and
  even for another user or salon location. Beauty Router would use the earlier result without analyzing the new picture.
  A similar picture at the same URL is fine.
- **Keep the URL when the picture stays the same.** Beauty Router then uses the earlier result, which is much faster.
  This helps when many products or salon locations share a picture, for example from your own image catalog.

## Send products again

Beauty Router caches its results, so you can send a product again at any time without worrying about a new recognition.
A repeated request also returns much faster:

- **By `externalId`.** Sending the same `externalId` again for the same company returns the existing match, even if other
  fields changed. This result is cached forever. To match a changed product again, send it with a new `externalId`.
- **By product data.** Beauty Router also caches by all the recognition fields, which is every field except `externalId`.
  When all of them are the same, Beauty Router does not recognize the product again.

Several of your products can match the same catalog product. Send each of them.

## Statuses

| Status        | Meaning                                                                               |
| ------------- | ------------------------------------------------------------------------------------- |
| `in_review`   | Beauty Router is matching the product.                                                |
| `needs_reply` | Beauty Router needs more information. `question` says what it needs.                  |
| `matched`     | The match is final. `productId` is the Beauty Router product ID.                      |

A review ends in one of two ways: `matched` with the product ID, or `needs_reply` with a question. A match never ends as
"not found". When the product is not in the catalog yet, Beauty Router adds it and returns `matched` with the new product
ID. When Beauty Router cannot tell what the product is, it asks for more information.

When a match is created as `matched` or changes status, Beauty Router sends a `product_match.updated`
[webhook](/webhooks/) with the match.

## Answer a question

When a match is `needs_reply`, show the `question` to your user and send their answer with
`PUT /v1/product-matches/{matchId}`. For example:

| Question                                         | Your user's answer           |
| ------------------------------------------------ | ---------------------------- |
| Is this product from Jadewell or Amberlin?       | Jadewell                     |
| Is this the 2 oz tube or the 3 oz tube?          | The 2 oz tube                |
| What does the label say under the product name?  | Demi-Permanent Hair Color 6N |

```bash
curl -X PUT https://api.beautyrouter.com/v1/product-matches/match-9Lk4jHg7Fd \
  -H "Authorization: Bearer $USER_TOKEN" \
  -H "Content-Type: application/json" \
  -d '{ "reply": "Jadewell" }'
```

The match goes back to `in_review`, and the response is the match:

```json
{
  "id": "match-9Lk4jHg7Fd",
  "externalId": "inv-10517",
  "status": "in_review",
  "createdAt": "2026-10-27T09:14:02Z",
  "updatedAt": "2026-10-27T18:02:47Z"
}
```

A match that is not `needs_reply` fails with `reply_not_expected`.

## Get matches

`GET /v1/product-matches/{matchId}` returns one match.

`GET /v1/product-matches` lists matches with [paging](/conventions/#paging). Filter by status with `status`, for
example to find every match that waits for a reply:

```bash
curl "https://api.beautyrouter.com/v1/product-matches?status=needs_reply&limit=100" \
  -H "Authorization: Bearer $USER_TOKEN"
```

## Use a matched product

A `matched` match has the `productId` of a catalog product. Find its details in the [catalog files](/catalog/), read them
with [`GET /v1/catalog/products/{productId}`](/catalog/#product-details), or show them in your salon app with
`showProductDetails(element, productId, options)`. See [Embedded marketplace](/embedded-marketplace/#product-details).

A product that Beauty Router added for your match reaches the catalog files with the next daily delta.