# How the Score Is Computed (/guides/how-the-score-is-computed)



<span className="eyebrow">
  Guides
</span>

Every `COMPLETED` PDP score response from [`GET /v1/pdps/id/{id}`](/api/product-details/getPdpById) or [`GET /v1/pdps/asin/{asin}`](/api/product-details/getPdpByAsin) says how its `pdp_score` was computed in `score_method`. This guide explains the two methods, the five components of a formula score, and how to read the `score_breakdown` that comes with it.

## Two scoring methods [#two-scoring-methods]

| `score_method` | What you get                                                                                                                                                                                                                                                                                                                                                                      |
| -------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `FORMULA`      | `pdp_score` is the sum of five weighted components. `score_breakdown` shows each component's score, weight, and points, `scoring_version` names the version of the scoring rules behind it, and `formula_version` gives the date its weights took effect; any of these can be `null` (see [below](#when-parts-of-the-breakdown-are-missing)). The four penalty fields are `null`. |
| `LEGACY`       | `pdp_score` comes from Vizit's earlier scoring method. There is no breakdown: `score_breakdown`, `scoring_version`, and `formula_version` are `null`, and the penalty fields (`score_penalty`, `image_count_penalty`, `image_mix_penalty`, `image_order_penalty`) say what lowered the score.                                                                                     |

Responses and exports can carry either method, so handle both. While `status` is `PROCESSING` or `ERROR`, `score_method`, `scoring_version`, `formula_version`, `score_breakdown`, `unavailable_reason`, and `withheld_reason` are all `null`.

### Which method is my score? [#which-method-is-my-score]

Read `score_method` on each response. Don't assume it from your account: while an organization moves from `LEGACY` to `FORMULA`, some of its PDPs can carry one method and some the other, and a PDP switches the next time it's scored.

```python
score = response.json()

if score["status"] != "COMPLETED":
    ...  # still scoring, or failed: no score yet
elif score["unavailable_reason"] == "SCORE_WITHHELD":
    print("withheld:", score["withheld_reason"])  # display as unscored, not 0
elif score["score_method"] == "FORMULA":
    breakdown = score["score_breakdown"]  # can be null, see below
    for name, part in (breakdown or {}).get("components", {}).items():
        print(name, part["points"], "points,", part["points_to_go"], "to go")
else:  # LEGACY
    penalties = [f for f in ("score_penalty", "image_count_penalty",
                             "image_mix_penalty", "image_order_penalty") if score[f]]
    print("lowered by:", penalties)
```

A `FORMULA` score can come without a breakdown; [When parts of the breakdown are missing](#when-parts-of-the-breakdown-are-missing) explains when.

## The five components [#the-five-components]

A formula score looks at five things about the listing:

| Component     | What it measures                                                                                                                                                                                          |
| ------------- | --------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------- |
| `hero`        | The hero image's score.                                                                                                                                                                                   |
| `carousel`    | The average score of the scored carousel images.                                                                                                                                                          |
| `order`       | Whether the carousel images are in the order that earns full credit. With fewer than two scored carousel images, order scores 0.                                                                          |
| `image_count` | How many images besides the hero (carousel images, including informational ones) the listing has against the count its category needs for full credit. Images beyond that count lower this component too. |
| `asset_mix`   | How well the images cover the asset types in the category's ideal content mix.                                                                                                                            |

## Utility, weight, and points [#utility-weight-and-points]

Each component in `score_breakdown.components` carries the same numbers:

* **`utility`** — the component's own score, a whole number from 0 to 100.
* **`weight`** — the component's share of the PDP score, as a fraction from 0 to 1. The five weights add up to 1. Weights vary by category, so the same utility can be worth more in one category than in another.
* **`points`** — what the component adds to `pdp_score`. When shown, the five `points` add up to `pdp_score`.
* **`points_to_go`** — what the component could still add. For each component, `points` plus `points_to_go` is its weight as a whole-number share of 100, so the five `points_to_go` add up to `100 - pdp_score`.

Points are whole numbers, rounded so that they add up exactly to `pdp_score`. That means a component's `points` can differ slightly from `weight × utility`, and a component whose `utility` shows as 100 can still have a point to go.

Here is a listing that scores 67:

| Component     | `utility` | `weight` | `points` | `points_to_go` |
| ------------- | --------- | -------- | -------- | -------------- |
| `hero`        | 72        | 0.30     | 22       | 8              |
| `carousel`    | 52        | 0.25     | 13       | 12             |
| `order`       | 90        | 0.10     | 9        | 1              |
| `image_count` | 67        | 0.15     | 10       | 5              |
| `asset_mix`   | 67        | 0.20     | 13       | 7              |
| **Total**     |           | **1.00** | **67**   | **33**         |

The largest `points_to_go` shows where the listing has the most to gain: here, a stronger carousel (12 points) and a better hero image (8 points).

## Tags and details [#tags-and-details]

Each component also has a `tag`:

* **`COMPUTED`** — the component was scored from the listing's content.
* **`ABSENT_CONTENT`** — there was nothing to score for this component, for example a listing with no carousel images, or a category that sets no content mix target. The component's `utility` is 0.

Three components add a `detail` object that explains their utility. `hero` and `carousel` never have one, so their `detail` is always `null`.

```json
"order": {
  "utility": 90, "weight": 0.1, "points": 9, "points_to_go": 1, "tag": "COMPUTED",
  "detail": { "is_sorted": false }
},
"image_count": {
  "utility": 67, "weight": 0.15, "points": 10, "points_to_go": 5, "tag": "COMPUTED",
  "detail": { "count": 4, "required": 6, "short_by": 2 }
},
"asset_mix": {
  "utility": 67, "weight": 0.2, "points": 13, "points_to_go": 7, "tag": "COMPUTED",
  "detail": {
    "types": [
      { "key": "feature_benefit", "display_name": "Feature/Benefit", "required": 2, "actual": 1, "short_by": 1 },
      { "key": "lifestyle", "display_name": "Lifestyle", "required": 2, "actual": 2, "short_by": 0 },
      { "key": "multi_pack", "display_name": "Multi-Pack", "required": 1, "actual": 0, "short_by": 1 },
      { "key": "size_and_scale", "display_name": "Size and Scale", "required": 1, "actual": 1, "short_by": 0 }
    ]
  }
}
```

* **`order.detail.is_sorted`** is `true` only when the order earns full credit. A `utility` that rounds to 100 can still come with `is_sorted: false`.
* **`image_count.detail`** compares `count`, the listing's images besides the hero (carousel images, including informational ones), with the `required` count. `short_by` is how many images are still missing; it is 0 once the listing has enough, including when it has more than `required`.
* **`asset_mix.detail.types`** has one entry per asset type the category's ideal content mix asks for, with the `required` and `actual` image counts and how many the listing is `short_by`. `display_name` matches the classification labels on images; `key` is a stable identifier to match on in code.

A `detail` is `null` when its component isn't `COMPUTED` or the counts can't be shown.

## When parts of the breakdown are missing [#when-parts-of-the-breakdown-are-missing]

Build your integration so each of these reads cleanly:

* **No breakdown.** `score_breakdown` is `null` for every `LEGACY` score, and for a `FORMULA` score when the breakdown can't be shown. Unless the score is withheld (below), `pdp_score` is still valid. `scoring_version` and `formula_version` can also be `null` on a `FORMULA` score.
* **No points.** When points can't be shown, `points` and `points_to_go` are `null` on every component. `utility`, `weight`, and `tag` are still there.
* **Withheld score.** Sometimes scoring finishes but no formula score can be produced for the listing. The response is then `COMPLETED` with `score_method: "FORMULA"`, `pdp_score: null`, `score_breakdown: null`, and `unavailable_reason: "SCORE_WITHHELD"`; `vizit_certified` and `score_change` are `null` too. `withheld_reason` says why, so you can tell your users what to fix. The hero and carousel image scores and the content mix are still in the response. Show the listing as unscored rather than as 0.

```json
{
  "status": "COMPLETED",
  "pdp_score": null,
  "vizit_certified": null,
  "score_method": "FORMULA",
  "score_breakdown": null,
  "unavailable_reason": "SCORE_WITHHELD",
  "withheld_reason": "HERO_IMAGE_NOT_SCORED"
}
```

| `withheld_reason`           | What it means                                          | What to do                                                 |
| --------------------------- | ------------------------------------------------------ | ---------------------------------------------------------- |
| `HERO_IMAGE_MISSING`        | The listing has no hero image.                         | Add a hero image.                                          |
| `HERO_IMAGE_NOT_SCORED`     | The hero image could not be scored.                    | Check the image URL is reachable, or submit the PDP again. |
| `CAROUSEL_IMAGE_NOT_SCORED` | A carousel image could not be scored.                  | Check the carousel image URLs, or submit the PDP again.    |
| `CAROUSEL_IMAGES_MISSING`   | The listing has no carousel images that can be scored. | Add carousel images.                                       |
| `IMAGE_TYPES_UNRESOLVED`    | Some image types could not be determined yet.          | Try again later.                                           |
| `CATEGORY_NOT_SUPPORTED`    | The product category is not set up for scoring.        | Contact Vizit.                                             |
| `SCORING_ERROR`             | Scoring failed for another reason.                     | Submit the PDP again; contact Vizit if it persists.        |

New codes may be added, so treat a code you don't recognize like `SCORING_ERROR`.

In the CSV export, a withheld score leaves `listing_score` empty and fills the `withheld_reason` column.

## When weights change [#when-weights-change]

Vizit updates its scoring weights from time to time. New weights apply the next time a PDP is scored; an existing score keeps its weights until the PDP is scored again. `formula_version` tells you which weights produced a score, as the date they took effect (for example `2026-09-24`). `listing_score_at_ingest` is the first score a PDP gets after its scoring weights or setup change, and `score_change` is measured from it, so `score_change` is `0` until the PDP is scored again. A new `formula_version` always restarts it; other scoring setup changes can restart it too while `formula_version` stays the same.

## In the CSV export [#in-the-csv-export]

`subject: "pdps"` exports add `score_method`, `scoring_version`, and `formula_version` columns, plus one utility column per component (`hero_utility`, `carousel_utility`, `order_utility`, `image_count_utility`, `asset_mix_utility`) that is filled on `FORMULA` rows. See [PDP scoring columns](/guides/export-scores#pdp-scoring-columns) in the Export Scores guide.

## What's next [#whats-next]

* The full response contracts are in the API Reference:
  * [`GET /v1/pdps/id/{id}`](/api/product-details/getPdpById)
  * [`GET /v1/pdps/asin/{asin}`](/api/product-details/getPdpByAsin)
* To submit PDPs and poll for their scores: [Ingest a PDP](/guides/ingest-a-pdp).
* To pull every PDP's scores at once: [Export Scores](/guides/export-scores).
