# Ingest a PDP (/guides/ingest-a-pdp)



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

Vizit exposes two purpose-built PDP ingestion endpoints. Both follow the same shape — `PUT /v1/pdps/{identifier_type}/{value}` to submit, `GET` the same path to poll — but they serve different integration patterns. This guide walks each end-to-end and lays out the error catalog and rate-limit posture you should plan around.

## Prerequisites [#prerequisites]

Before any of the requests below will succeed, make sure you have:

* **Client credentials** provisioned by Vizit for your organization and target environment (production, or `dev1`–`dev5`).
* **A Bearer token** obtained via the OAuth 2.0 client-credentials flow described in [Authentication](/standards/authentication). Cache it until expiry — tokens are valid for 24 hours.
* **A request ID strategy.** Send `X-Request-Id` on each request to correlate your logs with Vizit's. If you omit it, Vizit generates one and echoes it on the response.

All examples below assume `$BASE_URL` is the environment-appropriate hostname (e.g., `https://ext.vizit.com`) and `$TOKEN` is your current access token.

## Choose the right endpoint [#choose-the-right-endpoint]

| You want…                                                                                 | Endpoint pair          | What you send                                                         |
| ----------------------------------------------------------------------------------------- | ---------------------- | --------------------------------------------------------------------- |
| Vizit to scrape Amazon for you and score the result                                       | `/v1/pdps/asin/{asin}` | Just the ASIN (and optionally a regional Amazon storefront)           |
| To push your own image URLs and metadata (PIM-style integrations like Syndigo or Salsify) | `/v1/pdps/gtin/{gtin}` | GTIN plus your hero / carousel image URLs and a `product_category_id` |

Both flows return `202 Accepted` immediately. Scoring runs asynchronously; clients **poll** `GET` until `status` transitions from `"processing"` to `"scored"`.

<Callout type="info" title="One identifier per endpoint">
  Don't try to submit the same product to both endpoints. The ASIN flow scrapes Amazon to get its own image set; the GTIN flow stores the images you provide. Mixing them creates duplicate PDPs in your organization's library.
</Callout>

## Walkthrough: scrape an Amazon ASIN [#walkthrough-scrape-an-amazon-asin]

The scraped-content flow is built for services teams onboarding a customer's ASIN catalog and for partner workflows that want Vizit scores across a target brand's listings without managing images themselves.

### 1. Submit the ASIN [#1-submit-the-asin]

```bash
curl -X PUT "$BASE_URL/v1/pdps/asin/B07XVTRJKX" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{"region": "us"}'
```

Path parameter and body:

| Field         | Type     | Required | Default | Notes                                                                                                                    |
| ------------- | -------- | -------- | ------- | ------------------------------------------------------------------------------------------------------------------------ |
| `asin` (path) | `string` | Yes      | —       | Must match `^[A-Z0-9]{10}$` exactly. Lowercase or wrong-length values return `400 INVALID_ASIN` without a retailer call. |
| `region`      | `string` | No       | `"us"`  | Amazon storefront region. Common values: `us`, `uk`, `de`, `ca`. Unsupported values return `400 INVALID_REGION`.         |

Successful response (`202 Accepted`):

```json
{
  "asin": "B07XVTRJKX",
  "region": "us",
  "pdp_id": "9b0a0c4e-3c1d-4b9e-9f08-2c7e3a1f1d77",
  "status": "processing",
  "score_url": "/v1/pdps/asin/B07XVTRJKX?region=us"
}
```

`score_url` is the URL you poll next — it includes the region so you don't have to remember the storefront the PDP was ingested under.

Submitting an ASIN that's already in your library refreshes it (re-scrapes and re-scores) rather than rejecting the request, the same behavior as the in-product refresh button.

### 2. Poll for scores [#2-poll-for-scores]

```bash
curl "$BASE_URL/v1/pdps/asin/B07XVTRJKX?region=us" \
  -H "Authorization: Bearer $TOKEN"
```

While the pipeline is in flight, the response carries `status: "processing"` and the score fields are null. Once scoring lands, `status` flips to `"scored"`, `scored_at` is populated, and the full payload is available:

```json
{
  "asin": "B07XVTRJKX",
  "region": "us",
  "pdp_id": "9b0a0c4e-3c1d-4b9e-9f08-2c7e3a1f1d77",
  "status": "scored",
  "name": "Example Product",
  "category": "Fish Oil Supplements",
  "pdp_score": 67,
  "carousel_score": 52.4,
  "hero": {
    "image_id": "...",
    "image_url": "https://...",
    "hero_score": 72.0,
    "score_breakdown": { "...": "..." },
    "classification": "Packshot"
  },
  "carousel_images": [
    { "image_id": "...", "position": 1, "vizit_score": 55.0, "classification": "Lifestyle" }
  ],
  "asset_mix": { "total_images": 4, "by_classification": { "Packshot": 1, "Lifestyle": 2, "Feature Highlight": 1 } },
  "scored_at": "2026-05-13T19:42:10Z"
}
```

End-to-end latency for a typical Amazon ASIN is on the order of tens of seconds, capped by the scrape and scoring pipeline rather than the API layer. A polling interval of **15–30 seconds** balances time-to-first-result against request volume.

If the PDP is being refreshed and a prior snapshot exists, the response returns the **prior scores** alongside `status: "processing"` so your consumers can keep using the existing data while new scoring runs.

## Walkthrough: submit a GTIN with your own images [#walkthrough-submit-a-gtin-with-your-own-images]

The caller-supplied-content flow is built for PIM partners pushing their own image sets — Syndigo, Salsify, in-house systems — where Vizit's job is to score the assets you provide, not to scrape a retailer.

### 1. Discover a product category [#1-discover-a-product-category]

GTIN submissions require a `product_category_id`. Pull the valid IDs once and cache them:

```bash
curl "$BASE_URL/v1/product-categories" \
  -H "Authorization: Bearer $TOKEN"
```

The ASIN flow doesn't need this — categories are inferred server-side from the scrape.

### 2. Submit the PDP [#2-submit-the-pdp]

```bash
curl -X PUT "$BASE_URL/v1/pdps/gtin/00012345678905" \
  -H "Authorization: Bearer $TOKEN" \
  -H "Content-Type: application/json" \
  -d '{
    "hero_image_url": "https://your-cdn.example.com/products/abc/hero.jpg",
    "carousel_image_urls": [
      "https://your-cdn.example.com/products/abc/carousel1.jpg",
      "https://your-cdn.example.com/products/abc/carousel2.jpg"
    ],
    "product_category_id": "5b1a7c30-7e9b-4f23-9b6f-8c12a18d3a40",
    "name": "Example Product",
    "retailer": "amazon_us"
  }'
```

Vizit downloads the images server-side, stores them, and kicks off the standard scoring pipeline. GTINs accept 8, 12, 13, or 14-digit values.

Successful response (`202 Accepted`):

```json
{
  "gtin": "00012345678905",
  "pdp_id": "...",
  "status": "processing",
  "score_url": "/v1/pdps/gtin/00012345678905"
}
```

### 3. Poll for scores [#3-poll-for-scores]

Same pattern as ASIN. The response shape mirrors `/v1/pdps/asin/{asin}` with `gtin` instead of `asin` and `external_id` instead of `region`.

```bash
curl "$BASE_URL/v1/pdps/gtin/00012345678905" \
  -H "Authorization: Bearer $TOKEN"
```

## Error cheat sheet [#error-cheat-sheet]

Every non-`2xx` response follows the standard error shape: `error_code`, `detail`, `status_code`, `request_id`. Branch on `error_code`, never on `detail`.

### Validation errors (`400`) [#validation-errors-400]

| `error_code`              | Endpoint   | What it means                                                                                         | Likely caller action                                                                               |
| ------------------------- | ---------- | ----------------------------------------------------------------------------------------------------- | -------------------------------------------------------------------------------------------------- |
| `INVALID_ASIN`            | ASIN       | ASIN doesn't match `^[A-Z0-9]{10}$`.                                                                  | Uppercase and validate the format before sending. Don't retry the same value.                      |
| `INVALID_REGION`          | ASIN       | The body's `region` isn't a supported Amazon storefront.                                              | Use one of the documented region codes (`us`, `uk`, `de`, `ca`, …).                                |
| `INVALID_GTIN`            | GTIN       | GTIN isn't 8/12/13/14 digits.                                                                         | Validate format before sending.                                                                    |
| `INVALID_RETAILER`        | GTIN       | The `retailer` value isn't a supported `retailer_region` key.                                         | Use `amazon_us` (default) or another documented retailer region.                                   |
| `CATEGORY_NOT_IN_ORG_ICP` | ASIN, GTIN | The product's category isn't in your organization's ICP (Ideal Customer Profile) configuration.       | Confirm category coverage with Vizit; this PDP can't be scored under the current configuration.    |
| `NO_VALID_CATEGORY`       | ASIN       | The retailer scrape didn't yield a category Vizit could resolve.                                      | Vizit logs the gap automatically; retry later if the category coverage is updated.                 |
| `TOO_MANY_IMAGES` (`413`) | GTIN       | `carousel_image_urls` exceeded 20 entries.                                                            | Trim the carousel to 20 or fewer URLs.                                                             |
| `IMAGE_DOWNLOAD_FAILED`   | GTIN       | One or more submitted image URLs couldn't be fetched. `extra.failed_urls` lists each `{url, reason}`. | Inspect `extra.failed_urls` and fix the source URLs (auth-expired, 404, wrong content-type, etc.). |

### Resource errors (`404`) [#resource-errors-404]

| `error_code`                 | When                                                                                     | What to do                                                                                                                     |
| ---------------------------- | ---------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
| `ASIN_NOT_FOUND_AT_RETAILER` | The ASIN couldn't be resolved at the retailer.                                           | Verify the ASIN exists on the target storefront. Don't retry the same value.                                                   |
| `PDP_NOT_FOUND`              | `GET` for a PDP your organization hasn't ingested (or that lives in a different region). | Check the region query parameter for ASIN GETs; for GTIN GETs, confirm the PDP was previously PUT under the same organization. |

### Auth and rate-limit errors [#auth-and-rate-limit-errors]

| Status                  | Header / Code        | Action                                                                                        |
| ----------------------- | -------------------- | --------------------------------------------------------------------------------------------- |
| `401 Unauthorized`      | —                    | Request a fresh access token and retry once. See [Authentication](/standards/authentication). |
| `403 Forbidden`         | —                    | The token is valid but doesn't have permission for this resource. Contact Vizit.              |
| `429 Too Many Requests` | `Retry-After` header | Back off for `Retry-After` seconds, then resume. Use exponential backoff for repeated 429s.   |

### Upstream errors (`5xx`) [#upstream-errors-5xx]

| `error_code`            | Status | What to do                                                                                       |
| ----------------------- | ------ | ------------------------------------------------------------------------------------------------ |
| `SCRAPE_TIMEOUT`        | `504`  | Retailer scrape timed out. Retry with exponential backoff.                                       |
| `SCRAPE_UPSTREAM_ERROR` | `502`  | Retailer scrape failed. Retry with exponential backoff; if it persists for hours, contact Vizit. |

See [Errors](/standards/errors) for the full shared error format and HTTP status reference.

## Rate limits and retries [#rate-limits-and-retries]

* Per-organization rate limits are enforced at the API gateway. The exact tier is set when Vizit provisions your credentials; treat **a dozen submissions per minute per organization** as a safe baseline unless you've been told otherwise.
* Exceeding the limit returns `429 Too Many Requests` with a `Retry-After` header.
* Retry `429` and `5xx` responses with exponential backoff (start at \~5s, double on each retry, cap at \~60s).
* Do **not** retry `4xx` responses other than `429` until you've fixed the underlying request — the same input will fail the same way.

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

* The full operation contracts are in the API Reference:
  * [`PUT /v1/pdps/asin/{asin}`](/api/product-details/upsertPdpByAsin)
  * [`GET /v1/pdps/asin/{asin}`](/api/product-details/getPdpByAsin)
  * [`PUT /v1/pdps/gtin/{gtin}`](/api/product-details/upsertPdpByGtin)
  * [`GET /v1/pdps/gtin/{gtin}`](/api/product-details/getPdpByGtin)
* For credentials, token acquisition, and security guidance: [Authentication](/standards/authentication).
* For the cross-cutting error response format: [Errors](/standards/errors).
